summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
commita5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch)
treefb69158581673816a8cd895f9d352dcb3c678b1e /spec
parentd16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff)
downloadgitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'spec')
-rw-r--r--spec/bin/changelog_spec.rb119
-rw-r--r--spec/bin/sidekiq_cluster_spec.rb6
-rw-r--r--spec/config/mail_room_spec.rb4
-rw-r--r--spec/config/metrics/aggregates/aggregated_metrics_spec.rb8
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb2
-rw-r--r--spec/controllers/admin/application_settings/appearances_controller_spec.rb (renamed from spec/controllers/admin/appearances_controller_spec.rb)2
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb21
-rw-r--r--spec/controllers/admin/integrations_controller_spec.rb4
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb8
-rw-r--r--spec/controllers/admin/services_controller_spec.rb2
-rw-r--r--spec/controllers/admin/users_controller_spec.rb89
-rw-r--r--spec/controllers/application_controller_spec.rb4
-rw-r--r--spec/controllers/confirmations_controller_spec.rb64
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb9
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb47
-rw-r--r--spec/controllers/groups/registry/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/groups/settings/integrations_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb58
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb6
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb24
-rw-r--r--spec/controllers/passwords_controller_spec.rb12
-rw-r--r--spec/controllers/profiles_controller_spec.rb13
-rw-r--r--spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb46
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb34
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb15
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb12
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb17
-rw-r--r--spec/controllers/projects/feature_flags_controller_spec.rb94
-rw-r--r--spec/controllers/projects/feature_flags_user_lists_controller_spec.rb33
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb19
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb6
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb124
-rw-r--r--spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb45
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb45
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb74
-rw-r--r--spec/controllers/projects/service_hook_logs_controller_spec.rb8
-rw-r--r--spec/controllers/projects/services_controller_spec.rb8
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb5
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb26
-rw-r--r--spec/controllers/projects_controller_spec.rb18
-rw-r--r--spec/controllers/registrations_controller_spec.rb24
-rw-r--r--spec/controllers/search_controller_spec.rb2
-rw-r--r--spec/db/schema_spec.rb4
-rw-r--r--spec/deprecation_toolkit_env.rb6
-rw-r--r--spec/docs_screenshots/wiki_docs.rb47
-rw-r--r--spec/experiments/application_experiment_spec.rb65
-rw-r--r--spec/experiments/members/invite_email_experiment_spec.rb10
-rw-r--r--spec/factories/bulk_import/trackers.rb6
-rw-r--r--spec/factories/ci/builds.rb32
-rw-r--r--spec/factories/ci/job_artifacts.rb15
-rw-r--r--spec/factories/ci/job_token/project_scope_links.rb9
-rw-r--r--spec/factories/clusters/applications/helm.rb26
-rw-r--r--spec/factories/clusters/clusters.rb6
-rw-r--r--spec/factories/environments.rb8
-rw-r--r--spec/factories/gitlab/database/background_migration/batched_migrations.rb1
-rw-r--r--spec/factories/groups.rb7
-rw-r--r--spec/factories/integration_data.rb6
-rw-r--r--spec/factories/integrations.rb36
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/operations/feature_flag_scopes.rb2
-rw-r--r--spec/factories/operations/feature_flags.rb1
-rw-r--r--spec/factories/packages.rb2
-rw-r--r--spec/factories/packages/debian/component_file.rb6
-rw-r--r--spec/factories/packages/debian/distribution_key.rb16
-rw-r--r--spec/factories/packages/debian/file_metadatum.rb48
-rw-r--r--spec/factories/packages/package_file.rb28
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/factories/user_details.rb1
-rw-r--r--spec/factories_spec.rb3
-rw-r--r--spec/features/admin/admin_appearance_spec.rb20
-rw-r--r--spec/features/admin/admin_hooks_spec.rb18
-rw-r--r--spec/features/admin/admin_mode/logout_spec.rb31
-rw-r--r--spec/features/admin/admin_mode_spec.rb77
-rw-r--r--spec/features/admin/admin_runners_spec.rb58
-rw-r--r--spec/features/admin/admin_search_settings_spec.rb2
-rw-r--r--spec/features/admin/admin_sees_background_migrations_spec.rb84
-rw-r--r--spec/features/admin/admin_settings_spec.rb46
-rw-r--r--spec/features/admin/clusters/applications_spec.rb22
-rw-r--r--spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb16
-rw-r--r--spec/features/admin/users/user_spec.rb30
-rw-r--r--spec/features/admin/users/users_spec.rb458
-rw-r--r--spec/features/alert_management/alert_management_list_spec.rb24
-rw-r--r--spec/features/alerts_settings/user_views_alerts_settings_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb42
-rw-r--r--spec/features/boards/issue_ordering_spec.rb2
-rw-r--r--spec/features/boards/multi_select_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb34
-rw-r--r--spec/features/boards/sidebar_assignee_spec.rb2
-rw-r--r--spec/features/boards/sidebar_milestones_spec.rb4
-rw-r--r--spec/features/calendar_spec.rb13
-rw-r--r--spec/features/clusters/cluster_detail_page_spec.rb24
-rw-r--r--spec/features/clusters/cluster_health_dashboard_spec.rb12
-rw-r--r--spec/features/clusters/installing_applications_shared_examples.rb252
-rw-r--r--spec/features/contextual_sidebar_spec.rb8
-rw-r--r--spec/features/cycle_analytics_spec.rb4
-rw-r--r--spec/features/dashboard/active_tab_spec.rb21
-rw-r--r--spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb19
-rw-r--r--spec/features/dashboard/group_spec.rb2
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb18
-rw-r--r--spec/features/frequently_visited_projects_and_groups_spec.rb14
-rw-r--r--spec/features/groups/clusters/applications_spec.rb23
-rw-r--r--spec/features/groups/group_settings_spec.rb20
-rw-r--r--spec/features/groups/import_export/connect_instance_spec.rb4
-rw-r--r--spec/features/groups/import_export/import_file_spec.rb14
-rw-r--r--spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb16
-rw-r--r--spec/features/groups/members/manage_groups_spec.rb76
-rw-r--r--spec/features/groups/members/manage_members_spec.rb59
-rw-r--r--spec/features/groups/members/tabs_spec.rb6
-rw-r--r--spec/features/groups/milestones/gfm_autocomplete_spec.rb1
-rw-r--r--spec/features/groups/navbar_spec.rb12
-rw-r--r--spec/features/groups/settings/user_searches_in_settings_spec.rb2
-rw-r--r--spec/features/groups_spec.rb38
-rw-r--r--spec/features/incidents/incident_details_spec.rb38
-rw-r--r--spec/features/issues/csv_spec.rb4
-rw-r--r--spec/features/issues/issue_detail_spec.rb63
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb4
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb48
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb5
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb10
-rw-r--r--spec/features/issues/user_resets_their_incoming_email_token_spec.rb4
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb71
-rw-r--r--spec/features/markdown/mermaid_spec.rb97
-rw-r--r--spec/features/markdown/metrics_spec.rb2
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb6
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb4
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb4
-rw-r--r--spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb61
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb12
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb8
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb1
-rw-r--r--spec/features/merge_requests/user_exports_as_csv_spec.rb8
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb53
-rw-r--r--spec/features/profile_spec.rb4
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb11
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb2
-rw-r--r--spec/features/profiles/user_search_settings_spec.rb2
-rw-r--r--spec/features/projects/active_tabs_spec.rb25
-rw-r--r--spec/features/projects/badges/pipeline_badge_spec.rb2
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb22
-rw-r--r--spec/features/projects/branches/user_deletes_branch_spec.rb33
-rw-r--r--spec/features/projects/branches/user_views_branches_spec.rb11
-rw-r--r--spec/features/projects/branches_spec.rb67
-rw-r--r--spec/features/projects/clusters/applications_spec.rb23
-rw-r--r--spec/features/projects/confluence/user_views_confluence_page_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb22
-rw-r--r--spec/features/projects/environments/environments_spec.rb2
-rw-r--r--spec/features/projects/environments_pod_logs_spec.rb2
-rw-r--r--spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb7
-rw-r--r--spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb58
-rw-r--r--spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb8
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb10
-rw-r--r--spec/features/projects/infrastructure_registry_spec.rb61
-rw-r--r--spec/features/projects/integrations/user_activates_flowdock_spec.rb (renamed from spec/features/projects/services/user_activates_flowdock_spec.rb)0
-rw-r--r--spec/features/projects/integrations/user_activates_jira_spec.rb (renamed from spec/features/projects/services/user_activates_jira_spec.rb)0
-rw-r--r--spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb (renamed from spec/features/projects/services/user_activates_pivotaltracker_spec.rb)0
-rw-r--r--spec/features/projects/members/invite_group_spec.rb8
-rw-r--r--spec/features/projects/members/list_spec.rb50
-rw-r--r--spec/features/projects/members/tabs_spec.rb4
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb7
-rw-r--r--spec/features/projects/navbar_spec.rb4
-rw-r--r--spec/features/projects/new_project_spec.rb52
-rw-r--r--spec/features/projects/pages/user_edits_settings_spec.rb6
-rw-r--r--spec/features/projects/releases/user_views_releases_spec.rb172
-rw-r--r--spec/features/projects/serverless/functions_spec.rb1
-rw-r--r--spec/features/projects/services/disable_triggers_spec.rb2
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb35
-rw-r--r--spec/features/projects/services/user_activates_slack_notifications_spec.rb2
-rw-r--r--spec/features/projects/settings/monitor_settings_spec.rb16
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb12
-rw-r--r--spec/features/projects/settings/service_desk_setting_spec.rb19
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/user_searches_in_settings_spec.rb2
-rw-r--r--spec/features/projects/show/schema_markup_spec.rb4
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb12
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb8
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb23
-rw-r--r--spec/features/projects/wiki/user_views_wiki_empty_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb7
-rw-r--r--spec/features/projects_spec.rb14
-rw-r--r--spec/features/protected_branches_spec.rb52
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb7
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb4
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb8
-rw-r--r--spec/features/security/project/snippet/private_access_spec.rb81
-rw-r--r--spec/features/users/show_spec.rb8
-rw-r--r--spec/features/users/signup_spec.rb2
-rw-r--r--spec/finders/ci/auth_job_finder_spec.rb30
-rw-r--r--spec/finders/ci/runners_finder_spec.rb58
-rw-r--r--spec/finders/concerns/packages/finder_helper_spec.rb82
-rw-r--r--spec/finders/deployments_finder_spec.rb24
-rw-r--r--spec/finders/feature_flags_finder_spec.rb8
-rw-r--r--spec/finders/issues_finder_spec.rb10
-rw-r--r--spec/finders/packages/helm/package_files_finder_spec.rb47
-rw-r--r--spec/finders/packages/maven/package_finder_spec.rb136
-rw-r--r--spec/finders/packages/pypi/package_finder_spec.rb10
-rw-r--r--spec/finders/pending_todos_finder_spec.rb28
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb2
-rw-r--r--spec/finders/projects_finder_spec.rb35
-rw-r--r--spec/finders/security/security_jobs_finder_spec.rb6
-rw-r--r--spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json21
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json2
-rw-r--r--spec/fixtures/api/schemas/entities/issue_board.json3
-rw-r--r--spec/fixtures/api/schemas/external_validation.json4
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json1
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json13
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/board.json2
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/label_basic.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/project.json8
-rw-r--r--spec/fixtures/bulk_imports/boards.ndjson1
-rw-r--r--spec/fixtures/bulk_imports/gz/boards.ndjson.gzbin0 -> 432 bytes
-rw-r--r--spec/fixtures/bulk_imports/gz/labels.ndjson.gz (renamed from spec/fixtures/bulk_imports/labels.ndjson.gz)bin202 -> 202 bytes
-rw-r--r--spec/fixtures/bulk_imports/gz/milestones.ndjson.gzbin0 -> 402 bytes
-rw-r--r--spec/fixtures/bulk_imports/labels.ndjson1
-rw-r--r--spec/fixtures/bulk_imports/milestones.ndjson5
-rw-r--r--spec/fixtures/config/redis_cache_config_with_env.yml2
-rw-r--r--spec/fixtures/config/redis_cache_new_format_host.yml29
-rw-r--r--spec/fixtures/config/redis_cache_new_format_socket.yml6
-rw-r--r--spec/fixtures/config/redis_cache_old_format_host.yml5
-rw-r--r--spec/fixtures/config/redis_cache_old_format_socket.yml3
-rw-r--r--spec/fixtures/config/redis_new_format_host.yml18
-rw-r--r--spec/fixtures/config/redis_queues_config_with_env.yml2
-rw-r--r--spec/fixtures/config/redis_queues_new_format_host.yml29
-rw-r--r--spec/fixtures/config/redis_queues_new_format_socket.yml6
-rw-r--r--spec/fixtures/config/redis_queues_old_format_host.yml5
-rw-r--r--spec/fixtures/config/redis_queues_old_format_socket.yml3
-rw-r--r--spec/fixtures/config/redis_shared_state_config_with_env.yml2
-rw-r--r--spec/fixtures/config/redis_shared_state_new_format_host.yml29
-rw-r--r--spec/fixtures/config/redis_shared_state_new_format_socket.yml6
-rw-r--r--spec/fixtures/config/redis_shared_state_old_format_host.yml5
-rw-r--r--spec/fixtures/config/redis_shared_state_old_format_socket.yml3
-rw-r--r--spec/fixtures/dns/a_rr.json4
-rw-r--r--spec/fixtures/dns/a_with_aaaa_rr_in_additional_section.json4
-rw-r--r--spec/fixtures/dns/aaaa_rr.json4
-rw-r--r--spec/fixtures/dns/srv_with_a_rr_in_additional_section.json4
-rw-r--r--spec/fixtures/helm/helm_list_v2_cilium_deployed.json.gzbin302 -> 0 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_cilium_failed.json.gzbin304 -> 0 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_cilium_missing.json.gzbin320 -> 0 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_empty_blob.json.gzbin81 -> 0 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gzbin338 -> 0 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gzbin339 -> 0 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gzbin320 -> 0 bytes
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric.yml1
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml1
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml1
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric.rb14
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb7
-rw-r--r--spec/fixtures/packages/debian/sample-dev_1.2.3~binary_amd64.debbin1164 -> 1164 bytes
-rw-r--r--spec/fixtures/packages/debian/sample/debian/control2
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc6
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xzbin864 -> 864 bytes
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo275
-rw-r--r--spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes26
-rw-r--r--spec/fixtures/product_intelligence/survey_response_schema.json8
-rw-r--r--spec/fixtures/whats_new/20201225_01_05.yml2
-rw-r--r--spec/frontend/__helpers__/mock_user_callout_dismisser.js16
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper.js11
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper_spec.js3
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js32
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap (renamed from spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap)17
-rw-r--r--spec/frontend/alerts_settings/components/alerts_form_spec.js (renamed from spec/frontend/incidents_settings/components/alerts_form_spec.js)3
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js11
-rw-r--r--spec/frontend/api_spec.js68
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js4
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js6
-rw-r--r--spec/frontend/behaviors/shortcuts/keybindings_spec.js11
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js67
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js20
-rw-r--r--spec/frontend/boards/board_list_spec.js7
-rw-r--r--spec/frontend/boards/boards_util_spec.js112
-rw-r--r--spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap2
-rw-r--r--spec/frontend/boards/components/board_card_spec.js38
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js32
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js4
-rw-r--r--spec/frontend/boards/components/board_form_spec.js20
-rw-r--r--spec/frontend/boards/components/board_list_header_deprecated_spec.js13
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js14
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js137
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js178
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js11
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js13
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js6
-rw-r--r--spec/frontend/boards/project_select_deprecated_spec.js8
-rw-r--r--spec/frontend/boards/stores/actions_spec.js171
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js47
-rw-r--r--spec/frontend/branches/components/delete_branch_button_spec.js96
-rw-r--r--spec/frontend/branches/components/delete_branch_modal_spec.js157
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js12
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap2
-rw-r--r--spec/frontend/clusters/components/applications_spec.js41
-rw-r--r--spec/frontend/clusters/components/fluentd_output_settings_spec.js186
-rw-r--r--spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js192
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js13
-rw-r--r--spec/frontend/clusters/services/mock_data.js3
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js23
-rw-r--r--spec/frontend/commit/pipelines/pipelines_spec.js280
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js253
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap36
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js9
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js151
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js131
-rw-r--r--spec/frontend/content_editor/components/top_toolbar_spec.js19
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js37
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js61
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js45
-rw-r--r--spec/frontend/content_editor/test_utils.js132
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js6
-rw-r--r--spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap9
-rw-r--r--spec/frontend/cycle_analytics/base_spec.js197
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js175
-rw-r--r--spec/frontend/cycle_analytics/path_navigation_spec.js148
-rw-r--r--spec/frontend/cycle_analytics/store/actions_spec.js191
-rw-r--r--spec/frontend/cycle_analytics/store/getters_spec.js16
-rw-r--r--spec/frontend/cycle_analytics/store/mutations_spec.js34
-rw-r--r--spec/frontend/cycle_analytics/utils_spec.js129
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js8
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js1
-rw-r--r--spec/frontend/diffs/components/diff_stats_spec.js53
-rw-r--r--spec/frontend/diffs/components/settings_dropdown_spec.js1
-rw-r--r--spec/frontend/diffs/mock_data/diff_file.js2
-rw-r--r--spec/frontend/diffs/store/actions_spec.js62
-rw-r--r--spec/frontend/diffs/store/utils_spec.js328
-rw-r--r--spec/frontend/diffs/utils/diff_file_spec.js78
-rw-r--r--spec/frontend/diffs/utils/workers_spec.js309
-rw-r--r--spec/frontend/editor/editor_ci_schema_ext_spec.js7
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js191
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js10
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/empty_state_spec.js (renamed from spec/frontend/feature_flags/components/feature_flags_tab_spec.js)43
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js145
-rw-r--r--spec/frontend/feature_flags/store/index/actions_spec.js165
-rw-r--r--spec/frontend/feature_flags/store/index/mutations_spec.js94
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js2
-rw-r--r--spec/frontend/fixtures/api_markdown.rb2
-rw-r--r--spec/frontend/fixtures/releases.rb11
-rw-r--r--spec/frontend/fixtures/runner.rb75
-rw-r--r--spec/frontend/fixtures/services.rb2
-rw-r--r--spec/frontend/fixtures/startup_css.rb88
-rw-r--r--spec/frontend/fixtures/static/projects.json9
-rw-r--r--spec/frontend/flash_spec.js49
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js18
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap42
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js22
-rw-r--r--spec/frontend/groups/components/group_item_spec.js122
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js14
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js4
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js2
-rw-r--r--spec/frontend/ide/components/nav_dropdown_spec.js4
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js2
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js18
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js6
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js8
-rw-r--r--spec/frontend/ide/ide_router_spec.js12
-rw-r--r--spec/frontend/ide/mock_data.js14
-rw-r--r--spec/frontend/ide/services/index_spec.js2
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js16
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js14
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js32
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js22
-rw-r--r--spec/frontend/ide/stores/actions_spec.js6
-rw-r--r--spec/frontend/ide/stores/getters_spec.js36
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js22
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js12
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js6
-rw-r--r--spec/frontend/ide/stores/mutations/branch_spec.js10
-rw-r--r--spec/frontend/ide/stores/mutations/file_spec.js6
-rw-r--r--spec/frontend/ide/stores/mutations/tree_spec.js24
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js22
-rw-r--r--spec/frontend/ide/stores/utils_spec.js18
-rw-r--r--spec/frontend/ide/utils_spec.js34
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_row_spec.js117
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js18
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js31
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap18
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap16
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js7
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js23
-rw-r--r--spec/frontend/integrations/edit/components/active_checkbox_spec.js1
-rw-r--r--spec/frontend/integrations/edit/components/confirmation_modal_spec.js8
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js16
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js61
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js54
-rw-r--r--spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js18
-rw-r--r--spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js9
-rw-r--r--spec/frontend/integrations/edit/components/override_dropdown_spec.js10
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js23
-rw-r--r--spec/frontend/integrations/index/components/integrations_list_spec.js6
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js33
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js50
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js16
-rw-r--r--spec/frontend/issuable/components/csv_export_modal_spec.js44
-rw-r--r--spec/frontend/issuable/components/csv_import_export_buttons_spec.js58
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js48
-rw-r--r--spec/frontend/issuable/components/issuable_by_email_spec.js11
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js4
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js6
-rw-r--r--spec/frontend/issuable_list/components/issuable_item_spec.js4
-rw-r--r--spec/frontend/issue_show/components/app_spec.js41
-rw-r--r--spec/frontend/issue_show/components/description_spec.js2
-rw-r--r--spec/frontend/issue_show/components/edit_actions_spec.js180
-rw-r--r--spec/frontend/issue_show/components/fields/type_spec.js84
-rw-r--r--spec/frontend/issue_show/components/form_spec.js17
-rw-r--r--spec/frontend/issue_show/components/incidents/incident_tabs_spec.js2
-rw-r--r--spec/frontend/issue_show/issue_spec.js2
-rw-r--r--spec/frontend/issue_show/mock_data/apollo_mock.js9
-rw-r--r--spec/frontend/issue_show/mock_data/mock_data.js (renamed from spec/frontend/issue_show/mock_data.js)1
-rw-r--r--spec/frontend/issues_list/components/issuables_list_app_spec.js5
-rw-r--r--spec/frontend/issues_list/components/issues_list_app_spec.js134
-rw-r--r--spec/frontend/issues_list/mock_data.js14
-rw-r--r--spec/frontend/issues_list/utils_spec.js10
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap6
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js2
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js90
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js17
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js4
-rw-r--r--spec/frontend/lib/utils/table_utility_spec.js11
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js88
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js3
-rw-r--r--spec/frontend/logs/mock_data.js27
-rw-r--r--spec/frontend/logs/stores/actions_spec.js27
-rw-r--r--spec/frontend/logs/stores/getters_spec.js48
-rw-r--r--spec/frontend/logs/stores/mutations_spec.js36
-rw-r--r--spec/frontend/members/components/app_spec.js2
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js10
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js2
-rw-r--r--spec/frontend/members/components/table/expires_at_spec.js2
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js2
-rw-r--r--spec/frontend/members/index_spec.js19
-rw-r--r--spec/frontend/members/mock_data.js13
-rw-r--r--spec/frontend/members/utils_spec.js10
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js2
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap1
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js8
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js26
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js44
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js2
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js14
-rw-r--r--spec/frontend/nav/components/responsive_app_spec.js173
-rw-r--r--spec/frontend/nav/components/responsive_header_spec.js67
-rw-r--r--spec/frontend/nav/components/responsive_home_spec.js137
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js31
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js60
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js121
-rw-r--r--spec/frontend/nav/components/top_nav_menu_item_spec.js76
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js107
-rw-r--r--spec/frontend/nav/components/top_nav_new_dropdown_spec.js122
-rw-r--r--spec/frontend/nav/mock_data.js4
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js41
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js11
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js23
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js12
-rw-r--r--spec/frontend/notes/stores/actions_spec.js180
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js16
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap30
-rw-r--r--spec/frontend/packages/details/components/app_spec.js109
-rw-r--r--spec/frontend/packages/details/components/file_sha_spec.js33
-rw-r--r--spec/frontend/packages/details/components/installations_commands_spec.js4
-rw-r--r--spec/frontend/packages/details/components/package_files_spec.js132
-rw-r--r--spec/frontend/packages/details/store/actions_spec.js62
-rw-r--r--spec/frontend/packages/details/store/mutations_spec.js9
-rw-r--r--spec/frontend/packages/list/stores/actions_spec.js6
-rw-r--r--spec/frontend/packages/mock_data.js17
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/__snapshots__/terraform_installation_spec.js.snap44
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details_title_spec.js93
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/terraform_installation_spec.js61
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js26
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap19
-rw-r--r--spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js49
-rw-r--r--spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js4
-rw-r--r--spec/frontend/pages/projects/forks/new/components/app_spec.js1
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js182
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js2
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap2
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap2
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap2
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js27
-rw-r--r--spec/frontend/pages/shared/nav/sidebar_tracking_spec.js160
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js115
-rw-r--r--spec/frontend/pages/users/activity_calendar_spec.js16
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js9
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js12
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js31
-rw-r--r--spec/frontend/pipeline_editor/components/editor/text_editor_spec.js51
-rw-r--r--spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js153
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js1
-rw-r--r--spec/frontend/pipelines/components/dag/mock_data.js14
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js6
-rw-r--r--spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap7
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js23
-rw-r--r--spec/frontend/pipelines/header_component_spec.js16
-rw-r--r--spec/frontend/pipelines/mock_data.js51
-rw-r--r--spec/frontend/pipelines/notification/pipeline_notification_spec.js79
-rw-r--r--spec/frontend/pipelines/parsing_utils_spec.js10
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js32
-rw-r--r--spec/frontend/pipelines/pipeline_graph/utils_spec.js22
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js32
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js2
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js12
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js7
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js6
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js6
-rw-r--r--spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap1
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js8
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js53
-rw-r--r--spec/frontend/related_merge_requests/store/actions_spec.js6
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap56
-rw-r--r--spec/frontend/releases/components/app_index_apollo_client_spec.js394
-rw-r--r--spec/frontend/releases/components/releases_empty_state_spec.js56
-rw-r--r--spec/frontend/releases/components/releases_pagination_apollo_client_spec.js126
-rw-r--r--spec/frontend/releases/components/releases_sort_apollo_client_spec.js103
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js26
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js3
-rw-r--r--spec/frontend/reports/codequality_report/store/mutations_spec.js9
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js69
-rw-r--r--spec/frontend/repository/components/blob_header_edit_spec.js82
-rw-r--r--spec/frontend/repository/components/blob_replace_spec.js67
-rw-r--r--spec/frontend/repository/components/table/row_spec.js22
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js5
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js80
-rw-r--r--spec/frontend/repository/log_tree_spec.js15
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js201
-rw-r--r--spec/frontend/runner/components/cells/runner_name_cell_spec.js42
-rw-r--r--spec/frontend/runner/components/cells/runner_type_cell_spec.js48
-rw-r--r--spec/frontend/runner/components/runner_filtered_search_bar_spec.js137
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js130
-rw-r--r--spec/frontend/runner/components/runner_manual_setup_help_spec.js84
-rw-r--r--spec/frontend/runner/components/runner_pagination_spec.js160
-rw-r--r--spec/frontend/runner/components/runner_tags_spec.js64
-rw-r--r--spec/frontend/runner/components/runner_type_alert_spec.js61
-rw-r--r--spec/frontend/runner/components/runner_type_badge_spec.js10
-rw-r--r--spec/frontend/runner/components/runner_type_help_spec.js32
-rw-r--r--spec/frontend/runner/components/runner_update_form_spec.js263
-rw-r--r--spec/frontend/runner/mock_data.js6
-rw-r--r--spec/frontend/runner/runner_detail/runner_details_app_spec.js21
-rw-r--r--spec/frontend/runner/runner_list/runner_list_app_spec.js232
-rw-r--r--spec/frontend/runner/runner_list/runner_search_utils_spec.js239
-rw-r--r--spec/frontend/search/mock_data.js28
-rw-r--r--spec/frontend/search/store/actions_spec.js21
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js97
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js58
-rw-r--r--spec/frontend/security_configuration/components/redesigned_app_spec.js232
-rw-r--r--spec/frontend/security_configuration/components/section_layout_spec.js49
-rw-r--r--spec/frontend/security_configuration/components/upgrade_banner_spec.js60
-rw-r--r--spec/frontend/security_configuration/utils_spec.js81
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap4
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap2
-rw-r--r--spec/frontend/serverless/components/missing_prometheus_spec.js2
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js28
-rw-r--r--spec/frontend/sidebar/assignees_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js11
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js503
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js2
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js131
-rw-r--r--spec/frontend/sidebar/mock_data.js96
-rw-r--r--spec/frontend/sidebar/track_invite_members_spec.js37
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap1
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js8
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/editor_service_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js)14
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js)6
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/modals/insert_video_modal_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_integration_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js)6
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js)12
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/build_custom_renderer_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/mock_data.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js)0
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js)4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js)4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_heading_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js)4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_html_block_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js)4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js)4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_list_item_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js)4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_softbreak_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_utils_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js)4
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/services/sanitize_html_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/services/sanitize_html_spec.js)2
-rw-r--r--spec/frontend/static_site_editor/rich_content_editor/toolbar_item_spec.js (renamed from spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js)2
-rw-r--r--spec/frontend/tracking/get_standard_context_spec.js53
-rw-r--r--spec/frontend/tracking_spec.js152
-rw-r--r--spec/frontend/user_lists/components/user_lists_spec.js195
-rw-r--r--spec/frontend/user_lists/components/user_lists_table_spec.js (renamed from spec/frontend/feature_flags/components/user_lists_table_spec.js)6
-rw-r--r--spec/frontend/user_lists/store/index/actions_spec.js203
-rw-r--r--spec/frontend/user_lists/store/index/mutations_spec.js121
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js77
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js47
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js10
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js6
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js38
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js6
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js53
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap19
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/alert_details_table_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js212
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap110
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js127
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js90
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js84
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js57
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js (renamed from spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js17
-rw-r--r--spec/frontend/vue_shared/components/user_callout_dismisser_mock_data.js30
-rw-r--r--spec/frontend/vue_shared/components/user_callout_dismisser_spec.js306
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js4
-rw-r--r--spec/frontend/whats_new/components/feature_spec.js12
-rw-r--r--spec/generator_helper.rb15
-rw-r--r--spec/graphql/mutations/ci/runner/delete_spec.rb92
-rw-r--r--spec/graphql/mutations/ci/runner/update_spec.rb95
-rw-r--r--spec/graphql/mutations/commits/create_spec.rb5
-rw-r--r--spec/graphql/mutations/design_management/upload_spec.rb2
-rw-r--r--spec/graphql/mutations/issues/set_subscription_spec.rb36
-rw-r--r--spec/graphql/mutations/issues/update_spec.rb30
-rw-r--r--spec/graphql/mutations/merge_requests/set_subscription_spec.rb28
-rw-r--r--spec/graphql/mutations/todos/mark_all_done_spec.rb7
-rw-r--r--spec/graphql/resolvers/boards_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runners_resolver_spec.rb66
-rw-r--r--spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb1
-rw-r--r--spec/graphql/resolvers/group_packages_resolver_spec.rb22
-rw-r--r--spec/graphql/resolvers/package_details_resolver_spec.rb1
-rw-r--r--spec/graphql/resolvers/projects_resolver_spec.rb18
-rw-r--r--spec/graphql/resolvers/timelog_resolver_spec.rb131
-rw-r--r--spec/graphql/types/ci/runner_type_spec.rb6
-rw-r--r--spec/graphql/types/global_id_type_spec.rb146
-rw-r--r--spec/graphql/types/label_type_spec.rb1
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb33
-rw-r--r--spec/graphql/types/mutation_type_spec.rb22
-rw-r--r--spec/graphql/types/packages/package_group_sort_enum_spec.rb9
-rw-r--r--spec/graphql/types/packages/package_sort_enum_spec.rb9
-rw-r--r--spec/graphql/types/packages/pypi/metadatum_type_spec.rb13
-rw-r--r--spec/graphql/types/projects/service_type_spec.rb4
-rw-r--r--spec/graphql/types/projects/services_enum_spec.rb8
-rw-r--r--spec/graphql/types/snippet_type_spec.rb28
-rw-r--r--spec/graphql/types/snippets/blob_viewer_type_spec.rb22
-rw-r--r--spec/graphql/types/timelog_type_spec.rb2
-rw-r--r--spec/helpers/admin/background_migrations_helper_spec.rb67
-rw-r--r--spec/helpers/application_settings_helper_spec.rb20
-rw-r--r--spec/helpers/commits_helper_spec.rb16
-rw-r--r--spec/helpers/environments_helper_spec.rb5
-rw-r--r--spec/helpers/events_helper_spec.rb7
-rw-r--r--spec/helpers/gitlab_script_tag_helper_spec.rb7
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb106
-rw-r--r--spec/helpers/invite_members_helper_spec.rb65
-rw-r--r--spec/helpers/issuables_description_templates_helper_spec.rb93
-rw-r--r--spec/helpers/issuables_helper_spec.rb14
-rw-r--r--spec/helpers/issues_helper_spec.rb6
-rw-r--r--spec/helpers/keyset_helper_spec.rb94
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb320
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb235
-rw-r--r--spec/helpers/notify_helper_spec.rb32
-rw-r--r--spec/helpers/operations_helper_spec.rb18
-rw-r--r--spec/helpers/packages_helper_spec.rb169
-rw-r--r--spec/helpers/preferences_helper_spec.rb37
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb2
-rw-r--r--spec/helpers/projects/project_members_helper_spec.rb75
-rw-r--r--spec/helpers/registrations_helper_spec.rb6
-rw-r--r--spec/helpers/user_callouts_helper_spec.rb20
-rw-r--r--spec/helpers/webpack_helper_spec.rb3
-rw-r--r--spec/initializers/database_config_spec.rb26
-rw-r--r--spec/initializers/global_id_spec.rb35
-rw-r--r--spec/initializers/lograge_spec.rb56
-rw-r--r--spec/initializers/mailer_retries_spec.rb25
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb11
-rw-r--r--spec/lib/api/helpers/runner_helpers_spec.rb71
-rw-r--r--spec/lib/api/helpers/runner_spec.rb69
-rw-r--r--spec/lib/api/helpers_spec.rb60
-rw-r--r--spec/lib/backup/gitaly_backup_spec.rb110
-rw-r--r--spec/lib/backup/gitaly_rpc_backup_spec.rb153
-rw-r--r--spec/lib/backup/repositories_spec.rb191
-rw-r--r--spec/lib/banzai/filter/references/label_reference_filter_spec.rb68
-rw-r--r--spec/lib/banzai/filter/references/reference_cache_spec.rb7
-rw-r--r--spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb14
-rw-r--r--spec/lib/banzai/pipeline/post_process_pipeline_spec.rb13
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb4
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb10
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb21
-rw-r--r--spec/lib/bulk_imports/clients/http_spec.rb42
-rw-r--r--spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb53
-rw-r--r--spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb2
-rw-r--r--spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb2
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb35
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/boards_pipeline_spec.rb49
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb34
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb102
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb110
-rw-r--r--spec/lib/bulk_imports/ndjson_pipeline_spec.rb186
-rw-r--r--spec/lib/bulk_imports/pipeline/context_spec.rb23
-rw-r--r--spec/lib/bulk_imports/pipeline_spec.rb2
-rw-r--r--spec/lib/bulk_imports/stage_spec.rb3
-rw-r--r--spec/lib/csv_builder_spec.rb12
-rw-r--r--spec/lib/expand_variables_spec.rb40
-rw-r--r--spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb4
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb4
-rw-r--r--spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb4
-rw-r--r--spec/lib/generators/gitlab/usage_metric_generator_spec.rb70
-rw-r--r--spec/lib/gitlab/application_context_spec.rb18
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb8
-rw-r--r--spec/lib/gitlab/auth_spec.rb26
-rw-r--r--spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb82
-rw-r--r--spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb142
-rw-r--r--spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb34
-rw-r--r--spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb44
-rw-r--r--spec/lib/gitlab/cache/import/caching_spec.rb12
-rw-r--r--spec/lib/gitlab/cache_spec.rb12
-rw-r--r--spec/lib/gitlab/checks/changes_access_spec.rb42
-rw-r--r--spec/lib/gitlab/checks/lfs_check_spec.rb10
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb30
-rw-r--r--spec/lib/gitlab/checks/matching_merge_request_spec.rb68
-rw-r--r--spec/lib/gitlab/checks/single_change_access_spec.rb (renamed from spec/lib/gitlab/checks/change_access_spec.rb)10
-rw-r--r--spec/lib/gitlab/ci/ansi2json/line_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/badge/coverage/template_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/badge/pipeline/template_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/need_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/jwt_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/matching/build_matcher_spec.rb64
-rw-r--r--spec/lib/gitlab/ci/matching/runner_matcher_spec.rb113
-rw-r--r--spec/lib/gitlab/ci/parsers/test/junit_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb59
-rw-r--r--spec/lib/gitlab/ci/pipeline/preloader_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/npm_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/templates/templates_spec.rb162
-rw-r--r--spec/lib/gitlab/ci/trace/chunked_io_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/variables/collection/sort_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb76
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/result_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb25
-rw-r--r--spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb2
-rw-r--r--spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb117
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb1
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb9
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/wiki_page_spec.rb5
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb10
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb63
-rw-r--r--spec/lib/gitlab/database/consistency_spec.rb25
-rw-r--r--spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb2
-rw-r--r--spec/lib/gitlab/database/dynamic_model_helpers_spec.rb88
-rw-r--r--spec/lib/gitlab/database/load_balancing/active_record_proxy_spec.rb20
-rw-r--r--spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb316
-rw-r--r--spec/lib/gitlab/database/load_balancing/host_list_spec.rb188
-rw-r--r--spec/lib/gitlab/database/load_balancing/host_spec.rb445
-rw-r--r--spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb522
-rw-r--r--spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb243
-rw-r--r--spec/lib/gitlab/database/load_balancing/resolver_spec.rb80
-rw-r--r--spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb252
-rw-r--r--spec/lib/gitlab/database/load_balancing/session_spec.rb353
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb161
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb201
-rw-r--r--spec/lib/gitlab/database/load_balancing/srv_resolver_spec.rb61
-rw-r--r--spec/lib/gitlab/database/load_balancing/sticking_spec.rb307
-rw-r--r--spec/lib/gitlab/database/load_balancing_spec.rb834
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb35
-rw-r--r--spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb124
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb68
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb6
-rw-r--r--spec/lib/gitlab/database_spec.rb34
-rw-r--r--spec/lib/gitlab/diff/highlight_cache_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb20
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb8
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb20
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb11
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb3
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb64
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb3
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb3
-rw-r--r--spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb3
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb16
-rw-r--r--spec/lib/gitlab/emoji_spec.rb11
-rw-r--r--spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb10
-rw-r--r--spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb10
-rw-r--r--spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb10
-rw-r--r--spec/lib/gitlab/error_tracking_spec.rb17
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb18
-rw-r--r--spec/lib/gitlab/experimentation/controller_concern_spec.rb21
-rw-r--r--spec/lib/gitlab/file_hook_spec.rb2
-rw-r--r--spec/lib/gitlab/git/conflict/resolver_spec.rb32
-rw-r--r--spec/lib/gitlab/git/remote_repository_spec.rb41
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb32
-rw-r--r--spec/lib/gitlab/git_access_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb51
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb132
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb31
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb118
-rw-r--r--spec/lib/gitlab/github_import/page_counter_spec.rb11
-rw-r--r--spec/lib/gitlab/global_id/deprecations_spec.rb46
-rw-r--r--spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/unicorn_check_spec.rb67
-rw-r--r--spec/lib/gitlab/highlight_spec.rb24
-rw-r--r--spec/lib/gitlab/hook_data/issue_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb22
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml28
-rw-r--r--spec/lib/gitlab/import_export/base/relation_factory_spec.rb9
-rw-r--r--spec/lib/gitlab/import_export/command_line_util_spec.rb23
-rw-r--r--spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/group/tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/import_failure_service_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_writer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project/export_task_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/import_task_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/gitlab/import_export/shared_spec.rb22
-rw-r--r--spec/lib/gitlab/instrumentation/redis_base_spec.rb18
-rw-r--r--spec/lib/gitlab/instrumentation/redis_spec.rb43
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb36
-rw-r--r--spec/lib/gitlab/json_spec.rb12
-rw-r--r--spec/lib/gitlab/kas/client_spec.rb84
-rw-r--r--spec/lib/gitlab/kas_spec.rb6
-rw-r--r--spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb100
-rw-r--r--spec/lib/gitlab/mail_room/mail_room_spec.rb4
-rw-r--r--spec/lib/gitlab/markdown_cache/field_data_spec.rb7
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb38
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb141
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb136
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb40
-rw-r--r--spec/lib/gitlab/metrics_spec.rb20
-rw-r--r--spec/lib/gitlab/nav/top_nav_menu_item_spec.rb4
-rw-r--r--spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb2
-rw-r--r--spec/lib/gitlab/pagination/keyset/paginator_spec.rb120
-rw-r--r--spec/lib/gitlab/pagination/keyset/request_context_spec.rb15
-rw-r--r--spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb33
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb4
-rw-r--r--spec/lib/gitlab/profiler_spec.rb8
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb35
-rw-r--r--spec/lib/gitlab/prometheus/adapter_spec.rb24
-rw-r--r--spec/lib/gitlab/redis/cache_spec.rb22
-rw-r--r--spec/lib/gitlab/redis/queues_spec.rb22
-rw-r--r--spec/lib/gitlab/redis/shared_state_spec.rb22
-rw-r--r--spec/lib/gitlab/redis/trace_chunks_spec.rb55
-rw-r--r--spec/lib/gitlab/redis/wrapper_spec.rb42
-rw-r--r--spec/lib/gitlab/regex_spec.rb11
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb25
-rw-r--r--spec/lib/gitlab/runtime_spec.rb19
-rw-r--r--spec/lib/gitlab/sidekiq_cluster/cli_spec.rb165
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb147
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb82
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb54
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb105
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/size_limiter/compressor_spec.rb200
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb33
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb186
-rw-r--r--spec/lib/gitlab/sidekiq_middleware_spec.rb303
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb49
-rw-r--r--spec/lib/gitlab/time_tracking_formatter_spec.rb6
-rw-r--r--spec/lib/gitlab/tracking/standard_context_spec.rb6
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb18
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb31
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb5
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb5
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb12
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb15
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/hostname_metric_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb28
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb4
-rw-r--r--spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb94
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb18
-rw-r--r--spec/lib/gitlab/usage/metrics/query_spec.rb51
-rw-r--r--spec/lib/gitlab/usage_data/topology_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_counters/merge_request_activity_unique_counter_spec.rb24
-rw-r--r--spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_queries_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb170
-rw-r--r--spec/lib/gitlab/utils/measuring_spec.rb2
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb31
-rw-r--r--spec/lib/gitlab_spec.rb19
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb2
-rw-r--r--spec/lib/mattermost/client_spec.rb4
-rw-r--r--spec/lib/mattermost/command_spec.rb6
-rw-r--r--spec/lib/mattermost/session_spec.rb8
-rw-r--r--spec/lib/mattermost/team_spec.rb8
-rw-r--r--spec/lib/peek/views/active_record_spec.rb175
-rw-r--r--spec/lib/peek/views/memory_spec.rb53
-rw-r--r--spec/lib/prometheus/pid_provider_spec.rb60
-rw-r--r--spec/lib/security/ci_configuration/sast_build_action_spec.rb51
-rw-r--r--spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb8
-rw-r--r--spec/lib/serializers/json_spec.rb2
-rw-r--r--spec/lib/sidebars/menu_spec.rb46
-rw-r--r--spec/lib/sidebars/projects/menus/confluence_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb2
-rw-r--r--spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb1
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/menus/project_information_menu_spec.rb14
-rw-r--r--spec/lib/sidebars/projects/menus/scope_menu_spec.rb23
-rw-r--r--spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb36
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/panel_spec.rb2
-rw-r--r--spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb7
-rw-r--r--spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb7
-rw-r--r--spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb31
-rw-r--r--spec/lib/system_check/orphans/namespace_check_spec.rb4
-rw-r--r--spec/lib/system_check/orphans/repository_check_spec.rb4
-rw-r--r--spec/lib/system_check/simple_executor_spec.rb5
-rw-r--r--spec/lib/system_check_spec.rb5
-rw-r--r--spec/mailers/emails/in_product_marketing_spec.rb36
-rw-r--r--spec/mailers/emails/profile_spec.rb6
-rw-r--r--spec/mailers/emails/service_desk_spec.rb12
-rw-r--r--spec/mailers/notify_spec.rb32
-rw-r--r--spec/migrations/20190924152703_migrate_issue_trackers_data_spec.rb4
-rw-r--r--spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb2
-rw-r--r--spec/migrations/20191204114127_delete_legacy_triggers_spec.rb2
-rw-r--r--spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb2
-rw-r--r--spec/migrations/20200122123016_backfill_project_settings_spec.rb2
-rw-r--r--spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb2
-rw-r--r--spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb2
-rw-r--r--spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb14
-rw-r--r--spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb2
-rw-r--r--spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb2
-rw-r--r--spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb2
-rw-r--r--spec/migrations/20200526115436_dedup_mr_metrics_spec.rb2
-rw-r--r--spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb2
-rw-r--r--spec/migrations/20200703125016_backfill_namespace_settings_spec.rb2
-rw-r--r--spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb2
-rw-r--r--spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb2
-rw-r--r--spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb2
-rw-r--r--spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb2
-rw-r--r--spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb2
-rw-r--r--spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb2
-rw-r--r--spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb2
-rw-r--r--spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb2
-rw-r--r--spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb2
-rw-r--r--spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb2
-rw-r--r--spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb2
-rw-r--r--spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb2
-rw-r--r--spec/migrations/20210112143418_remove_duplicate_services2_spec.rb2
-rw-r--r--spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb2
-rw-r--r--spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb2
-rw-r--r--spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb2
-rw-r--r--spec/migrations/20210226141517_dedup_issue_metrics_spec.rb2
-rw-r--r--spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb2
-rw-r--r--spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb2
-rw-r--r--spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb48
-rw-r--r--spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb2
-rw-r--r--spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb10
-rw-r--r--spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb2
-rw-r--r--spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb2
-rw-r--r--spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb2
-rw-r--r--spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb37
-rw-r--r--spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb30
-rw-r--r--spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb29
-rw-r--r--spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb24
-rw-r--r--spec/migrations/add_default_and_free_plans_spec.rb2
-rw-r--r--spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb2
-rw-r--r--spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb2
-rw-r--r--spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb2
-rw-r--r--spec/migrations/add_new_post_eoa_plans_spec.rb2
-rw-r--r--spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb2
-rw-r--r--spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb2
-rw-r--r--spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb2
-rw-r--r--spec/migrations/add_unique_constraint_to_approvals_user_id_and_merge_request_id_spec.rb2
-rw-r--r--spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb2
-rw-r--r--spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb80
-rw-r--r--spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb100
-rw-r--r--spec/migrations/backfill_imported_snippet_repositories_spec.rb2
-rw-r--r--spec/migrations/backfill_operations_feature_flags_active_spec.rb2
-rw-r--r--spec/migrations/backfill_operations_feature_flags_iid_spec.rb2
-rw-r--r--spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb2
-rw-r--r--spec/migrations/backfill_snippet_repositories_spec.rb2
-rw-r--r--spec/migrations/backfill_status_page_published_incidents_spec.rb2
-rw-r--r--spec/migrations/backport_enterprise_schema_spec.rb2
-rw-r--r--spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb2
-rw-r--r--spec/migrations/change_outbound_local_requests_whitelist_default_spec.rb2
-rw-r--r--spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb2
-rw-r--r--spec/migrations/change_web_hook_events_default_spec.rb2
-rw-r--r--spec/migrations/clean_grafana_url_spec.rb2
-rw-r--r--spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb2
-rw-r--r--spec/migrations/clean_up_pending_builds_table_spec.rb47
-rw-r--r--spec/migrations/cleanup_empty_commit_user_mentions_spec.rb2
-rw-r--r--spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb4
-rw-r--r--spec/migrations/cleanup_legacy_artifact_migration_spec.rb2
-rw-r--r--spec/migrations/cleanup_move_container_registry_enabled_to_project_features_spec.rb45
-rw-r--r--spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb2
-rw-r--r--spec/migrations/cleanup_optimistic_locking_nulls_spec.rb2
-rw-r--r--spec/migrations/cleanup_projects_with_missing_namespace_spec.rb4
-rw-r--r--spec/migrations/complete_namespace_settings_migration_spec.rb2
-rw-r--r--spec/migrations/confirm_project_bot_users_spec.rb2
-rw-r--r--spec/migrations/create_environment_for_self_monitoring_project_spec.rb2
-rw-r--r--spec/migrations/deduplicate_epic_iids_spec.rb2
-rw-r--r--spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb2
-rw-r--r--spec/migrations/delete_template_project_services_spec.rb2
-rw-r--r--spec/migrations/delete_template_services_duplicated_by_type_spec.rb2
-rw-r--r--spec/migrations/delete_user_callout_alerts_moved_spec.rb2
-rw-r--r--spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb46
-rw-r--r--spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb2
-rw-r--r--spec/migrations/drop_alerts_service_data_spec.rb2
-rw-r--r--spec/migrations/drop_background_migration_jobs_spec.rb2
-rw-r--r--spec/migrations/drop_project_ci_cd_settings_merge_trains_enabled_spec.rb2
-rw-r--r--spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb2
-rw-r--r--spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb2
-rw-r--r--spec/migrations/enqueue_reset_merge_status_second_run_spec.rb2
-rw-r--r--spec/migrations/enqueue_reset_merge_status_spec.rb2
-rw-r--r--spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb2
-rw-r--r--spec/migrations/ensure_filled_file_store_on_package_files_spec.rb2
-rw-r--r--spec/migrations/ensure_namespace_settings_creation_spec.rb2
-rw-r--r--spec/migrations/ensure_target_project_id_is_filled_spec.rb2
-rw-r--r--spec/migrations/ensure_u2f_registrations_migrated_spec.rb2
-rw-r--r--spec/migrations/fill_file_store_ci_job_artifacts_spec.rb2
-rw-r--r--spec/migrations/fill_file_store_lfs_objects_spec.rb2
-rw-r--r--spec/migrations/fill_productivity_analytics_start_date_spec.rb2
-rw-r--r--spec/migrations/fill_store_uploads_spec.rb2
-rw-r--r--spec/migrations/fix_max_pages_size_spec.rb2
-rw-r--r--spec/migrations/fix_null_type_labels_spec.rb2
-rw-r--r--spec/migrations/fix_pool_repository_source_project_id_spec.rb2
-rw-r--r--spec/migrations/fix_projects_without_project_feature_spec.rb2
-rw-r--r--spec/migrations/fix_projects_without_prometheus_services_spec.rb2
-rw-r--r--spec/migrations/fix_wrong_pages_access_level_spec.rb2
-rw-r--r--spec/migrations/generate_ci_jwt_signing_key_spec.rb2
-rw-r--r--spec/migrations/generate_lets_encrypt_private_key_spec.rb2
-rw-r--r--spec/migrations/generate_missing_routes_for_bots_spec.rb2
-rw-r--r--spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb73
-rw-r--r--spec/migrations/insert_daily_invites_plan_limits_spec.rb2
-rw-r--r--spec/migrations/insert_project_feature_flags_plan_limits_spec.rb6
-rw-r--r--spec/migrations/insert_project_hooks_plan_limits_spec.rb2
-rw-r--r--spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb2
-rw-r--r--spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb2
-rw-r--r--spec/migrations/migrate_bot_type_to_user_type_spec.rb2
-rw-r--r--spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb2
-rw-r--r--spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb2
-rw-r--r--spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb2
-rw-r--r--spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb2
-rw-r--r--spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb2
-rw-r--r--spec/migrations/migrate_elastic_index_settings_spec.rb2
-rw-r--r--spec/migrations/migrate_incident_issues_to_incident_type_spec.rb2
-rw-r--r--spec/migrations/migrate_k8s_service_integration_spec.rb2
-rw-r--r--spec/migrations/migrate_legacy_managed_clusters_to_unmanaged_spec.rb2
-rw-r--r--spec/migrations/migrate_managed_clusters_with_no_token_to_unmanaged_spec.rb2
-rw-r--r--spec/migrations/migrate_merge_request_mentions_to_db_spec.rb2
-rw-r--r--spec/migrations/migrate_ops_feature_flags_scopes_target_user_ids_spec.rb2
-rw-r--r--spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb33
-rw-r--r--spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb2
-rw-r--r--spec/migrations/move_limits_from_plans_spec.rb2
-rw-r--r--spec/migrations/nullify_users_role_spec.rb2
-rw-r--r--spec/migrations/populate_project_statistics_packages_size_spec.rb2
-rw-r--r--spec/migrations/populate_rule_type_on_approval_merge_request_rules_spec.rb2
-rw-r--r--spec/migrations/remove_additional_application_settings_rows_spec.rb2
-rw-r--r--spec/migrations/remove_alerts_service_records_again_spec.rb2
-rw-r--r--spec/migrations/remove_alerts_service_records_spec.rb2
-rw-r--r--spec/migrations/remove_deprecated_jenkins_service_records_spec.rb5
-rw-r--r--spec/migrations/remove_duplicate_labels_from_groups_spec.rb2
-rw-r--r--spec/migrations/remove_duplicate_labels_from_project_spec.rb2
-rw-r--r--spec/migrations/remove_empty_github_service_templates_spec.rb2
-rw-r--r--spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb2
-rw-r--r--spec/migrations/remove_hipchat_service_records_spec.rb2
-rw-r--r--spec/migrations/remove_orphan_service_hooks_spec.rb2
-rw-r--r--spec/migrations/remove_orphaned_invited_members_spec.rb2
-rw-r--r--spec/migrations/remove_packages_deprecated_dependencies_spec.rb2
-rw-r--r--spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb2
-rw-r--r--spec/migrations/remove_security_dashboard_feature_flag_spec.rb2
-rw-r--r--spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb2
-rw-r--r--spec/migrations/rename_sitemap_namespace_spec.rb2
-rw-r--r--spec/migrations/rename_sitemap_root_namespaces_spec.rb2
-rw-r--r--spec/migrations/reschedule_artifact_expiry_backfill_spec.rb2
-rw-r--r--spec/migrations/reseed_merge_trains_enabled_spec.rb2
-rw-r--r--spec/migrations/reseed_repository_storages_weighted_spec.rb2
-rw-r--r--spec/migrations/retry_backfill_traversal_ids_spec.rb93
-rw-r--r--spec/migrations/save_instance_administrators_group_id_spec.rb2
-rw-r--r--spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb2
-rw-r--r--spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb2
-rw-r--r--spec/migrations/schedule_calculate_wiki_sizes_spec.rb60
-rw-r--r--spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb47
-rw-r--r--spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb2
-rw-r--r--spec/migrations/schedule_link_lfs_objects_projects_spec.rb2
-rw-r--r--spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb2
-rw-r--r--spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb2
-rw-r--r--spec/migrations/schedule_migrate_security_scans_spec.rb2
-rw-r--r--spec/migrations/schedule_migrate_u2f_webauthn_spec.rb2
-rw-r--r--spec/migrations/schedule_pages_metadata_migration_spec.rb2
-rw-r--r--spec/migrations/schedule_populate_issue_email_participants_spec.rb2
-rw-r--r--spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb2
-rw-r--r--spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb2
-rw-r--r--spec/migrations/schedule_populate_project_snippet_statistics_spec.rb2
-rw-r--r--spec/migrations/schedule_populate_user_highest_roles_table_spec.rb2
-rw-r--r--spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb2
-rw-r--r--spec/migrations/schedule_recalculate_project_authorizations_spec.rb2
-rw-r--r--spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb2
-rw-r--r--spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb127
-rw-r--r--spec/migrations/schedule_sync_issuables_state_id_spec.rb2
-rw-r--r--spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb2
-rw-r--r--spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb2
-rw-r--r--spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb2
-rw-r--r--spec/migrations/schedule_update_timelogs_project_id_spec.rb2
-rw-r--r--spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb2
-rw-r--r--spec/migrations/seed_merge_trains_enabled_spec.rb2
-rw-r--r--spec/migrations/seed_repository_storages_weighted_spec.rb2
-rw-r--r--spec/migrations/services_remove_temporary_index_on_project_id_spec.rb2
-rw-r--r--spec/migrations/set_issue_id_for_all_versions_spec.rb2
-rw-r--r--spec/migrations/set_job_waiter_ttl_spec.rb2
-rw-r--r--spec/migrations/sync_issuables_state_id_spec.rb2
-rw-r--r--spec/migrations/truncate_user_fullname_spec.rb2
-rw-r--r--spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb2
-rw-r--r--spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb2
-rw-r--r--spec/migrations/update_fingerprint_sha256_within_keys_spec.rb2
-rw-r--r--spec/migrations/update_historical_data_recorded_at_spec.rb2
-rw-r--r--spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb2
-rw-r--r--spec/migrations/update_minimum_password_length_spec.rb2
-rw-r--r--spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb2
-rw-r--r--spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb2
-rw-r--r--spec/models/application_record_spec.rb42
-rw-r--r--spec/models/application_setting_spec.rb28
-rw-r--r--spec/models/bulk_imports/export_status_spec.rb77
-rw-r--r--spec/models/bulk_imports/export_upload_spec.rb2
-rw-r--r--spec/models/bulk_imports/file_transfer/group_config_spec.rb18
-rw-r--r--spec/models/bulk_imports/file_transfer/project_config_spec.rb18
-rw-r--r--spec/models/ci/build_dependencies_spec.rb9
-rw-r--r--spec/models/ci/build_spec.rb383
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb116
-rw-r--r--spec/models/ci/build_trace_chunks/database_spec.rb6
-rw-r--r--spec/models/ci/build_trace_chunks/redis_spec.rb6
-rw-r--r--spec/models/ci/job_artifact_spec.rb26
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb68
-rw-r--r--spec/models/ci/job_token/scope_spec.rb65
-rw-r--r--spec/models/ci/pending_build_spec.rb33
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb77
-rw-r--r--spec/models/ci/pipeline_spec.rb126
-rw-r--r--spec/models/ci/runner_spec.rb177
-rw-r--r--spec/models/ci/running_build_spec.rb55
-rw-r--r--spec/models/clusters/applications/fluentd_spec.rb84
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb90
-rw-r--r--spec/models/clusters/cluster_spec.rb96
-rw-r--r--spec/models/clusters/clusters_hierarchy_spec.rb8
-rw-r--r--spec/models/commit_spec.rb101
-rw-r--r--spec/models/commit_status_spec.rb26
-rw-r--r--spec/models/concerns/awardable_spec.rb4
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb31
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb32
-rw-r--r--spec/models/concerns/has_timelogs_report_spec.rb55
-rw-r--r--spec/models/concerns/has_user_type_spec.rb2
-rw-r--r--spec/models/concerns/integrations/has_data_fields_spec.rb (renamed from spec/models/project_services/data_fields_spec.rb)4
-rw-r--r--spec/models/concerns/issuable_spec.rb12
-rw-r--r--spec/models/concerns/limitable_spec.rb25
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb94
-rw-r--r--spec/models/container_repository_spec.rb55
-rw-r--r--spec/models/cycle_analytics/project_level_stage_adapter_spec.rb2
-rw-r--r--spec/models/deploy_token_spec.rb76
-rw-r--r--spec/models/deployment_metrics_spec.rb6
-rw-r--r--spec/models/diff_discussion_spec.rb6
-rw-r--r--spec/models/diff_note_spec.rb8
-rw-r--r--spec/models/email_spec.rb2
-rw-r--r--spec/models/environment_spec.rb67
-rw-r--r--spec/models/environment_status_spec.rb11
-rw-r--r--spec/models/experiment_spec.rb66
-rw-r--r--spec/models/experiment_subject_spec.rb30
-rw-r--r--spec/models/group_spec.rb298
-rw-r--r--spec/models/hooks/project_hook_spec.rb11
-rw-r--r--spec/models/hooks/service_hook_spec.rb10
-rw-r--r--spec/models/hooks/system_hook_spec.rb10
-rw-r--r--spec/models/hooks/web_hook_log_archived_spec.rb52
-rw-r--r--spec/models/hooks/web_hook_spec.rb49
-rw-r--r--spec/models/import_export_upload_spec.rb99
-rw-r--r--spec/models/integration_spec.rb31
-rw-r--r--spec/models/integrations/assembla_spec.rb6
-rw-r--r--spec/models/integrations/bamboo_spec.rb42
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb (renamed from spec/models/project_services/chat_notification_service_spec.rb)28
-rw-r--r--spec/models/integrations/base_issue_tracker_spec.rb (renamed from spec/models/project_services/issue_tracker_service_spec.rb)6
-rw-r--r--spec/models/integrations/bugzilla_spec.rb (renamed from spec/models/project_services/bugzilla_service_spec.rb)2
-rw-r--r--spec/models/integrations/buildkite_spec.rb (renamed from spec/models/project_services/buildkite_service_spec.rb)2
-rw-r--r--spec/models/integrations/campfire_spec.rb8
-rw-r--r--spec/models/integrations/chat_message/wiki_page_message_spec.rb46
-rw-r--r--spec/models/integrations/confluence_spec.rb6
-rw-r--r--spec/models/integrations/custom_issue_tracker_spec.rb (renamed from spec/models/project_services/custom_issue_tracker_service_spec.rb)2
-rw-r--r--spec/models/integrations/discord_spec.rb (renamed from spec/models/project_services/discord_service_spec.rb)4
-rw-r--r--spec/models/integrations/drone_ci_spec.rb (renamed from spec/models/project_services/drone_ci_service_spec.rb)16
-rw-r--r--spec/models/integrations/ewm_spec.rb (renamed from spec/models/project_services/ewm_service_spec.rb)2
-rw-r--r--spec/models/integrations/external_wiki_spec.rb (renamed from spec/models/project_services/external_wiki_service_spec.rb)2
-rw-r--r--spec/models/integrations/flowdock_spec.rb (renamed from spec/models/project_services/flowdock_service_spec.rb)2
-rw-r--r--spec/models/integrations/hangouts_chat_spec.rb (renamed from spec/models/project_services/hangouts_chat_service_spec.rb)4
-rw-r--r--spec/models/integrations/irker_spec.rb (renamed from spec/models/project_services/irker_service_spec.rb)2
-rw-r--r--spec/models/integrations/issue_tracker_data_spec.rb (renamed from spec/models/project_services/issue_tracker_data_spec.rb)2
-rw-r--r--spec/models/integrations/jenkins_spec.rb (renamed from spec/models/project_services/jenkins_service_spec.rb)2
-rw-r--r--spec/models/integrations/jira_spec.rb (renamed from spec/models/project_services/jira_service_spec.rb)169
-rw-r--r--spec/models/integrations/jira_tracker_data_spec.rb (renamed from spec/models/project_services/jira_tracker_data_spec.rb)2
-rw-r--r--spec/models/integrations/mattermost_slash_commands_spec.rb (renamed from spec/models/project_services/mattermost_slash_commands_service_spec.rb)8
-rw-r--r--spec/models/integrations/mattermost_spec.rb7
-rw-r--r--spec/models/integrations/microsoft_teams_spec.rb (renamed from spec/models/project_services/microsoft_teams_service_spec.rb)4
-rw-r--r--spec/models/integrations/open_project_spec.rb (renamed from spec/models/project_services/open_project_service_spec.rb)2
-rw-r--r--spec/models/integrations/open_project_tracker_data_spec.rb (renamed from spec/models/project_services/open_project_tracker_data_spec.rb)2
-rw-r--r--spec/models/integrations/packagist_spec.rb (renamed from spec/models/project_services/packagist_service_spec.rb)2
-rw-r--r--spec/models/integrations/pipelines_email_spec.rb (renamed from spec/models/project_services/pipelines_email_service_spec.rb)2
-rw-r--r--spec/models/integrations/pivotaltracker_spec.rb (renamed from spec/models/project_services/pivotaltracker_service_spec.rb)4
-rw-r--r--spec/models/integrations/pushover_spec.rb (renamed from spec/models/project_services/pushover_service_spec.rb)2
-rw-r--r--spec/models/integrations/redmine_spec.rb (renamed from spec/models/project_services/redmine_service_spec.rb)2
-rw-r--r--spec/models/integrations/slack_slash_commands_spec.rb (renamed from spec/models/project_services/slack_slash_commands_service_spec.rb)4
-rw-r--r--spec/models/integrations/slack_spec.rb (renamed from spec/models/project_services/slack_service_spec.rb)4
-rw-r--r--spec/models/integrations/teamcity_spec.rb (renamed from spec/models/project_services/teamcity_service_spec.rb)2
-rw-r--r--spec/models/integrations/unify_circuit_spec.rb (renamed from spec/models/project_services/unify_circuit_service_spec.rb)4
-rw-r--r--spec/models/integrations/webex_teams_spec.rb (renamed from spec/models/project_services/webex_teams_service_spec.rb)4
-rw-r--r--spec/models/integrations/youtrack_spec.rb (renamed from spec/models/project_services/youtrack_service_spec.rb)2
-rw-r--r--spec/models/issue_spec.rb54
-rw-r--r--spec/models/key_spec.rb12
-rw-r--r--spec/models/label_link_spec.rb25
-rw-r--r--spec/models/lfs_object_spec.rb30
-rw-r--r--spec/models/lfs_objects_project_spec.rb2
-rw-r--r--spec/models/member_spec.rb45
-rw-r--r--spec/models/members/group_member_spec.rb16
-rw-r--r--spec/models/members/last_group_owner_assigner_spec.rb2
-rw-r--r--spec/models/members/project_member_spec.rb10
-rw-r--r--spec/models/merge_request_diff_spec.rb55
-rw-r--r--spec/models/merge_request_spec.rb103
-rw-r--r--spec/models/milestone_spec.rb43
-rw-r--r--spec/models/namespace_spec.rb96
-rw-r--r--spec/models/note_spec.rb12
-rw-r--r--spec/models/onboarding_progress_spec.rb22
-rw-r--r--spec/models/operations/feature_flag_scope_spec.rb4
-rw-r--r--spec/models/operations/feature_flag_spec.rb8
-rw-r--r--spec/models/packages/debian/file_entry_spec.rb12
-rw-r--r--spec/models/packages/debian/group_distribution_key_spec.rb7
-rw-r--r--spec/models/packages/debian/project_distribution_key_spec.rb7
-rw-r--r--spec/models/packages/package_file_spec.rb16
-rw-r--r--spec/models/packages/package_spec.rb82
-rw-r--r--spec/models/pages/lookup_path_spec.rb9
-rw-r--r--spec/models/pages_domain_spec.rb32
-rw-r--r--spec/models/plan_limits_spec.rb1
-rw-r--r--spec/models/postgresql/replication_slot_spec.rb4
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb6
-rw-r--r--spec/models/project_feature_usage_spec.rb50
-rw-r--r--spec/models/project_repository_storage_move_spec.rb35
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb36
-rw-r--r--spec/models/project_services/mattermost_service_spec.rb7
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb27
-rw-r--r--spec/models/project_spec.rb390
-rw-r--r--spec/models/release_highlight_spec.rb6
-rw-r--r--spec/models/repository_spec.rb64
-rw-r--r--spec/models/service_desk_setting_spec.rb31
-rw-r--r--spec/models/snippet_repository_storage_move_spec.rb13
-rw-r--r--spec/models/snippet_spec.rb2
-rw-r--r--spec/models/timelog_spec.rb56
-rw-r--r--spec/models/todo_spec.rb12
-rw-r--r--spec/models/user_detail_spec.rb5
-rw-r--r--spec/models/user_spec.rb69
-rw-r--r--spec/policies/global_policy_spec.rb30
-rw-r--r--spec/policies/group_policy_spec.rb20
-rw-r--r--spec/policies/issue_policy_spec.rb123
-rw-r--r--spec/policies/merge_request_policy_spec.rb5
-rw-r--r--spec/policies/project_policy_spec.rb244
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb4
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb2
-rw-r--r--spec/presenters/packages/detail/package_presenter_spec.rb3
-rw-r--r--spec/presenters/packages/pypi/package_presenter_spec.rb55
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/presenters/release_presenter_spec.rb40
-rw-r--r--spec/presenters/service_hook_presenter_spec.rb8
-rw-r--r--spec/presenters/web_hook_log_presenter_spec.rb12
-rw-r--r--spec/rack_servers/unicorn_spec.rb105
-rw-r--r--spec/rake_helper.rb6
-rw-r--r--spec/requests/api/branches_spec.rb43
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb70
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb47
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb37
-rw-r--r--spec/requests/api/ci/runners_spec.rb10
-rw-r--r--spec/requests/api/commit_statuses_spec.rb66
-rw-r--r--spec/requests/api/commits_spec.rb19
-rw-r--r--spec/requests/api/composer_packages_spec.rb2
-rw-r--r--spec/requests/api/conan_instance_packages_spec.rb2
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb14
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb28
-rw-r--r--spec/requests/api/feature_flag_scopes_spec.rb319
-rw-r--r--spec/requests/api/feature_flags_spec.rb216
-rw-r--r--spec/requests/api/files_spec.rb3
-rw-r--r--spec/requests/api/generic_packages_spec.rb3
-rw-r--r--spec/requests/api/go_proxy_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb1
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb17
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb7
-rw-r--r--spec/requests/api/graphql/group/timelogs_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb32
-rw-r--r--spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb122
-rw-r--r--spec/requests/api/graphql/mutations/labels/create_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb6
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_many_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/composer_spec.rb51
-rw-r--r--spec/requests/api/graphql/packages/conan_spec.rb50
-rw-r--r--spec/requests/api/graphql/packages/maven_spec.rb92
-rw-r--r--spec/requests/api/graphql/packages/nuget_spec.rb61
-rw-r--r--spec/requests/api/graphql/packages/pypi_spec.rb27
-rw-r--r--spec/requests/api/graphql/project/base_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb14
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb106
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb25
-rw-r--r--spec/requests/api/group_avatar_spec.rb64
-rw-r--r--spec/requests/api/group_container_repositories_spec.rb1
-rw-r--r--spec/requests/api/group_export_spec.rb19
-rw-r--r--spec/requests/api/group_labels_spec.rb4
-rw-r--r--spec/requests/api/groups_spec.rb41
-rw-r--r--spec/requests/api/helm_packages_spec.rb63
-rw-r--r--spec/requests/api/internal/base_spec.rb53
-rw-r--r--spec/requests/api/invitations_spec.rb50
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb11
-rw-r--r--spec/requests/api/labels_spec.rb14
-rw-r--r--spec/requests/api/maven_packages_spec.rb426
-rw-r--r--spec/requests/api/members_spec.rb68
-rw-r--r--spec/requests/api/merge_requests_spec.rb45
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb5
-rw-r--r--spec/requests/api/nuget_group_packages_spec.rb2
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb8
-rw-r--r--spec/requests/api/project_attributes.yml4
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb6
-rw-r--r--spec/requests/api/project_debian_distributions_spec.rb66
-rw-r--r--spec/requests/api/project_export_spec.rb13
-rw-r--r--spec/requests/api/project_packages_spec.rb32
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb2
-rw-r--r--spec/requests/api/project_statistics_spec.rb10
-rw-r--r--spec/requests/api/projects_spec.rb214
-rw-r--r--spec/requests/api/pypi_packages_spec.rb242
-rw-r--r--spec/requests/api/releases_spec.rb2
-rw-r--r--spec/requests/api/repositories_spec.rb10
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb14
-rw-r--r--spec/requests/api/services_spec.rb2
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/api/tags_spec.rb129
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb13
-rw-r--r--spec/requests/api/terraform/state_spec.rb22
-rw-r--r--spec/requests/api/unleash_spec.rb162
-rw-r--r--spec/requests/api/users_preferences_spec.rb12
-rw-r--r--spec/requests/api/users_spec.rb23
-rw-r--r--spec/requests/api/wikis_spec.rb45
-rw-r--r--spec/requests/git_http_spec.rb4
-rw-r--r--spec/requests/groups/email_campaigns_controller_spec.rb13
-rw-r--r--spec/requests/lfs_http_spec.rb4
-rw-r--r--spec/requests/oauth/tokens_controller_spec.rb71
-rw-r--r--spec/requests/openid_connect_spec.rb110
-rw-r--r--spec/requests/users_controller_spec.rb42
-rw-r--r--spec/routing/project_routing_spec.rb34
-rw-r--r--spec/routing/routing_spec.rb28
-rw-r--r--spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb108
-rw-r--r--spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb64
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb4
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb12
-rw-r--r--spec/serializers/cluster_entity_spec.rb4
-rw-r--r--spec/serializers/fork_namespace_entity_spec.rb32
-rw-r--r--spec/serializers/issue_board_entity_spec.rb2
-rw-r--r--spec/serializers/member_serializer_spec.rb2
-rw-r--r--spec/serializers/merge_request_diff_entity_spec.rb68
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb19
-rw-r--r--spec/services/admin/propagate_service_template_spec.rb2
-rw-r--r--spec/services/authorized_project_update/periodic_recalculate_service_spec.rb2
-rw-r--r--spec/services/authorized_project_update/project_recalculate_service_spec.rb76
-rw-r--r--spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb21
-rw-r--r--spec/services/bulk_imports/file_decompression_service_spec.rb99
-rw-r--r--spec/services/bulk_imports/file_download_service_spec.rb133
-rw-r--r--spec/services/bulk_imports/relation_export_service_spec.rb2
-rw-r--r--spec/services/bulk_update_integration_service_spec.rb8
-rw-r--r--spec/services/ci/append_build_trace_service_spec.rb2
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb47
-rw-r--r--spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb13
-rw-r--r--spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/needs_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb3
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb47
-rw-r--r--spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb20
-rw-r--r--spec/services/ci/pipeline_processing/shared_processing_service.rb2
-rw-r--r--spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb1
-rw-r--r--spec/services/ci/register_job_service_spec.rb147
-rw-r--r--spec/services/ci/retry_build_service_spec.rb10
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb368
-rw-r--r--spec/services/ci/update_build_state_service_spec.rb42
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb7
-rw-r--r--spec/services/clusters/cleanup/app_service_spec.rb118
-rw-r--r--spec/services/clusters/destroy_service_spec.rb6
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb6
-rw-r--r--spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb126
-rw-r--r--spec/services/commits/cherry_pick_service_spec.rb13
-rw-r--r--spec/services/container_expiration_policies/cleanup_service_spec.rb286
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb36
-rw-r--r--spec/services/design_management/copy_design_collection/copy_service_spec.rb8
-rw-r--r--spec/services/discussions/resolve_service_spec.rb45
-rw-r--r--spec/services/feature_flags/disable_service_spec.rb92
-rw-r--r--spec/services/feature_flags/enable_service_spec.rb154
-rw-r--r--spec/services/feature_flags/update_service_spec.rb145
-rw-r--r--spec/services/groups/create_service_spec.rb2
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/services/groups/group_links/create_service_spec.rb46
-rw-r--r--spec/services/groups/participants_service_spec.rb37
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb77
-rw-r--r--spec/services/issue_rebalancing_service_spec.rb21
-rw-r--r--spec/services/issues/close_service_spec.rb85
-rw-r--r--spec/services/issues/create_service_spec.rb24
-rw-r--r--spec/services/issues/update_service_spec.rb90
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb10
-rw-r--r--spec/services/jira_import/users_importer_spec.rb72
-rw-r--r--spec/services/lfs/push_service_spec.rb17
-rw-r--r--spec/services/members/create_service_spec.rb45
-rw-r--r--spec/services/members/invite_service_spec.rb16
-rw-r--r--spec/services/merge_requests/build_service_spec.rb70
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/handle_assignees_change_service_spec.rb22
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb4
-rw-r--r--spec/services/merge_requests/update_assignees_service_spec.rb47
-rw-r--r--spec/services/merge_requests/update_service_spec.rb8
-rw-r--r--spec/services/namespace_settings/update_service_spec.rb32
-rw-r--r--spec/services/namespaces/in_product_marketing_emails_service_spec.rb29
-rw-r--r--spec/services/notes/create_service_spec.rb4
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb16
-rw-r--r--spec/services/notification_recipients/builder/default_spec.rb16
-rw-r--r--spec/services/packages/debian/create_distribution_service_spec.rb9
-rw-r--r--spec/services/packages/debian/destroy_distribution_service_spec.rb78
-rw-r--r--spec/services/packages/debian/extract_changes_metadata_service_spec.rb30
-rw-r--r--spec/services/packages/debian/generate_distribution_service_spec.rb175
-rw-r--r--spec/services/packages/debian/parse_debian822_service_spec.rb2
-rw-r--r--spec/services/packages/debian/process_changes_service_spec.rb3
-rw-r--r--spec/services/packages/debian/update_distribution_service_spec.rb2
-rw-r--r--spec/services/packages/helm/extract_file_metadata_service_spec.rb59
-rw-r--r--spec/services/packages/nuget/metadata_extraction_service_spec.rb7
-rw-r--r--spec/services/packages/nuget/update_package_from_metadata_service_spec.rb28
-rw-r--r--spec/services/pages/delete_service_spec.rb37
-rw-r--r--spec/services/pod_logs/elasticsearch_service_spec.rb18
-rw-r--r--spec/services/projects/create_service_spec.rb72
-rw-r--r--spec/services/projects/destroy_service_spec.rb17
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb24
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb58
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb39
-rw-r--r--spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb25
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb10
-rw-r--r--spec/services/security/ci_configuration/sast_parser_service_spec.rb30
-rw-r--r--spec/services/snippets/create_service_spec.rb2
-rw-r--r--spec/services/snippets/update_service_spec.rb2
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb2
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb35
-rw-r--r--spec/services/users/activity_service_spec.rb47
-rw-r--r--spec/services/users/authorized_build_service_spec.rb16
-rw-r--r--spec/services/users/build_service_spec.rb139
-rw-r--r--spec/services/users/update_assigned_open_issue_count_service_spec.rb49
-rw-r--r--spec/services/web_hook_service_spec.rb96
-rw-r--r--spec/spec_helper.rb9
-rw-r--r--spec/support/capybara.rb3
-rw-r--r--spec/support/database_cleaner.rb2
-rw-r--r--spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb31
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_for_sast.yml1
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml15
-rw-r--r--spec/support/helpers/access_matchers_helpers.rb16
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb5
-rw-r--r--spec/support/helpers/feature_flag_helpers.rb3
-rw-r--r--spec/support/helpers/features/top_nav_spec_helpers.rb53
-rw-r--r--spec/support/helpers/gitaly_setup.rb4
-rw-r--r--spec/support/helpers/global_id_deprecation_helpers.rb13
-rw-r--r--spec/support/helpers/graphql_helpers.rb4
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb10
-rw-r--r--spec/support/helpers/login_helpers.rb6
-rw-r--r--spec/support/helpers/query_recorder.rb7
-rw-r--r--spec/support/helpers/rake_helpers.rb5
-rw-r--r--spec/support/helpers/reference_parser_helpers.rb18
-rw-r--r--spec/support/helpers/usage_data_helpers.rb2
-rw-r--r--spec/support/import_export/common_util.rb8
-rw-r--r--spec/support/matchers/be_one_of.rb11
-rw-r--r--spec/support/redis.rb8
-rw-r--r--spec/support/redis/redis_helpers.rb5
-rw-r--r--spec/support/redis/redis_shared_examples.rb149
-rw-r--r--spec/support/shared_contexts/changes_access_checks_shared_context.rb40
-rw-r--r--spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb28
-rw-r--r--spec/support/shared_contexts/features/integrations/instance_and_group_integrations_shared_context.rb18
-rw-r--r--spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb24
-rw-r--r--spec/support/shared_contexts/features/integrations/integrations_shared_context.rb (renamed from spec/support/shared_contexts/services_shared_context.rb)10
-rw-r--r--spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb (renamed from spec/support/shared_contexts/project_service_jira_context.rb)0
-rw-r--r--spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb (renamed from spec/support/shared_contexts/project_service_shared_context.rb)6
-rw-r--r--spec/support/shared_contexts/graphql/requests/packages_shared_context.rb33
-rw-r--r--spec/support/shared_contexts/load_balancing_configuration_shared_context.rb19
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb20
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/read_ci_configuration_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/requests/api/helm_packages_shared_context.rb10
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/services/packages/debian/distribution_shared_context.rb20
-rw-r--r--spec/support/shared_contexts/single_change_access_checks_shared_context.rb (renamed from spec/support/shared_contexts/change_access_checks_shared_context.rb)2
-rw-r--r--spec/support/shared_examples/ci/badge_template_shared_examples.rb57
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb4
-rw-r--r--spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/finders/assignees_filter_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb53
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb74
-rw-r--r--spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb (renamed from spec/support/shared_examples/models/chat_service_shared_examples.rb)84
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb (renamed from spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb)33
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb (renamed from spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb)2
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb18
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_key_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/namespaces/linear_traversal_examples.rb23
-rw-r--r--spec/support/shared_examples/namespaces/traversal_examples.rb38
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb222
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb53
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb173
-rw-r--r--spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/tracking_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb166
-rw-r--r--spec/support/shared_examples/services/users/build_service_shared_examples.rb125
-rw-r--r--spec/support/shared_examples/uncached_response_shared_examples.rb12
-rw-r--r--spec/support/unicorn.rb27
-rw-r--r--spec/tasks/admin_mode_spec.rb2
-rw-r--r--spec/tasks/cache/clear/redis_spec.rb39
-rw-r--r--spec/tasks/config_lint_spec.rb4
-rw-r--r--spec/tasks/gettext_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/artifacts/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/artifacts/migrate_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb78
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/cleanup_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/container_registry_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb18
-rw-r--r--spec/tasks/gitlab/external_diffs_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb2
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/info_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/ldap_rake_spec.rb6
-rw-r--r--spec/tasks/gitlab/lfs/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/lfs/migrate_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/packages/composer_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/packages/events_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/packages/migrate_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/pages_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/password_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/praefect_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/seed/group_seed_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/sidekiq_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/snippets_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb31
-rw-r--r--spec/tasks/gitlab/terraform/migrate_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/update_templates_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/uploads/check_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/usage_data_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/user_management_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/web_hook_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/x509/update_rake_spec.rb2
-rw-r--r--spec/tasks/migrate/schema_check_rake_spec.rb2
-rw-r--r--spec/tasks/tokens_spec.rb2
-rw-r--r--spec/tooling/danger/changelog_spec.rb299
-rw-r--r--spec/tooling/danger/product_intelligence_spec.rb150
-rw-r--r--spec/tooling/danger/project_helper_spec.rb36
-rw-r--r--spec/tooling/graphql/docs/renderer_spec.rb (renamed from spec/lib/gitlab/graphql/docs/renderer_spec.rb)12
-rw-r--r--spec/views/admin/users/_user.html.haml_spec.rb51
-rw-r--r--spec/views/devise/shared/_signup_box.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/header/_new_dropdown.haml_spec.rb12
-rw-r--r--spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb12
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb45
-rw-r--r--spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb12
-rw-r--r--spec/views/projects/empty.html.haml_spec.rb47
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb17
-rw-r--r--spec/views/projects/services/edit.html.haml_spec.rb6
-rw-r--r--spec/views/projects/settings/operations/show.html.haml_spec.rb40
-rw-r--r--spec/views/shared/nav/_sidebar.html.haml_spec.rb2
-rw-r--r--spec/workers/analytics/instance_statistics/count_job_trigger_worker_spec.rb17
-rw-r--r--spec/workers/analytics/instance_statistics/counter_job_worker_spec.rb70
-rw-r--r--spec/workers/authorized_project_update/project_recalculate_worker_spec.rb68
-rw-r--r--spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb11
-rw-r--r--spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb58
-rw-r--r--spec/workers/build_hooks_worker_spec.rb15
-rw-r--r--spec/workers/build_queue_worker_spec.rb31
-rw-r--r--spec/workers/bulk_import_worker_spec.rb22
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb2
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb122
-rw-r--r--spec/workers/ci/initial_pipeline_process_worker_spec.rb4
-rw-r--r--spec/workers/clusters/applications/activate_service_worker_spec.rb6
-rw-r--r--spec/workers/clusters/applications/deactivate_service_worker_spec.rb12
-rw-r--r--spec/workers/clusters/cleanup/app_worker_spec.rb41
-rw-r--r--spec/workers/concerns/application_worker_spec.rb34
-rw-r--r--spec/workers/concerns/worker_attributes_spec.rb68
-rw-r--r--spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb560
-rw-r--r--spec/workers/container_expiration_policy_worker_spec.rb104
-rw-r--r--spec/workers/deployments/execute_hooks_worker_spec.rb51
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb8
-rw-r--r--spec/workers/expire_pipeline_cache_worker_spec.rb11
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb26
-rw-r--r--spec/workers/incident_management/process_alert_worker_spec.rb88
-rw-r--r--spec/workers/incident_management/process_prometheus_alert_worker_spec.rb28
-rw-r--r--spec/workers/issue_placement_worker_spec.rb4
-rw-r--r--spec/workers/issue_rebalancing_worker_spec.rb85
-rw-r--r--spec/workers/merge_requests/assignees_change_worker_spec.rb59
-rw-r--r--spec/workers/packages/debian/generate_distribution_worker_spec.rb62
-rw-r--r--spec/workers/pipeline_hooks_worker_spec.rb5
-rw-r--r--spec/workers/pipeline_process_worker_spec.rb45
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--spec/workers/process_commit_worker_spec.rb2
-rw-r--r--spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb12
-rw-r--r--spec/workers/project_service_worker_spec.rb2
-rw-r--r--spec/workers/project_update_repository_storage_worker_spec.rb15
-rw-r--r--spec/workers/projects/post_creation_worker_spec.rb4
-rw-r--r--spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb2
-rw-r--r--spec/workers/propagate_integration_inherit_worker_spec.rb2
-rw-r--r--spec/workers/propagate_integration_worker_spec.rb8
-rw-r--r--spec/workers/propagate_service_template_worker_spec.rb2
-rw-r--r--spec/workers/prune_web_hook_logs_worker_spec.rb26
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb2
-rw-r--r--spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb8
-rw-r--r--spec/workers/snippet_schedule_bulk_repository_shard_moves_worker_spec.rb12
-rw-r--r--spec/workers/snippet_update_repository_storage_worker_spec.rb15
-rw-r--r--spec/workers/ssh_keys/expired_notification_worker_spec.rb36
-rw-r--r--spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb10
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb132
-rw-r--r--spec/workers/users/update_open_issue_count_worker_spec.rb65
-rw-r--r--spec/workers/web_hook_worker_spec.rb7
1683 files changed, 40155 insertions, 18630 deletions
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
deleted file mode 100644
index 8a86f574bf5..00000000000
--- a/spec/bin/changelog_spec.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-load File.expand_path('../../bin/changelog', __dir__)
-
-RSpec.describe 'bin/changelog' do
- let(:options) { OpenStruct.new(title: 'Test title', type: 'fixed', dry_run: true) }
-
- describe ChangelogEntry do
- it 'truncates the file path' do
- entry = described_class.new(options)
-
- allow(entry).to receive(:ee?).and_return(false)
- allow(entry).to receive(:branch_name).and_return('long-branch-' * 100)
-
- file_path = entry.send(:file_path)
- expect(file_path.length).to eq(99)
- end
- end
-
- describe ChangelogOptionParser do
- describe '.parse' do
- it 'parses --amend' do
- options = described_class.parse(%w[foo bar --amend])
-
- expect(options.amend).to eq true
- end
-
- it 'parses --force and -f' do
- %w[--force -f].each do |flag|
- options = described_class.parse(%W[foo #{flag} bar])
-
- expect(options.force).to eq true
- end
- end
-
- it 'parses --merge-request and -m' do
- %w[--merge-request -m].each do |flag|
- options = described_class.parse(%W[foo #{flag} 1234 bar])
-
- expect(options.merge_request).to eq 1234
- end
- end
-
- it 'parses --dry-run and -n' do
- %w[--dry-run -n].each do |flag|
- options = described_class.parse(%W[foo #{flag} bar])
-
- expect(options.dry_run).to eq true
- end
- end
-
- it 'parses --git-username and -u' do
- allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
-
- %w[--git-username -u].each do |flag|
- options = described_class.parse(%W[foo #{flag} bar])
-
- expect(options.author).to eq 'Jane Doe'
- end
- end
-
- it 'parses --type and -t' do
- %w[--type -t].each do |flag|
- options = described_class.parse(%W[foo #{flag} security])
-
- expect(options.type).to eq 'security'
- end
- end
-
- it 'parses --ee and -e' do
- %w[--ee -e].each do |flag|
- options = described_class.parse(%W[foo #{flag} security])
-
- expect(options.ee).to eq true
- end
- end
-
- it 'parses -h' do
- expect do
- expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
- end.to raise_error(ChangelogHelpers::Done)
- end
-
- it 'assigns title' do
- options = described_class.parse(%W[foo -m 1 bar\n baz\r\n --amend])
-
- expect(options.title).to eq 'foo bar baz'
- end
- end
-
- describe '.read_type' do
- let(:type) { '1' }
-
- it 'reads type from $stdin' do
- expect($stdin).to receive(:getc).and_return(type)
- expect do
- expect(described_class.read_type).to eq('added')
- end.to output.to_stdout
- end
-
- context 'invalid type given' do
- let(:type) { '99' }
-
- it 'shows error message and exits the program' do
- allow($stdin).to receive(:getc).and_return(type)
-
- expect do
- expect { described_class.read_type }.to raise_error(
- ChangelogHelpers::Abort,
- 'Invalid category index, please select an index between 1 and 8'
- )
- end.to output.to_stdout
- end
- end
- end
- end
-end
diff --git a/spec/bin/sidekiq_cluster_spec.rb b/spec/bin/sidekiq_cluster_spec.rb
index 503cc0999c5..1bba048a27c 100644
--- a/spec/bin/sidekiq_cluster_spec.rb
+++ b/spec/bin/sidekiq_cluster_spec.rb
@@ -10,8 +10,6 @@ RSpec.describe 'bin/sidekiq-cluster' do
where(:args, :included, :excluded) do
%w[--negate cronjob] | '-qdefault,1' | '-qcronjob,1'
%w[--queue-selector resource_boundary=cpu] | '-qupdate_merge_requests,1' | '-qdefault,1'
- # Remove with https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/646
- %w[--experimental-queue-selector resource_boundary=cpu] | '-qupdate_merge_requests,1' | '-qdefault,1'
end
with_them do
@@ -31,9 +29,7 @@ RSpec.describe 'bin/sidekiq-cluster' do
context 'when selecting all queues' do
[
%w[*],
- %w[--queue-selector *],
- # Remove with https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/646
- %w[--experimental-queue-selector *]
+ %w[--queue-selector *]
].each do |args|
it "runs successfully with `#{args}`", :aggregate_failures do
cmd = %w[bin/sidekiq-cluster --dryrun] + args
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index ce514bd8905..074549ff591 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'mail_room.yml' do
context 'when both incoming email and service desk email are enabled' do
let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' }
- let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' }
+ let(:queues_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' }
let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) }
it 'contains the intended configuration' do
@@ -72,7 +72,7 @@ RSpec.describe 'mail_room.yml' do
context 'when both incoming email and service desk email are enabled for Microsoft Graph' do
let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled_ms_graph.yml' }
- let(:queues_config_path) { 'spec/fixtures/config/redis_queues_new_format_host.yml' }
+ let(:queues_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' }
let(:gitlab_redis_queues) { Gitlab::Redis::Queues.new(Rails.env) }
it 'contains the intended configuration' do
diff --git a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb b/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
index 9aba86cdaf2..b5f8d363d40 100644
--- a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
+++ b/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
@@ -25,9 +25,9 @@ RSpec.describe 'aggregated metrics' do
RSpec::Matchers.define :have_known_time_frame do
allowed_time_frames = [
- Gitlab::Utils::UsageData::ALL_TIME_TIME_FRAME_NAME,
- Gitlab::Utils::UsageData::TWENTY_EIGHT_DAYS_TIME_FRAME_NAME,
- Gitlab::Utils::UsageData::SEVEN_DAYS_TIME_FRAME_NAME
+ Gitlab::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME,
+ Gitlab::Usage::TimeFrame::TWENTY_EIGHT_DAYS_TIME_FRAME_NAME,
+ Gitlab::Usage::TimeFrame::SEVEN_DAYS_TIME_FRAME_NAME
]
match do |aggregate|
@@ -63,7 +63,7 @@ RSpec.describe 'aggregated metrics' do
let_it_be(:events_records) { known_events.select { |event| aggregate[:events].include?(event[:name]) } }
it "does not include 'all' time frame for Redis sourced aggregate" do
- expect(aggregate[:time_frame]).not_to include(Gitlab::Utils::UsageData::ALL_TIME_TIME_FRAME_NAME)
+ expect(aggregate[:time_frame]).not_to include(Gitlab::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME)
end
it "only refers to known events" do
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
index da7577c371d..bab0d033056 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe AbuseReportsController do
it 'redirects back to the reported user' do
post :create, params: { abuse_report: attrs }
- expect(response).to redirect_to user
+ expect(response).to redirect_to root_path
end
end
diff --git a/spec/controllers/admin/appearances_controller_spec.rb b/spec/controllers/admin/application_settings/appearances_controller_spec.rb
index ee6a4a4c7af..cc914f3c9b8 100644
--- a/spec/controllers/admin/appearances_controller_spec.rb
+++ b/spec/controllers/admin/application_settings/appearances_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::AppearancesController do
+RSpec.describe Admin::ApplicationSettings::AppearancesController do
let(:admin) { create(:admin) }
let(:header_message) { 'Header message' }
let(:footer_message) { 'Footer' }
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 8e31ef12adf..d9b7e00fd75 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Admin::GroupsController do
- let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
- let(:admin) { create(:admin) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
@@ -46,9 +46,9 @@ RSpec.describe Admin::GroupsController do
end
describe 'PUT #members_update' do
- let(:group_user) { create(:user) }
+ let_it_be(:group_user) { create(:user) }
- it 'adds user to members' do
+ it 'adds user to members', :aggregate_failures, :snowplow do
put :members_update, params: {
id: group,
user_ids: group_user.id,
@@ -58,9 +58,16 @@ RSpec.describe Admin::GroupsController do
expect(controller).to set_flash.to 'Users were successfully added.'
expect(response).to redirect_to(admin_group_path(group))
expect(group.users).to include group_user
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: 'admin-group-page',
+ property: 'existing_user',
+ user: admin
+ )
end
- it 'can add unlimited members' do
+ it 'can add unlimited members', :aggregate_failures do
put :members_update, params: {
id: group,
user_ids: 1.upto(1000).to_a.join(','),
@@ -71,7 +78,7 @@ RSpec.describe Admin::GroupsController do
expect(response).to redirect_to(admin_group_path(group))
end
- it 'adds no user to members' do
+ it 'adds no user to members', :aggregate_failures do
put :members_update, params: {
id: group,
user_ids: '',
diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb
index 971f2f121aa..79c39784173 100644
--- a/spec/controllers/admin/integrations_controller_spec.rb
+++ b/spec/controllers/admin/integrations_controller_spec.rb
@@ -93,8 +93,8 @@ RSpec.describe Admin::IntegrationsController do
end
it 'deletes the integration and all inheriting integrations' do
- expect { subject }.to change { JiraService.for_instance.count }.by(-1)
- .and change { JiraService.inherit_from_id(integration.id).count }.by(-1)
+ expect { subject }.to change { Integrations::Jira.for_instance.count }.by(-1)
+ .and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end
end
end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 3984784f045..b5e7af2c012 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -12,6 +12,10 @@ RSpec.describe Admin::RunnersController do
describe '#index' do
render_views
+ before do
+ stub_feature_flags(runner_list_view_vue_ui: false)
+ end
+
it 'lists all runners' do
get :index
@@ -50,6 +54,10 @@ RSpec.describe Admin::RunnersController do
describe '#show' do
render_views
+ before do
+ stub_feature_flags(runner_detailed_view_vue_ui: false)
+ end
+
let_it_be(:project) { create(:project) }
let_it_be(:project_two) { create(:project) }
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
index d5ec9907b48..995282ca4bb 100644
--- a/spec/controllers/admin/services_controller_spec.rb
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Admin::ServicesController do
describe "#update" do
let(:project) { create(:project) }
let!(:service_template) do
- RedmineService.create!(
+ Integrations::Redmine.create!(
project: nil,
active: false,
template: true,
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 722c9c322cc..da57e5f8a92 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -651,6 +651,95 @@ RSpec.describe Admin::UsersController do
expect { post :update, params: params }.to change { user.reload.note }.to(note)
end
end
+
+ context 'when updating credit card validation for user account' do
+ let(:params) do
+ {
+ id: user.to_param,
+ user: user_params
+ }
+ end
+
+ shared_examples 'no credit card validation param' do
+ let(:user_params) { { name: 'foo' } }
+
+ it 'does not change credit card validation' do
+ expect { post :update, params: params }.not_to change(Users::CreditCardValidation, :count)
+ end
+ end
+
+ context 'when user has a credit card validation' do
+ before do
+ user.create_credit_card_validation!(credit_card_validated_at: Time.zone.now)
+ end
+
+ context 'with unchecked credit card validation' do
+ let(:user_params) do
+ { credit_card_validation_attributes: { credit_card_validated_at: '0' } }
+ end
+
+ it 'deletes credit_card_validation' do
+ expect { post :update, params: params }.to change { Users::CreditCardValidation.count }.by(-1)
+ end
+ end
+
+ context 'with checked credit card validation' do
+ let(:user_params) do
+ { credit_card_validation_attributes: { credit_card_validated_at: '1' } }
+ end
+
+ it 'does not change credit_card_validated_at' do
+ expect { post :update, params: params }.not_to change { user.credit_card_validated_at }
+ end
+ end
+
+ it_behaves_like 'no credit card validation param'
+ end
+
+ context 'when user does not have a credit card validation' do
+ context 'with checked credit card validation' do
+ let(:user_params) do
+ { credit_card_validation_attributes: { credit_card_validated_at: '1' } }
+ end
+
+ it 'creates new credit card validation' do
+ expect { post :update, params: params }.to change { Users::CreditCardValidation.count }.by 1
+ end
+ end
+
+ context 'with unchecked credit card validation' do
+ let(:user_params) do
+ { credit_card_validation_attributes: { credit_card_validated_at: '0' } }
+ end
+
+ it 'does not blow up' do
+ expect { post :update, params: params }.not_to change(Users::CreditCardValidation, :count)
+ end
+ end
+
+ it_behaves_like 'no credit card validation param'
+ end
+
+ context 'invalid parameters' do
+ let(:user_params) do
+ { credit_card_validation_attributes: { credit_card_validated_at: Time.current.iso8601 } }
+ end
+
+ it_behaves_like 'no credit card validation param'
+ end
+
+ context 'with non permitted params' do
+ let(:user_params) do
+ { credit_card_validation_attributes: { _destroy: true } }
+ end
+
+ before do
+ user.create_credit_card_validation!(credit_card_validated_at: Time.zone.now)
+ end
+
+ it_behaves_like 'no credit card validation param'
+ end
+ end
end
describe "DELETE #remove_email" do
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 0235d7eb95a..218aa04dd3f 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -704,7 +704,7 @@ RSpec.describe ApplicationController do
get :index
- expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store'
+ expect(response.headers['Cache-Control']).to eq 'no-store'
expect(response.headers['Pragma']).to eq 'no-cache'
end
@@ -740,7 +740,7 @@ RSpec.describe ApplicationController do
it 'sets no-cache headers', :aggregate_failures do
subject
- expect(response.headers['Cache-Control']).to eq 'no-cache, no-store'
+ expect(response.headers['Cache-Control']).to eq 'no-store'
expect(response.headers['Pragma']).to eq 'no-cache'
expect(response.headers['Expires']).to eq 'Fri, 01 Jan 1990 00:00:00 GMT'
end
diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb
index 49a39f257fe..c9a0ae981fc 100644
--- a/spec/controllers/confirmations_controller_spec.rb
+++ b/spec/controllers/confirmations_controller_spec.rb
@@ -12,7 +12,9 @@ RSpec.describe ConfirmationsController do
describe '#show' do
render_views
- subject { get :show, params: { confirmation_token: confirmation_token } }
+ def perform_request
+ get :show, params: { confirmation_token: confirmation_token }
+ end
context 'user is already confirmed' do
let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
@@ -20,20 +22,37 @@ RSpec.describe ConfirmationsController do
before do
user.confirm
- subject
end
it 'renders `new`' do
+ perform_request
+
expect(response).to render_template(:new)
end
it 'displays an error message' do
+ perform_request
+
expect(response.body).to include('Email was already confirmed, please try signing in')
end
it 'does not display the email of the user' do
+ perform_request
+
expect(response.body).not_to include(user.email)
end
+
+ it 'sets the username and caller_id in the context' do
+ expect(controller).to receive(:show).and_wrap_original do |m, *args|
+ m.call(*args)
+
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.user' => user.username,
+ 'meta.caller_id' => 'ConfirmationsController#show')
+ end
+
+ perform_request
+ end
end
context 'user accesses the link after the expiry of confirmation token has passed' do
@@ -42,39 +61,64 @@ RSpec.describe ConfirmationsController do
before do
allow(Devise).to receive(:confirm_within).and_return(1.day)
-
- travel_to(3.days.from_now) do
- subject
- end
end
it 'renders `new`' do
+ travel_to(3.days.from_now) { perform_request }
+
expect(response).to render_template(:new)
end
it 'displays an error message' do
+ travel_to(3.days.from_now) { perform_request }
+
expect(response.body).to include('Email needs to be confirmed within 1 day, please request a new one below')
end
it 'does not display the email of the user' do
+ travel_to(3.days.from_now) { perform_request }
+
expect(response.body).not_to include(user.email)
end
+
+ it 'sets the username and caller_id in the context' do
+ expect(controller).to receive(:show).and_wrap_original do |m, *args|
+ m.call(*args)
+
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.user' => user.username,
+ 'meta.caller_id' => 'ConfirmationsController#show')
+ end
+
+ travel_to(3.days.from_now) { perform_request }
+ end
end
context 'with an invalid confirmation token' do
let(:confirmation_token) { 'invalid_confirmation_token' }
- before do
- subject
- end
-
it 'renders `new`' do
+ perform_request
+
expect(response).to render_template(:new)
end
it 'displays an error message' do
+ perform_request
+
expect(response.body).to include('Confirmation token is invalid')
end
+
+ it 'sets the the caller_id in the context' do
+ expect(controller).to receive(:show).and_wrap_original do |m, *args|
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.caller_id' => 'ConfirmationsController#show')
+
+ m.call(*args)
+ end
+
+ perform_request
+ end
end
end
end
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index dcec8012f03..ed8dc1eb7cb 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -156,6 +156,12 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures do
let!(:design_event) { create(:design_event, project: project) }
let!(:wiki_page_event) { create(:wiki_page_event, project: project) }
let!(:issue_event) { create(:closed_issue_event, project: project) }
+ let!(:push_event) do
+ create(:push_event, project: project).tap do |event|
+ create(:push_event_payload, event: event, ref_count: 2, ref: nil, ref_type: :tag, commit_count: 0, action: :pushed)
+ end
+ end
+
let(:design) { design_event.design }
let(:wiki_page) { wiki_page_event.wiki_page }
let(:issue) { issue_event.issue }
@@ -168,9 +174,10 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures do
it 'renders all kinds of event without error' do
get :index, format: :atom
- expect(assigns(:events)).to include(design_event, wiki_page_event, issue_event)
+ expect(assigns(:events)).to include(design_event, wiki_page_event, issue_event, push_event)
expect(response).to render_template('dashboard/projects/index')
expect(response.body).to include(
+ "pushed to project",
"uploaded design #{design.to_reference}",
"created wiki page #{wiki_page.title}",
"joined project #{project.full_name}",
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index b666f73110a..4b17326de09 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Groups::GroupMembersController do
end
describe 'GET index' do
- it 'renders index with 200 status code' do
+ it 'renders index with 200 status code', :aggregate_failures do
get :index, params: { group_id: group }
expect(response).to have_gitlab_http_status(:ok)
@@ -32,14 +32,6 @@ RSpec.describe Groups::GroupMembersController do
sign_in(user)
end
- it 'assigns max_access_for_group' do
- allow(controller).to receive(:current_user).and_return(user)
-
- get :index, params: { group_id: group }
-
- expect(user.max_access_for_group[group.id]).to eq(Gitlab::Access::OWNER)
- end
-
it 'assigns invited members' do
get :index, params: { group_id: group }
@@ -126,7 +118,7 @@ RSpec.describe Groups::GroupMembersController do
group.add_developer(user)
end
- it 'returns 403' do
+ it 'returns 403', :aggregate_failures do
post :create, params: {
group_id: group,
user_ids: group_user.id,
@@ -143,7 +135,7 @@ RSpec.describe Groups::GroupMembersController do
group.add_owner(user)
end
- it 'adds user to members' do
+ it 'adds user to members', :aggregate_failures, :snowplow do
post :create, params: {
group_id: group,
user_ids: group_user.id,
@@ -153,9 +145,16 @@ RSpec.describe Groups::GroupMembersController do
expect(controller).to set_flash.to 'Users were successfully added.'
expect(response).to redirect_to(group_group_members_path(group))
expect(group.users).to include group_user
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: 'group-members-page',
+ property: 'existing_user',
+ user: user
+ )
end
- it 'adds no user to members' do
+ it 'adds no user to members', :aggregate_failures do
post :create, params: {
group_id: group,
user_ids: '',
@@ -185,7 +184,7 @@ RSpec.describe Groups::GroupMembersController do
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago }
- it 'does not add user to members' do
+ it 'does not add user to members', :aggregate_failures do
subject
expect(flash[:alert]).to include('Expires at cannot be a date in the past')
@@ -197,7 +196,7 @@ RSpec.describe Groups::GroupMembersController do
context 'when set to a date in the future' do
let(:expires_at) { 5.days.from_now }
- it 'adds user to members' do
+ it 'adds user to members', :aggregate_failures do
subject
expect(controller).to set_flash.to 'Users were successfully added.'
@@ -334,7 +333,7 @@ RSpec.describe Groups::GroupMembersController do
group.add_developer(user)
end
- it 'returns 403' do
+ it 'returns 403', :aggregate_failures do
delete :destroy, params: { group_id: group, id: member }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -347,7 +346,7 @@ RSpec.describe Groups::GroupMembersController do
group.add_owner(user)
end
- it '[HTML] removes user from members' do
+ it '[HTML] removes user from members', :aggregate_failures do
delete :destroy, params: { group_id: group, id: member }
expect(controller).to set_flash.to 'User was successfully removed from group.'
@@ -356,7 +355,7 @@ RSpec.describe Groups::GroupMembersController do
expect(sub_group.members).to include sub_member
end
- it '[HTML] removes user from members including subgroups and projects' do
+ it '[HTML] removes user from members including subgroups and projects', :aggregate_failures do
delete :destroy, params: { group_id: group, id: member, remove_sub_memberships: true }
expect(controller).to set_flash.to 'User was successfully removed from group and any subgroups and projects.'
@@ -365,7 +364,7 @@ RSpec.describe Groups::GroupMembersController do
expect(sub_group.members).not_to include sub_member
end
- it '[JS] removes user from members' do
+ it '[JS] removes user from members', :aggregate_failures do
delete :destroy, params: { group_id: group, id: member }, xhr: true
expect(response).to be_successful
@@ -394,7 +393,7 @@ RSpec.describe Groups::GroupMembersController do
group.add_developer(user)
end
- it 'removes user from members' do
+ it 'removes user from members', :aggregate_failures do
delete :leave, params: { group_id: group }
expect(controller).to set_flash.to "You left the \"#{group.name}\" group."
@@ -402,7 +401,7 @@ RSpec.describe Groups::GroupMembersController do
expect(group.users).not_to include user
end
- it 'supports json request' do
+ it 'supports json request', :aggregate_failures do
delete :leave, params: { group_id: group }, format: :json
expect(response).to have_gitlab_http_status(:ok)
@@ -429,7 +428,7 @@ RSpec.describe Groups::GroupMembersController do
group.request_access(user)
end
- it 'removes user from members' do
+ it 'removes user from members', :aggregate_failures do
delete :leave, params: { group_id: group }
expect(controller).to set_flash.to 'Your access request to the group has been withdrawn.'
@@ -446,7 +445,7 @@ RSpec.describe Groups::GroupMembersController do
sign_in(user)
end
- it 'creates a new GroupMember that is not a team member' do
+ it 'creates a new GroupMember that is not a team member', :aggregate_failures do
post :request_access, params: { group_id: group }
expect(controller).to set_flash.to 'Your request for access has been queued for review.'
@@ -477,7 +476,7 @@ RSpec.describe Groups::GroupMembersController do
group.add_developer(user)
end
- it 'returns 403' do
+ it 'returns 403', :aggregate_failures do
post :approve_access_request, params: { group_id: group, id: member }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -490,7 +489,7 @@ RSpec.describe Groups::GroupMembersController do
group.add_owner(user)
end
- it 'adds user to members' do
+ it 'adds user to members', :aggregate_failures do
post :approve_access_request, params: { group_id: group, id: member }
expect(response).to redirect_to(group_group_members_path(group))
diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb
index 35c9a80266e..f4541eda293 100644
--- a/spec/controllers/groups/registry/repositories_controller_spec.rb
+++ b/spec/controllers/groups/registry/repositories_controller_spec.rb
@@ -75,6 +75,8 @@ RSpec.describe Groups::Registry::RepositoriesController do
context 'json format' do
let(:format) { :json }
+ let(:namespace) { group }
+ let(:snowplow_gitlab_standard_context) { { user: user, namespace: group } }
it 'has the correct response schema' do
subject
diff --git a/spec/controllers/groups/settings/integrations_controller_spec.rb b/spec/controllers/groups/settings/integrations_controller_spec.rb
index 63d99a1fab1..4f1f6dcaae4 100644
--- a/spec/controllers/groups/settings/integrations_controller_spec.rb
+++ b/spec/controllers/groups/settings/integrations_controller_spec.rb
@@ -124,8 +124,8 @@ RSpec.describe Groups::Settings::IntegrationsController do
end
it 'deletes the integration and all inheriting integrations' do
- expect { subject }.to change { JiraService.for_group(group.id).count }.by(-1)
- .and change { JiraService.inherit_from_id(integration.id).count }.by(-1)
+ expect { subject }.to change { Integrations::Jira.for_group(group.id).count }.by(-1)
+ .and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end
end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index f47eac7ac25..91b11cd46c5 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -651,6 +651,45 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
+ describe 'updating :prevent_sharing_groups_outside_hierarchy' do
+ subject do
+ put :update,
+ params: {
+ id: group.to_param,
+ group: { prevent_sharing_groups_outside_hierarchy: true }
+ }
+ end
+
+ context 'when user is a group owner' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'updates the attribute' do
+ expect { subject }
+ .to change { group.namespace_settings.reload.prevent_sharing_groups_outside_hierarchy }
+ .from(false)
+ .to(true)
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+
+ context 'when not a group owner' do
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'does not update the attribute' do
+ expect { subject }.not_to change { group.namespace_settings.reload.prevent_sharing_groups_outside_hierarchy }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe '#ensure_canonical_path' do
before do
sign_in(user)
@@ -1026,14 +1065,13 @@ RSpec.describe GroupsController, factory_default: :keep do
describe 'GET #download_export' do
let(:admin) { create(:admin) }
+ let(:export_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
before do
enable_admin_mode!(admin)
end
context 'when there is a file available to download' do
- let(:export_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
-
before do
sign_in(admin)
create(:import_export_upload, group: group, export_file: export_file)
@@ -1046,6 +1084,22 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
+ context 'when the file is no longer present on disk' do
+ before do
+ sign_in(admin)
+
+ create(:import_export_upload, group: group, export_file: export_file)
+ group.export_file.file.delete
+ end
+
+ it 'returns not found' do
+ get :download_export, params: { id: group.to_param }
+
+ expect(flash[:alert]).to include('file containing the export is not available yet')
+ expect(response).to redirect_to(edit_group_path(group))
+ end
+ end
+
context 'when there is no file available to download' do
before do
sign_in(admin)
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index b450318f6f7..8f74d210667 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe Import::BulkImportsController do
end
describe 'GET status' do
- let(:client) { BulkImports::Clients::Http.new(uri: 'http://gitlab.example', token: 'token') }
+ let(:client) { BulkImports::Clients::HTTP.new(uri: 'http://gitlab.example', token: 'token') }
describe 'serialized group data' do
let(:client_response) do
@@ -73,7 +73,7 @@ RSpec.describe Import::BulkImportsController do
let(:client_params) do
{
top_level_only: true,
- min_access_level: Gitlab::Access::MAINTAINER
+ min_access_level: Gitlab::Access::OWNER
}
end
@@ -149,7 +149,7 @@ RSpec.describe Import::BulkImportsController do
context 'when connection error occurs' do
before do
allow(controller).to receive(:client).and_return(client)
- allow(client).to receive(:get).and_raise(BulkImports::Clients::Http::ConnectionError)
+ allow(client).to receive(:get).and_raise(BulkImports::Clients::HTTP::ConnectionError)
end
it 'returns 422' do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 4a47a4a2a53..9a142559fca 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -293,6 +293,18 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
expect(request.env['warden']).to be_authenticated
end
+
+ it 'sets the username and caller_id in the context' do
+ expect(controller).to receive(:atlassian_oauth2).and_wrap_original do |m, *args|
+ m.call(*args)
+
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.user' => user.username,
+ 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2')
+ end
+
+ post :atlassian_oauth2
+ end
end
context 'for a new user' do
@@ -454,6 +466,18 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
it 'doesn\'t link a new identity to the user' do
expect { post :saml, params: { SAMLResponse: mock_saml_response } }.not_to change { user.identities.count }
end
+
+ it 'sets the username and caller_id in the context' do
+ expect(controller).to receive(:saml).and_wrap_original do |m, *args|
+ m.call(*args)
+
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.user' => user.username,
+ 'meta.caller_id' => 'OmniauthCallbacksController#saml')
+ end
+
+ post :saml, params: { SAMLResponse: mock_saml_response }
+ end
end
end
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index e9883107456..08d68d7cec8 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -77,6 +77,18 @@ RSpec.describe PasswordsController do
expect(user.password_expires_at).not_to be_nil
end
end
+
+ it 'sets the username and caller_id in the context' do
+ expect(controller).to receive(:update).and_wrap_original do |m, *args|
+ m.call(*args)
+
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.user' => user.username,
+ 'meta.caller_id' => 'PasswordsController#update')
+ end
+
+ subject
+ end
end
end
end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 7a72a13febe..37a633afab4 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -100,6 +100,16 @@ RSpec.describe ProfilesController, :request_store do
expect(user.reload.job_title).to eq(title)
expect(response).to have_gitlab_http_status(:found)
end
+
+ it 'allows updating user specified pronouns', :aggregate_failures do
+ pronouns = 'they/them'
+ sign_in(user)
+
+ put :update, params: { user: { pronouns: pronouns } }
+
+ expect(user.reload.pronouns).to eq(pronouns)
+ expect(response).to have_gitlab_http_status(:found)
+ end
end
describe 'GET audit_log' do
@@ -110,7 +120,8 @@ RSpec.describe ProfilesController, :request_store do
expect_snowplow_event(
category: 'ProfilesController',
- action: 'search_audit_event'
+ action: 'search_audit_event',
+ user: user
)
end
end
diff --git a/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb
new file mode 100644
index 00000000000..1832b84ab6e
--- /dev/null
+++ b/spec/controllers/projects/analytics/cycle_analytics/summary_controller_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:params) { { namespace_id: project.namespace.to_param, project_id: project.to_param, created_after: '2010-01-01', created_before: '2010-01-02' } }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET "show"' do
+ subject { get :show, params: params }
+
+ it 'succeeds' do
+ project.add_reporter(user)
+
+ subject
+
+ expect(response).to be_successful
+ expect(response).to match_response_schema('analytics/cycle_analytics/summary')
+ end
+
+ context 'when analytics_disabled features are disabled' do
+ it 'renders 404' do
+ project.add_reporter(user)
+ project.project_feature.update!(analytics_access_level: ProjectFeature::DISABLED)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is not part of the project' do
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index b965feee645..9493215247a 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -444,6 +444,40 @@ RSpec.describe Projects::BlobController do
end
end
+ describe 'POST preview' do
+ subject(:request) { post :preview, params: default_params }
+
+ let(:user) { create(:user) }
+ let(:filename) { 'preview.md' }
+ let(:default_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: "#{project.default_branch}/#{filename}",
+ content: "Bar\n"
+ }
+ end
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ project.repository.create_file(
+ project.creator,
+ filename,
+ "Foo\n",
+ message: 'Test',
+ branch_name: project.default_branch
+ )
+ end
+
+ it 'is successful' do
+ request
+
+ expect(response).to be_successful
+ end
+ end
+
describe 'POST create' do
let(:user) { create(:user) }
let(:default_params) do
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index a99db2664a7..a00e302a64f 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -655,21 +655,6 @@ RSpec.describe Projects::BranchesController do
["feature", "improve/awesome", "merge-test", "markdown", "feature_conflict", "'test'"]
)
end
-
- context 'branch_list_keyset_pagination is disabled' do
- before do
- stub_feature_flags(branch_list_keyset_pagination: false)
- end
-
- it 'sets active and stale branches' do
- expect(assigns[:active_branches].map(&:name)).not_to include(
- "feature", "improve/awesome", "merge-test", "markdown", "feature_conflict", "'test'"
- )
- expect(assigns[:stale_branches].map(&:name)).to eq(
- ["feature", "improve/awesome", "merge-test", "markdown", "feature_conflict", "'test'"]
- )
- end
- end
end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index a231b54419e..c650d145bef 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -380,18 +380,6 @@ RSpec.describe Projects::CommitController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
-
- context 'disable pick_into_project feature flag' do
- before do
- stub_feature_flags(pick_into_project: false)
- end
-
- it 'does not cherry pick a commit from fork to upstream' do
- send_request
-
- expect(project.commit('feature').message).not_to include(forked_project.commit.id)
- end
- end
end
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 80a6d3960cd..2412b970342 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -156,7 +156,7 @@ RSpec.describe Projects::CompareController do
it 'shows a flash message and redirects' do
show_request
- expect(flash[:alert]).to eq('Invalid branch name')
+ expect(flash[:alert]).to eq("Invalid branch name(s): master%' AND 2554=4423 AND '%'='")
expect(response).to have_gitlab_http_status(:found)
end
end
@@ -169,7 +169,20 @@ RSpec.describe Projects::CompareController do
it 'shows a flash message and redirects' do
show_request
- expect(flash[:alert]).to eq('Invalid branch name')
+ expect(flash[:alert]).to eq("Invalid branch name(s): master%' AND 2554=4423 AND '%'='")
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+
+ context 'when the both refs are invalid' do
+ let(:from_project_id) { nil }
+ let(:from_ref) { "master%' AND 2554=4423 AND '%'='" }
+ let(:to_ref) { "improve%' =,awesome" }
+
+ it 'shows a flash message and redirects' do
+ show_request
+
+ expect(flash[:alert]).to eq("Invalid branch name(s): improve%' =,awesome, master%' AND 2554=4423 AND '%'='")
expect(response).to have_gitlab_http_status(:found)
end
end
diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb
index cd7d1ea0e8a..f809dd31b3b 100644
--- a/spec/controllers/projects/feature_flags_controller_spec.rb
+++ b/spec/controllers/projects/feature_flags_controller_spec.rb
@@ -154,60 +154,6 @@ RSpec.describe Projects::FeatureFlagsController do
end
end
- context 'when feature flags have additional scopes' do
- let!(:feature_flag_active_scope) do
- create(:operations_feature_flag_scope,
- feature_flag: feature_flag_active,
- environment_scope: 'production',
- active: false)
- end
-
- let!(:feature_flag_inactive_scope) do
- create(:operations_feature_flag_scope,
- feature_flag: feature_flag_inactive,
- environment_scope: 'staging',
- active: false)
- end
-
- it 'returns a correct summary' do
- subject
-
- expect(json_response['count']['all']).to eq(2)
- expect(json_response['count']['enabled']).to eq(1)
- expect(json_response['count']['disabled']).to eq(1)
- end
-
- it 'recognizes feature flag 1 as active' do
- subject
-
- expect(json_response['feature_flags'].first['active']).to be_truthy
- end
-
- it 'recognizes feature flag 2 as inactive' do
- subject
-
- expect(json_response['feature_flags'].second['active']).to be_falsy
- end
-
- it 'has ordered scopes' do
- subject
-
- expect(json_response['feature_flags'][0]['scopes'][0]['id'])
- .to be < json_response['feature_flags'][0]['scopes'][1]['id']
- expect(json_response['feature_flags'][1]['scopes'][0]['id'])
- .to be < json_response['feature_flags'][1]['scopes'][1]['id']
- end
-
- it 'does not have N+1 problem' do
- recorded = ActiveRecord::QueryRecorder.new { subject }
-
- related_count = recorded.log
- .count { |query| query.include?('operations_feature_flag') }
-
- expect(related_count).to be_within(5).of(2)
- end
- end
-
context 'with version 1 and 2 feature flags' do
let!(:new_version_feature_flag) do
create(:operations_feature_flag, :new_version_flag, project: project, name: 'feature_flag_c')
@@ -235,7 +181,7 @@ RSpec.describe Projects::FeatureFlagsController do
subject { get(:show, params: params, format: :json) }
let!(:feature_flag) do
- create(:operations_feature_flag, project: project)
+ create(:operations_feature_flag, :legacy_flag, project: project)
end
let(:params) do
@@ -371,6 +317,42 @@ RSpec.describe Projects::FeatureFlagsController do
end
end
+ describe 'GET edit' do
+ subject { get(:edit, params: params) }
+
+ context 'with legacy flags' do
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project) }
+
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ iid: feature_flag.iid
+ }
+ end
+
+ it 'returns not found' do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with new version flags' do
+ let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ iid: feature_flag.iid
+ }
+ end
+
+ it 'returns successfully' do
+ is_expected.to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
describe 'POST create.json' do
subject { post(:create, params: params, format: :json) }
@@ -762,7 +744,7 @@ RSpec.describe Projects::FeatureFlagsController do
describe 'DELETE destroy.json' do
subject { delete(:destroy, params: params, format: :json) }
- let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project) }
let(:params) do
{
diff --git a/spec/controllers/projects/feature_flags_user_lists_controller_spec.rb b/spec/controllers/projects/feature_flags_user_lists_controller_spec.rb
index e0d1d3765b2..32817f048e6 100644
--- a/spec/controllers/projects/feature_flags_user_lists_controller_spec.rb
+++ b/spec/controllers/projects/feature_flags_user_lists_controller_spec.rb
@@ -16,6 +16,39 @@ RSpec.describe Projects::FeatureFlagsUserListsController do
{ namespace_id: project.namespace, project_id: project }.merge(extra_params)
end
+ describe 'GET #index' do
+ it 'redirects when the user is unauthenticated' do
+ get(:index, params: request_params)
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+
+ it 'returns not found if the user does not belong to the project' do
+ user = create(:user)
+ sign_in(user)
+
+ get(:index, params: request_params)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns not found for a reporter' do
+ sign_in(reporter)
+
+ get(:index, params: request_params)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'renders the new page for a developer' do
+ sign_in(developer)
+
+ get(:index, params: request_params)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
describe 'GET #new' do
it 'redirects when the user is unauthenticated' do
get(:new, params: request_params)
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 8ca3009e0c7..e53e53980b5 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -195,6 +195,25 @@ RSpec.describe Projects::ForksController do
expect(json_response['namespaces'].length).to eq(1)
expect(json_response['namespaces'][0]['id']).to eq(group.id)
end
+
+ context 'N+1 queries' do
+ before do
+ create(:fork_network, root_project: project)
+ end
+
+ it 'avoids N+1 queries' do
+ do_request = -> { get :new, format: format, params: { namespace_id: project.namespace, project_id: project } }
+
+ # warm up
+ do_request.call
+
+ control = ActiveRecord::QueryRecorder.new { do_request.call }
+
+ create(:group, :public).add_owner(user)
+
+ expect { do_request.call }.not_to exceed_query_limit(control)
+ end
+ end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 059e7884d55..7569a18baeb 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -310,7 +310,7 @@ RSpec.describe Projects::IssuesController do
context 'external issue tracker' do
let!(:service) do
- create(:custom_issue_tracker_service, project: project, new_issue_url: 'http://test.com')
+ create(:custom_issue_tracker_integration, project: project, new_issue_url: 'http://test.com')
end
before do
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 10bcee28f71..edec8c3e9c6 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Projects::MattermostsController do
describe 'GET #new' do
before do
- allow_next_instance_of(MattermostSlashCommandsService) do |instance|
+ allow_next_instance_of(Integrations::MattermostSlashCommands) do |instance|
allow(instance).to receive(:list_teams).and_return([])
end
end
@@ -43,7 +43,7 @@ RSpec.describe Projects::MattermostsController do
context 'no request can be made to mattermost' do
it 'shows the error' do
- allow_next_instance_of(MattermostSlashCommandsService) do |instance|
+ allow_next_instance_of(Integrations::MattermostSlashCommands) do |instance|
allow(instance).to receive(:configure).and_return([false, "error message"])
end
@@ -53,7 +53,7 @@ RSpec.describe Projects::MattermostsController do
context 'the request is succesull' do
before do
- allow_next_instance_of(Mattermost::Command) do |instance|
+ allow_next_instance_of(::Mattermost::Command) do |instance|
allow(instance).to receive(:create).and_return('token')
end
end
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index c2cc3d10ea0..e07b7e4586a 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -17,8 +17,31 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
end
describe 'GET show' do
+ context 'when the request is html' do
+ before do
+ allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_loading_conflict_ui_action)
+
+ get :show,
+ params: {
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project,
+ id: merge_request_with_conflicts.iid
+ },
+ format: 'html'
+ end
+
+ it 'does tracks the resolve call' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_loading_conflict_ui_action).with(user: user)
+ end
+ end
+
context 'when the conflicts cannot be resolved in the UI' do
before do
+ allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_loading_conflict_ui_action)
+
allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
.and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
@@ -38,6 +61,11 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
it 'returns JSON with a message' do
expect(json_response.keys).to contain_exactly('message', 'type')
end
+
+ it 'does not track the resolve call' do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to have_received(:track_loading_conflict_ui_action).with(user: user)
+ end
end
context 'with valid conflicts' do
@@ -145,20 +173,19 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
conflict_for_path(path)
end
- it 'returns a 200 status code' do
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'returns the file in JSON format' do
+ it 'returns a 200 and the file in JSON format' do
content = MergeRequests::Conflicts::ListService.new(merge_request_with_conflicts)
.file_for_path(path, path)
.content
- expect(json_response).to include('old_path' => path,
- 'new_path' => path,
- 'blob_icon' => 'doc-text',
- 'blob_path' => a_string_ending_with(path),
- 'content' => content)
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include('old_path' => path,
+ 'new_path' => path,
+ 'blob_icon' => 'doc-text',
+ 'blob_path' => a_string_ending_with(path),
+ 'content' => content)
+ end
end
end
end
@@ -166,6 +193,11 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
context 'POST resolve_conflicts' do
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
+ before do
+ allow(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_resolve_conflict_action)
+ end
+
def resolve_conflicts(files)
post :resolve_conflicts,
params: {
@@ -201,13 +233,16 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
resolve_conflicts(resolved_files)
end
- it 'creates a new commit on the branch' do
- expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
- expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
- end
+ it 'handles the success case' do
+ aggregate_failures do
+ # creates a new commit on the branch
+ expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
+ expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
- it 'returns an OK response' do
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_resolve_conflict_action).with(user: user)
+ end
end
end
@@ -232,16 +267,17 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
resolve_conflicts(resolved_files)
end
- it 'returns a 400 error' do
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it 'has a message with the name of the first missing section' do
- expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21')
- end
+ it 'handles the error case' do
+ aggregate_failures do
+ # has a message with the name of the first missing section
+ expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21')
+ # does not create a new commit
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
- it 'does not create a new commit' do
- expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_resolve_conflict_action).with(user: user)
+ end
end
end
@@ -262,16 +298,17 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
resolve_conflicts(resolved_files)
end
- it 'returns a 400 error' do
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ it 'handles the error case' do
+ aggregate_failures do
+ # has a message with the name of the missing file
+ expect(json_response['message']).to include('files/ruby/popen.rb')
+ # does not create a new commit
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
- it 'has a message with the name of the missing file' do
- expect(json_response['message']).to include('files/ruby/popen.rb')
- end
-
- it 'does not create a new commit' do
- expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_resolve_conflict_action).with(user: user)
+ end
end
end
@@ -300,16 +337,17 @@ RSpec.describe Projects::MergeRequests::ConflictsController do
resolve_conflicts(resolved_files)
end
- it 'returns a 400 error' do
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it 'has a message with the path of the problem file' do
- expect(json_response['message']).to include('files/ruby/popen.rb')
- end
+ it 'handles the error case' do
+ aggregate_failures do
+ # has a message with the path of the problem file
+ expect(json_response['message']).to include('files/ruby/popen.rb')
+ # does not create a new commit
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
- it 'does not create a new commit' do
- expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to have_received(:track_resolve_conflict_action).with(user: user)
+ end
end
end
end
diff --git a/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
new file mode 100644
index 00000000000..dc5a022eb7b
--- /dev/null
+++ b/spec/controllers/projects/packages/infrastructure_registry_controller_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Packages::InfrastructureRegistryController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+
+ let(:params) { { namespace_id: project.namespace, project_id: project } }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+ end
+
+ describe 'GET #index' do
+ subject { get :index, params: params, format: :html }
+
+ it_behaves_like 'returning response status', :ok
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(infrastructure_registry_page: false)
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+
+ describe 'GET #show' do
+ let_it_be(:terraform_module) { create(:terraform_module_package, project: project) }
+
+ subject { get :show, params: params.merge(id: terraform_module.id), format: :html }
+
+ it_behaves_like 'returning response status', :ok
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(infrastructure_registry_page: false)
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 0e6b5e84d85..a80c5fa82f6 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -274,30 +274,26 @@ RSpec.describe Projects::PipelinesController do
end
describe 'GET #index' do
- context 'pipeline_empty_state_templates experiment' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
- it 'tracks the view', :experiment do
- expect(experiment(:pipeline_empty_state_templates))
- .to track(:view, value: project.namespace_id)
- .with_context(actor: user)
- .on_next_instance
+ def action
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+ end
- get :index, params: { namespace_id: project.namespace, project_id: project }
- end
+ subject { project.namespace }
+
+ context 'pipeline_empty_state_templates experiment' do
+ it_behaves_like 'tracks assignment and records the subject', :pipeline_empty_state_templates, :namespace
end
context 'code_quality_walkthrough experiment' do
- it 'tracks the view', :experiment do
- expect(experiment(:code_quality_walkthrough))
- .to track(:view, property: project.root_ancestor.id.to_s)
- .with_context(namespace: project.root_ancestor)
- .on_next_instance
+ it_behaves_like 'tracks assignment and records the subject', :code_quality_walkthrough, :namespace
+ end
- get :index, params: { namespace_id: project.namespace, project_id: project }
- end
+ context 'ci_runner_templates experiment' do
+ it_behaves_like 'tracks assignment and records the subject', :ci_runner_templates, :namespace
end
end
@@ -878,19 +874,6 @@ RSpec.describe Projects::PipelinesController do
expect(::Ci::RetryPipelineWorker).to have_received(:perform_async).with(pipeline.id, user.id)
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(background_pipeline_retry_endpoint: false)
- end
-
- it 'retries the pipeline without returning any content' do
- post_retry
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(build.reload).to be_retried
- end
- end
-
context 'when builds are disabled' do
let(:feature) { ProjectFeature::DISABLED }
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index bb817fc94b2..9ed43a251a2 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -3,9 +3,9 @@
require('spec_helper')
RSpec.describe Projects::ProjectMembersController do
- let(:user) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project, reload: true) { create(:project, :public) }
before do
travel_to DateTime.new(2019, 4, 1)
@@ -24,8 +24,8 @@ RSpec.describe Projects::ProjectMembersController do
context 'project members' do
context 'when project belongs to group' do
- let(:user_in_group) { create(:user) }
- let(:project_in_group) { create(:project, :public, group: group) }
+ 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)
@@ -69,7 +69,7 @@ RSpec.describe Projects::ProjectMembersController do
end
context 'group links' do
- let!(:project_group_link) { create(:project_group_link, project: project, group: group) }
+ let_it_be(:project_group_link) { create(:project_group_link, project: project, group: group) }
it 'lists group links' do
get :index, params: { namespace_id: project.namespace, project_id: project }
@@ -90,7 +90,7 @@ RSpec.describe Projects::ProjectMembersController do
end
context 'invited members' do
- let!(:invited_member) { create(:project_member, :invited, project: project) }
+ let_it_be(:invited_member) { create(:project_member, :invited, project: project) }
before do
project.add_maintainer(user)
@@ -123,7 +123,7 @@ RSpec.describe Projects::ProjectMembersController do
end
context 'access requests' do
- let(:access_requester_user) { create(:user) }
+ let_it_be(:access_requester_user) { create(:user) }
before do
project.request_access(access_requester_user)
@@ -158,7 +158,7 @@ RSpec.describe Projects::ProjectMembersController do
end
describe 'POST create' do
- let(:project_user) { create(:user) }
+ let_it_be(:project_user) { create(:user) }
before do
sign_in(user)
@@ -169,7 +169,7 @@ RSpec.describe Projects::ProjectMembersController do
project.add_developer(user)
end
- it 'returns 404' do
+ it 'returns 404', :aggregate_failures do
post :create, params: {
namespace_id: project.namespace,
project_id: project,
@@ -187,11 +187,7 @@ RSpec.describe Projects::ProjectMembersController do
project.add_maintainer(user)
end
- it 'adds user to members' do
- expect_next_instance_of(Members::CreateService) do |instance|
- expect(instance).to receive(:execute).and_return(status: :success)
- end
-
+ it 'adds user to members', :aggregate_failures, :snowplow do
post :create, params: {
namespace_id: project.namespace,
project_id: project,
@@ -201,9 +197,17 @@ RSpec.describe Projects::ProjectMembersController do
expect(controller).to set_flash.to 'Users were successfully added.'
expect(response).to redirect_to(project_project_members_path(project))
+ expect(project.users).to include project_user
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: 'project-members-page',
+ property: 'existing_user',
+ user: user
+ )
end
- it 'adds no user to members' do
+ it 'adds no user to members', :aggregate_failures do
expect_next_instance_of(Members::CreateService) do |instance|
expect(instance).to receive(:execute).and_return(status: :failure, message: 'Message')
end
@@ -230,7 +234,7 @@ RSpec.describe Projects::ProjectMembersController do
unrelated_project.add_maintainer(project_bot)
end
- it 'returns error' do
+ it 'returns error', :aggregate_failures do
post :create, params: {
namespace_id: project.namespace,
project_id: project,
@@ -261,7 +265,7 @@ RSpec.describe Projects::ProjectMembersController do
context 'when set to a date in the past' do
let(:expires_at) { 2.days.ago }
- it 'does not add user to members' do
+ it 'does not add user to members', :aggregate_failures do
subject
expect(flash[:alert]).to include('Expires at cannot be a date in the past')
@@ -273,7 +277,7 @@ RSpec.describe Projects::ProjectMembersController do
context 'when set to a date in the future' do
let(:expires_at) { 5.days.from_now }
- it 'adds user to members' do
+ it 'adds user to members', :aggregate_failures do
subject
expect(controller).to set_flash.to 'Users were successfully added.'
@@ -285,7 +289,7 @@ RSpec.describe Projects::ProjectMembersController do
end
describe 'PUT update' do
- let(:requester) { create(:project_member, :access_request, project: project) }
+ let_it_be(:requester) { create(:project_member, :access_request, project: project) }
before do
project.add_maintainer(user)
@@ -393,7 +397,7 @@ RSpec.describe Projects::ProjectMembersController do
end
describe 'DELETE destroy' do
- let(:member) { create(:project_member, :developer, project: project) }
+ let_it_be(:member) { create(:project_member, :developer, project: project) }
before do
sign_in(user)
@@ -417,7 +421,7 @@ RSpec.describe Projects::ProjectMembersController do
project.add_developer(user)
end
- it 'returns 404' do
+ it 'returns 404', :aggregate_failures do
delete :destroy, params: {
namespace_id: project.namespace,
project_id: project,
@@ -434,7 +438,7 @@ RSpec.describe Projects::ProjectMembersController do
project.add_maintainer(user)
end
- it '[HTML] removes user from members' do
+ it '[HTML] removes user from members', :aggregate_failures do
delete :destroy, params: {
namespace_id: project.namespace,
project_id: project,
@@ -447,7 +451,7 @@ RSpec.describe Projects::ProjectMembersController do
expect(project.members).not_to include member
end
- it '[JS] removes user from members' do
+ it '[JS] removes user from members', :aggregate_failures do
delete :destroy, params: {
namespace_id: project.namespace,
project_id: project,
@@ -483,7 +487,7 @@ RSpec.describe Projects::ProjectMembersController do
project.add_developer(user)
end
- it 'removes user from members' do
+ it 'removes user from members', :aggregate_failures do
delete :leave, params: {
namespace_id: project.namespace,
project_id: project
@@ -517,7 +521,7 @@ RSpec.describe Projects::ProjectMembersController do
project.request_access(user)
end
- it 'removes user from members' do
+ it 'removes user from members', :aggregate_failures do
delete :leave, params: {
namespace_id: project.namespace,
project_id: project
@@ -537,7 +541,7 @@ RSpec.describe Projects::ProjectMembersController do
sign_in(user)
end
- it 'creates a new ProjectMember that is not a team member' do
+ 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
@@ -553,7 +557,7 @@ RSpec.describe Projects::ProjectMembersController do
end
describe 'POST approve' do
- let(:member) { create(:project_member, :access_request, project: project) }
+ let_it_be(:member) { create(:project_member, :access_request, project: project) }
before do
sign_in(user)
@@ -577,7 +581,7 @@ RSpec.describe Projects::ProjectMembersController do
project.add_developer(user)
end
- it 'returns 404' do
+ it 'returns 404', :aggregate_failures do
post :approve_access_request, params: {
namespace_id: project.namespace,
project_id: project,
@@ -594,7 +598,7 @@ RSpec.describe Projects::ProjectMembersController do
project.add_maintainer(user)
end
- it 'adds user to members' do
+ it 'adds user to members', :aggregate_failures do
post :approve_access_request, params: {
namespace_id: project.namespace,
project_id: project,
@@ -611,8 +615,8 @@ RSpec.describe Projects::ProjectMembersController do
end
describe 'POST apply_import' do
- let(:another_project) { create(:project, :private) }
- let(:member) { create(:user) }
+ let_it_be(:another_project) { create(:project, :private) }
+ let_it_be(:member) { create(:user) }
before do
project.add_maintainer(user)
@@ -637,7 +641,7 @@ RSpec.describe Projects::ProjectMembersController do
include_context 'import applied'
- it 'imports source project members' do
+ it 'imports source project members', :aggregate_failures do
expect(project.team_members).to include member
expect(controller).to set_flash.to 'Successfully imported'
expect(response).to redirect_to(
@@ -660,7 +664,7 @@ RSpec.describe Projects::ProjectMembersController do
end
describe 'POST create' do
- let(:stranger) { create(:user) }
+ let_it_be(:stranger) { create(:user) }
context 'when creating owner' do
before do
@@ -700,7 +704,7 @@ RSpec.describe Projects::ProjectMembersController do
end
describe 'POST resend_invite' do
- let(:member) { create(:project_member, project: project) }
+ let_it_be(:member) { create(:project_member, project: project) }
before do
project.add_maintainer(user)
diff --git a/spec/controllers/projects/service_hook_logs_controller_spec.rb b/spec/controllers/projects/service_hook_logs_controller_spec.rb
index 97fb31f0546..040e59fc822 100644
--- a/spec/controllers/projects/service_hook_logs_controller_spec.rb
+++ b/spec/controllers/projects/service_hook_logs_controller_spec.rb
@@ -5,13 +5,13 @@ require 'spec_helper'
RSpec.describe Projects::ServiceHookLogsController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:service) { create(:drone_ci_service, project: project) }
- let(:log) { create(:web_hook_log, web_hook: service.service_hook) }
+ let(:integration) { create(:drone_ci_integration, project: project) }
+ let(:log) { create(:web_hook_log, web_hook: integration.service_hook) }
let(:log_params) do
{
namespace_id: project.namespace,
project_id: project,
- service_id: service.to_param,
+ service_id: integration.to_param,
id: log.id
}
end
@@ -35,7 +35,7 @@ RSpec.describe Projects::ServiceHookLogsController do
it 'executes the hook and redirects to the service form' do
expect_any_instance_of(ServiceHook).to receive(:execute)
expect_any_instance_of(described_class).to receive(:set_hook_execution_notice)
- expect(subject).to redirect_to(edit_project_service_path(project, service))
+ expect(subject).to redirect_to(edit_project_service_path(project, integration))
end
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index d8fb3b226ed..f8474ab1082 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Projects::ServicesController do
let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
it 'returns success' do
- allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
+ allow_any_instance_of(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
put :test, params: project_params
@@ -95,7 +95,7 @@ RSpec.describe Projects::ServicesController do
expect(response).to be_successful
expect(json_response).to be_empty
- expect(BuildkiteService.first).to be_present
+ expect(Integrations::Buildkite.first).to be_present
end
it 'creates the ServiceHook object' do
@@ -103,7 +103,7 @@ RSpec.describe Projects::ServicesController do
expect(response).to be_successful
expect(json_response).to be_empty
- expect(BuildkiteService.first.service_hook).to be_present
+ expect(Integrations::Buildkite.first.service_hook).to be_present
end
def do_put
@@ -145,7 +145,7 @@ RSpec.describe Projects::ServicesController do
end
it 'returns an error response when a network exception is raised' do
- expect_next(SlackService).to receive(:test).and_raise(Errno::ECONNREFUSED)
+ expect_next(Integrations::Slack).to receive(:test).and_raise(Errno::ECONNREFUSED)
put :test, params: project_params
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index d2934ec4e97..7ef5371f2b5 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -509,7 +509,10 @@ RSpec.describe Projects::Settings::OperationsController do
it 'tracks an event' do
expect_snowplow_event(
category: 'project:operations:tracing',
- action: 'external_url_populated'
+ action: 'external_url_populated',
+ user: user,
+ project: project,
+ namespace: project.namespace
)
end
end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index bd299efb5b5..da381357bda 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -160,28 +160,12 @@ RSpec.describe Projects::TemplatesController do
end
shared_examples 'template names request' do
- context 'when feature flag enabled' do
- it 'returns the template names', :aggregate_failures do
- get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['Project Templates'].size).to eq(2)
- expect(json_response['Project Templates'].map { |x| x.slice('name') }).to match(expected_template_names)
- end
- end
-
- context 'when feature flag disabled' do
- before do
- stub_feature_flags(inherited_issuable_templates: false)
- end
-
- it 'returns the template names', :aggregate_failures do
- get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
+ it 'returns the template names', :aggregate_failures do
+ get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(2)
- expect(json_response.map { |x| x.slice('name') }).to match(expected_template_names)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['Project Templates'].size).to eq(2)
+ expect(json_response['Project Templates'].map { |x| x.slice('name') }).to match(expected_template_names)
end
it 'fails for user with no access' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index fdc687496b7..ce229fb861a 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe ProjectsController do
include ProjectForksHelper
using RSpec::Parameterized::TableSyntax
- let_it_be(:project, reload: true) { create(:project, service_desk_enabled: false) }
+ let_it_be(:project, reload: true) { create(:project, :with_export, service_desk_enabled: false) }
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
@@ -243,9 +243,8 @@ RSpec.describe ProjectsController do
get :show, params: { namespace_id: empty_project.namespace, id: empty_project }
end
- it "renders the empty project view and records the experiment user", :aggregate_failures do
+ it "renders the empty project view" do
expect(response).to render_template('empty')
- expect(controller).to have_received(:record_experiment_user).with(:invite_members_empty_project_version_a)
end
end
end
@@ -1350,7 +1349,7 @@ RSpec.describe ProjectsController do
end
end
- describe '#download_export' do
+ describe '#download_export', :clean_gitlab_redis_cache do
let(:action) { :download_export }
context 'object storage enabled' do
@@ -1362,6 +1361,17 @@ RSpec.describe ProjectsController do
end
end
+ context 'when project export file is absent' do
+ it 'alerts the user and returns 302' do
+ project.export_file.file.delete
+
+ get action, params: { namespace_id: project.namespace, id: project }
+
+ expect(flash[:alert]).to include('file containing the export is not available yet')
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+
context 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index ff73c0aafe8..81486c310d4 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -434,6 +434,18 @@ RSpec.describe RegistrationsController do
expect(User.last.last_name).to eq(base_user_params[:last_name])
expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}")
end
+
+ it 'sets the username and caller_id in the context' do
+ expect(controller).to receive(:create).and_wrap_original do |m, *args|
+ m.call(*args)
+
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.user' => base_user_params[:username],
+ 'meta.caller_id' => 'RegistrationsController#create')
+ end
+
+ subject
+ end
end
describe '#destroy' do
@@ -525,5 +537,17 @@ RSpec.describe RegistrationsController do
end
end
end
+
+ it 'sets the username and caller_id in the context' do
+ expect(controller).to receive(:destroy).and_wrap_original do |m, *args|
+ m.call(*args)
+
+ expect(Gitlab::ApplicationContext.current)
+ .to include('meta.user' => user.username,
+ 'meta.caller_id' => 'RegistrationsController#destroy')
+ end
+
+ post :destroy
+ end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 32ac83847aa..3a2986f6cbe 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -258,7 +258,7 @@ RSpec.describe SearchController do
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Cache-Control']).to include('max-age=60, private')
+ expect(response.headers['Cache-Control']).to eq('no-store')
end
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 4d10b979c69..8b02cfa30ab 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -54,7 +54,6 @@ RSpec.describe 'Database schema' do
keys: %w[user_id],
label_links: %w[target_id],
ldap_group_links: %w[group_id],
- lfs_objects_projects: %w[lfs_object_id project_id],
members: %w[source_id created_by_id],
merge_requests: %w[last_edited_by_id state_id],
namespaces: %w[owner_id parent_id],
@@ -75,6 +74,7 @@ RSpec.describe 'Database schema' do
slack_integrations: %w[team_id user_id],
snippets: %w[author_id],
spam_logs: %w[user_id],
+ status_check_responses: %w[external_approval_rule_id],
subscriptions: %w[user_id subscribable_id],
suggestions: %w[commit_id],
taggings: %w[tag_id taggable_id tagger_id],
@@ -270,7 +270,7 @@ RSpec.describe 'Database schema' do
sql = <<~SQL
SELECT table_name, column_name, data_type
FROM information_schema.columns
- WHERE table_catalog = '#{ApplicationRecord.connection_config[:database]}'
+ WHERE table_catalog = '#{ApplicationRecord.connection_db_config.database}'
AND table_schema = 'public'
AND table_name NOT LIKE 'pg_%'
AND data_type = 'jsonb'
diff --git a/spec/deprecation_toolkit_env.rb b/spec/deprecation_toolkit_env.rb
index 57d32b5423c..00d66ff3cdc 100644
--- a/spec/deprecation_toolkit_env.rb
+++ b/spec/deprecation_toolkit_env.rb
@@ -55,9 +55,9 @@ module DeprecationToolkitEnv
# one by one
def self.allowed_kwarg_warning_paths
%w[
- activerecord-6.0.3.6/lib/active_record/migration.rb
- activesupport-6.0.3.6/lib/active_support/cache.rb
- activerecord-6.0.3.6/lib/active_record/relation.rb
+ activerecord-6.0.3.7/lib/active_record/migration.rb
+ activesupport-6.0.3.7/lib/active_support/cache.rb
+ activerecord-6.0.3.7/lib/active_record/relation.rb
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
attr_encrypted-3.1.0/lib/attr_encrypted/adapters/active_record.rb
]
diff --git a/spec/docs_screenshots/wiki_docs.rb b/spec/docs_screenshots/wiki_docs.rb
new file mode 100644
index 00000000000..e7b71408b1c
--- /dev/null
+++ b/spec/docs_screenshots/wiki_docs.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Wiki', :js do
+ include DocsScreenshotHelpers
+ include WikiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace, creator: user) }
+ let(:wiki) { create(:project_wiki, user: user, project: project) }
+
+ before do
+ page.driver.browser.manage.window.resize_to(1366, 1024)
+
+ sign_in(user)
+ visit wiki_path(wiki)
+
+ click_link "Create your first page"
+ end
+
+ context 'switching to content editor' do
+ it 'user/project/wiki/img/use_new_editor_button' do
+ screenshot_area = find('[data-testid="wiki-form-content-fieldset"]')
+ scroll_to screenshot_area
+ expect(screenshot_area).to have_content 'Use the new editor'
+ set_crop_data(screenshot_area, 0)
+ end
+ end
+
+ context 'content editor' do
+ it 'user/project/wiki/img/content_editor' do
+ content_editor_testid = '[data-testid="wiki-form-content-fieldset"]'
+
+ click_button 'Use the new editor'
+
+ expect(page).to have_css(content_editor_testid)
+
+ screenshot_area = find(content_editor_testid)
+ scroll_to screenshot_area
+
+ find("#{content_editor_testid} [contenteditable]").send_keys '## Using the Content Editor'
+
+ set_crop_data(screenshot_area, 0)
+ end
+ end
+end
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index 2ff16604c33..22c436e4159 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -64,8 +64,12 @@ RSpec.describe ApplicationExperiment, :experiment do
end
describe "publishing results" do
- it "doesn't track or push data to the client if we shouldn't track", :snowplow do
+ it "doesn't record, track or push data to the client if we shouldn't track", :snowplow do
allow(subject).to receive(:should_track?).and_return(false)
+ subject.record!
+
+ expect(subject).not_to receive(:record_experiment)
+ expect(subject).not_to receive(:track)
expect(Gon).not_to receive(:push)
subject.publish(:action)
@@ -73,6 +77,22 @@ RSpec.describe ApplicationExperiment, :experiment do
expect_no_snowplow_event
end
+ describe 'recording the experiment' do
+ it 'does not record the experiment if we do not tell it to' do
+ expect(subject).not_to receive(:record_experiment)
+
+ subject.publish
+ end
+
+ it 'records the experiment if we tell it to' do
+ subject.record!
+
+ expect(subject).to receive(:record_experiment)
+
+ subject.publish
+ end
+ end
+
it "tracks the assignment" do
expect(subject).to receive(:track).with(:assignment)
@@ -96,6 +116,49 @@ RSpec.describe ApplicationExperiment, :experiment do
expect(described_class.new('namespaced/stub') { |e| e.exclude! }).to be_excluded
end
+ describe 'recording the experiment subject' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new('namespaced/stub', nil, **context) }
+
+ before do
+ subject.record!
+ end
+
+ context 'when providing a compatible context' do
+ where(:context_key, :object_type) do
+ :namespace | :namespace
+ :group | :namespace
+ :project | :project
+ :user | :user
+ :actor | :user
+ end
+
+ with_them do
+ let(:context) { { context_key => build(object_type) }}
+
+ it 'records the experiment and the experiment subject from the context' do
+ expect { subject.publish }.to change(Experiment, :count).by(1)
+
+ expect(Experiment.last.name).to eq('namespaced/stub')
+ expect(ExperimentSubject.last.send(object_type)).to eq(context[context_key])
+ end
+ end
+ end
+
+ context 'when providing an incompatible or no context' do
+ where(context_hash: [{ foo: :bar }, {}])
+
+ with_them do
+ let(:context) { context_hash }
+
+ it 'does not record the experiment' do
+ expect { subject.publish }.not_to change(Experiment, :count)
+ end
+ end
+ end
+ end
+
describe "tracking events", :snowplow do
it "doesn't track if we shouldn't track" do
allow(subject).to receive(:should_track?).and_return(false)
diff --git a/spec/experiments/members/invite_email_experiment_spec.rb b/spec/experiments/members/invite_email_experiment_spec.rb
index ac4c05e3058..47ae6e529a1 100644
--- a/spec/experiments/members/invite_email_experiment_spec.rb
+++ b/spec/experiments/members/invite_email_experiment_spec.rb
@@ -39,20 +39,14 @@ RSpec.describe Members::InviteEmailExperiment, :clean_gitlab_redis_shared_state
allow(instance_1).to receive(:enabled?).and_return(true)
instance_2 = described_class.new('members/invite_email', **context)
allow(instance_2).to receive(:enabled?).and_return(true)
- instance_3 = described_class.new('members/invite_email', **context)
- allow(instance_3).to receive(:enabled?).and_return(true)
instance_1.try { }
- expect(instance_1.variant.name).to eq('permission_info')
+ expect(instance_1.variant.name).to eq('control')
instance_2.try { }
- expect(instance_2.variant.name).to eq('control')
-
- instance_3.try { }
-
- expect(instance_3.variant.name).to eq('avatar')
+ expect(instance_2.variant.name).to eq('activity')
end
end
diff --git a/spec/factories/bulk_import/trackers.rb b/spec/factories/bulk_import/trackers.rb
index 94340b0f389..22e0aa096fc 100644
--- a/spec/factories/bulk_import/trackers.rb
+++ b/spec/factories/bulk_import/trackers.rb
@@ -19,5 +19,11 @@ FactoryBot.define do
sequence(:jid) { |n| "bulk_import_entity_#{n}" }
end
+
+ trait :failed do
+ status { -1 }
+
+ sequence(:jid) { |n| "bulk_import_entity_#{n}" }
+ end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index f99021ad223..395d3ea598c 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -79,6 +79,7 @@ FactoryBot.define do
trait :pending do
queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
+
status { 'pending' }
end
@@ -237,6 +238,20 @@ FactoryBot.define do
coverage_regex { '/(d+)/' }
end
+ trait :trace_with_coverage do
+ coverage { nil }
+ coverage_regex { '(\d+\.\d+)%' }
+
+ transient do
+ trace_coverage { 60.0 }
+ end
+
+ after(:create) do |build, evaluator|
+ build.trace.set("Coverage #{evaluator.trace_coverage}%")
+ build.trace.archive! if build.complete?
+ end
+ end
+
trait :trace_live do
after(:create) do |build, evaluator|
build.trace.set('BUILD TRACE')
@@ -286,6 +301,15 @@ FactoryBot.define do
trait :queued do
queued_at { Time.now }
+
+ after(:create) do |build|
+ build.create_queuing_entry!
+ end
+ end
+
+ trait :picked do
+ running
+
runner factory: :ci_runner
end
@@ -484,14 +508,6 @@ FactoryBot.define do
end
end
- trait :license_management do
- options do
- {
- artifacts: { reports: { license_management: 'gl-license-management-report.json' } }
- }
- end
- end
-
trait :license_scanning do
options do
{
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 17cd495e217..642437b1119 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -317,21 +317,6 @@ FactoryBot.define do
end
end
- trait :cluster_applications do
- file_type { :cluster_applications }
- file_format { :gzip }
-
- transient do
- file do
- fixture_file_upload(Rails.root.join('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz'), 'application/x-gzip')
- end
- end
-
- after(:build) do |artifact, evaluator|
- artifact.file = evaluator.file
- end
- end
-
trait :correct_checksum do
after(:build) do |artifact, evaluator|
artifact.file_sha256 = Digest::SHA256.file(artifact.file.path).hexdigest
diff --git a/spec/factories/ci/job_token/project_scope_links.rb b/spec/factories/ci/job_token/project_scope_links.rb
new file mode 100644
index 00000000000..a11edd87e4e
--- /dev/null
+++ b/spec/factories/ci/job_token/project_scope_links.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_job_token_project_scope_link, class: 'Ci::JobToken::ProjectScopeLink' do
+ association :source_project, factory: :project
+ association :target_project, factory: :project
+ association :added_by, factory: :user
+ end
+end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 1ff1292c36e..29197768ec0 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -96,26 +96,7 @@ FactoryBot.define do
end
factory :clusters_applications_ingress, class: 'Clusters::Applications::Ingress' do
- modsecurity_enabled { false }
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
-
- trait :modsecurity_blocking do
- modsecurity_enabled { true }
- modsecurity_mode { :blocking }
- end
-
- trait :modsecurity_logging do
- modsecurity_enabled { true }
- modsecurity_mode { :logging }
- end
-
- trait :modsecurity_disabled do
- modsecurity_enabled { false }
- end
-
- trait :modsecurity_not_installed do
- modsecurity_enabled { nil }
- end
end
factory :clusters_applications_cert_manager, class: 'Clusters::Applications::CertManager' do
@@ -151,13 +132,6 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp project)
end
- factory :clusters_applications_fluentd, class: 'Clusters::Applications::Fluentd' do
- host { 'example.com' }
- waf_log_enabled { true }
- cilium_log_enabled { true }
- cluster factory: %i(cluster with_installed_helm provided_by_gcp)
- end
-
factory :clusters_applications_cilium, class: 'Clusters::Applications::Cilium' do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 0daf624ae7e..7666533691e 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -88,6 +88,7 @@ FactoryBot.define do
trait :with_installed_prometheus do
application_prometheus factory: %i(clusters_applications_prometheus installed)
+ integration_prometheus factory: %i(clusters_integrations_prometheus)
end
trait :with_all_applications do
@@ -100,7 +101,6 @@ FactoryBot.define do
application_jupyter factory: %i(clusters_applications_jupyter installed)
application_knative factory: %i(clusters_applications_knative installed)
application_elastic_stack factory: %i(clusters_applications_elastic_stack installed)
- application_fluentd factory: %i(clusters_applications_fluentd installed)
application_cilium factory: %i(clusters_applications_cilium installed)
end
@@ -138,10 +138,6 @@ FactoryBot.define do
cleanup_status { 1 }
end
- trait :cleanup_uninstalling_applications do
- cleanup_status { 2 }
- end
-
trait :cleanup_removing_project_namespaces do
cleanup_status { 3 }
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 072a5f1f402..148ee64fb08 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -16,19 +16,19 @@ FactoryBot.define do
end
trait :production do
- tier { :production }
+ name { 'production' }
end
trait :staging do
- tier { :staging }
+ name { 'staging' }
end
trait :testing do
- tier { :testing }
+ name { 'testing' }
end
trait :development do
- tier { :development }
+ name { 'development' }
end
trait :with_review_app do |environment|
diff --git a/spec/factories/gitlab/database/background_migration/batched_migrations.rb b/spec/factories/gitlab/database/background_migration/batched_migrations.rb
index c03841d8c02..de57e0c1565 100644
--- a/spec/factories/gitlab/database/background_migration/batched_migrations.rb
+++ b/spec/factories/gitlab/database/background_migration/batched_migrations.rb
@@ -9,6 +9,7 @@ FactoryBot.define do
job_class_name { 'CopyColumnUsingBackgroundMigrationJob' }
table_name { :events }
column_name { :id }
+ sequence(:job_arguments) { |n| [["column_#{n}"], ["column_#{n}_convert_to_bigint"]] }
total_tuple_count { 10_000 }
pause_ms { 100 }
end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 5d232a9d09a..bd6e37c1cef 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -58,6 +58,13 @@ FactoryBot.define do
shared_runners_enabled { false }
end
+ trait :with_export do
+ after(:create) do |group, _evaluator|
+ export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz')
+ create(:import_export_upload, group: group, export_file: export_file)
+ end
+ end
+
trait :allow_descendants_override_disabled_shared_runners do
allow_descendants_override_disabled_shared_runners { true }
end
diff --git a/spec/factories/integration_data.rb b/spec/factories/integration_data.rb
index 2541a3d2da3..a6b2693b8df 100644
--- a/spec/factories/integration_data.rb
+++ b/spec/factories/integration_data.rb
@@ -3,15 +3,15 @@
# These factories should not be called directly unless we are testing a _tracker_data model.
# The factories are used when creating integrations.
FactoryBot.define do
- factory :jira_tracker_data do
+ factory :jira_tracker_data, class: 'Integrations::JiraTrackerData' do
integration factory: :jira_service
end
- factory :issue_tracker_data do
+ factory :issue_tracker_data, class: 'Integrations::IssueTrackerData' do
integration
end
- factory :open_project_tracker_data do
+ factory :open_project_tracker_data, class: 'Integrations::OpenProjectTrackerData' do
integration factory: :open_project_service
url { 'http://openproject.example.com'}
token { 'supersecret' }
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index 6bd6deb262a..fd570ca9c50 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -6,7 +6,7 @@ FactoryBot.define do
type { 'Integration' }
end
- factory :custom_issue_tracker_service, class: 'CustomIssueTrackerService' do
+ factory :custom_issue_tracker_integration, class: 'Integrations::CustomIssueTracker' do
project
active { true }
issue_tracker
@@ -38,14 +38,14 @@ FactoryBot.define do
end
end
- factory :drone_ci_service do
+ factory :drone_ci_integration, class: 'Integrations::DroneCi' do
project
active { true }
drone_url { 'https://bamboo.example.com' }
token { 'test' }
end
- factory :jira_service do
+ factory :jira_service, class: 'Integrations::Jira' do
project
active { true }
type { 'JiraService' }
@@ -79,31 +79,31 @@ FactoryBot.define do
end
end
- factory :confluence_service, class: 'Integrations::Confluence' do
+ factory :confluence_integration, class: 'Integrations::Confluence' do
project
active { true }
confluence_url { 'https://example.atlassian.net/wiki' }
end
- factory :bugzilla_service do
+ factory :bugzilla_integration, class: 'Integrations::Bugzilla' do
project
active { true }
issue_tracker
end
- factory :redmine_service do
+ factory :redmine_service, class: 'Integrations::Redmine' do
project
active { true }
issue_tracker
end
- factory :youtrack_service do
+ factory :youtrack_service, class: 'Integrations::Youtrack' do
project
active { true }
issue_tracker
end
- factory :ewm_service do
+ factory :ewm_service, class: 'Integrations::Ewm' do
project
active { true }
issue_tracker
@@ -127,14 +127,14 @@ FactoryBot.define do
end
end
- factory :external_wiki_service do
+ factory :external_wiki_service, class: 'Integrations::ExternalWiki' do
project
- type { ExternalWikiService }
+ type { 'ExternalWikiService' }
active { true }
external_wiki_url { 'http://external-wiki-url.com' }
end
- factory :open_project_service do
+ factory :open_project_service, class: 'Integrations::OpenProject' do
project
active { true }
@@ -160,14 +160,20 @@ FactoryBot.define do
password { 'my-secret-password' }
end
- factory :slack_service do
+ factory :slack_service, class: 'Integrations::Slack' do
project
active { true }
webhook { 'https://slack.service.url' }
type { 'SlackService' }
end
- factory :pipelines_email_service do
+ factory :slack_slash_commands_service, class: 'Integrations::SlackSlashCommands' do
+ project
+ active { true }
+ type { 'SlackSlashCommandsService' }
+ end
+
+ factory :pipelines_email_service, class: 'Integrations::PipelinesEmail' do
project
active { true }
type { 'PipelinesEmailService' }
@@ -182,13 +188,13 @@ FactoryBot.define do
create_data { false }
after(:build) do
- IssueTrackerService.skip_callback(:validation, :before, :handle_properties)
+ Integrations::BaseIssueTracker.skip_callback(:validation, :before, :handle_properties)
end
to_create { |instance| instance.save!(validate: false) }
after(:create) do
- IssueTrackerService.set_callback(:validation, :before, :handle_properties)
+ Integrations::BaseIssueTracker.set_callback(:validation, :before, :handle_properties)
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 0aab41df90b..6f706546402 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -341,6 +341,11 @@ FactoryBot.define do
create(:diff_note_on_merge_request, noteable: mr, project: mr.source_project)
end
end
+ factory :merge_request_with_multiple_diffs do
+ after(:create) do |mr|
+ mr.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+ end
factory :labeled_merge_request do
transient do
diff --git a/spec/factories/operations/feature_flag_scopes.rb b/spec/factories/operations/feature_flag_scopes.rb
index a98c397b8b5..4ca9b53f320 100644
--- a/spec/factories/operations/feature_flag_scopes.rb
+++ b/spec/factories/operations/feature_flag_scopes.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :operations_feature_flag_scope, class: 'Operations::FeatureFlagScope' do
- association :feature_flag, factory: :operations_feature_flag
+ association :feature_flag, factory: [:operations_feature_flag, :legacy_flag]
active { true }
strategies { [{ name: "default", parameters: {} }] }
sequence(:environment_scope) { |n| "review/patch-#{n}" }
diff --git a/spec/factories/operations/feature_flags.rb b/spec/factories/operations/feature_flags.rb
index 7e43d38a04f..32e5ec9fb26 100644
--- a/spec/factories/operations/feature_flags.rb
+++ b/spec/factories/operations/feature_flags.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
sequence(:name) { |n| "feature_flag_#{n}" }
project
active { true }
+ version { :new_version_flag }
trait :legacy_flag do
version { Operations::FeatureFlag.versions['legacy_flag'] }
diff --git a/spec/factories/packages.rb b/spec/factories/packages.rb
index a8020f396fd..cedda8d0854 100644
--- a/spec/factories/packages.rb
+++ b/spec/factories/packages.rb
@@ -75,7 +75,7 @@ FactoryBot.define do
create :debian_package_file, :source, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :dsc, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :deb, evaluator.file_metadatum_trait, package: package
- create :debian_package_file, :deb2, evaluator.file_metadatum_trait, package: package
+ create :debian_package_file, :deb_dev, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :udeb, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :buildinfo, evaluator.file_metadatum_trait, package: package
create :debian_package_file, :changes, evaluator.file_metadatum_trait, package: package
diff --git a/spec/factories/packages/debian/component_file.rb b/spec/factories/packages/debian/component_file.rb
index 19157b3c8c6..9aee91b0973 100644
--- a/spec/factories/packages/debian/component_file.rb
+++ b/spec/factories/packages/debian/component_file.rb
@@ -2,6 +2,10 @@
FactoryBot.define do
factory :debian_project_component_file, class: 'Packages::Debian::ProjectComponentFile' do
+ transient do
+ file_fixture { 'spec/fixtures/packages/debian/distribution/Packages' }
+ end
+
component { association(:debian_project_component) }
architecture { association(:debian_project_architecture, distribution: component.distribution) }
@@ -13,7 +17,7 @@ FactoryBot.define do
file_type { :packages }
after(:build) do |component_file, evaluator|
- component_file.file = fixture_file_upload('spec/fixtures/packages/debian/distribution/Packages')
+ component_file.file = fixture_file_upload(evaluator.file_fixture) if evaluator.file_fixture.present?
end
file_md5 { '12345abcde' }
diff --git a/spec/factories/packages/debian/distribution_key.rb b/spec/factories/packages/debian/distribution_key.rb
new file mode 100644
index 00000000000..6bd457c50d0
--- /dev/null
+++ b/spec/factories/packages/debian/distribution_key.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :debian_project_distribution_key, class: 'Packages::Debian::ProjectDistributionKey' do
+ distribution { association(:debian_project_distribution) }
+
+ private_key { '-----BEGIN PGP PRIVATE KEY BLOCK-----' }
+ passphrase { '12345' }
+ public_key { '-----BEGIN PGP PUBLIC KEY BLOCK-----' }
+ fingerprint { '12345' }
+
+ factory :debian_group_distribution_key, class: 'Packages::Debian::GroupDistributionKey' do
+ distribution { association(:debian_group_distribution) }
+ end
+ end
+end
diff --git a/spec/factories/packages/debian/file_metadatum.rb b/spec/factories/packages/debian/file_metadatum.rb
index 088d0140afe..f761dd18b4e 100644
--- a/spec/factories/packages/debian/file_metadatum.rb
+++ b/spec/factories/packages/debian/file_metadatum.rb
@@ -33,7 +33,53 @@ FactoryBot.define do
file_type { 'deb' }
component { 'main' }
architecture { 'amd64' }
- fields { { 'a': 'b' } }
+ fields do
+ {
+ 'Package' => 'libsample0',
+ 'Source' => package_file.package.name,
+ 'Version' => package_file.package.version,
+ 'Architecture' => 'amd64',
+ 'Maintainer' => "#{FFaker::Name.name} <#{FFaker::Internet.email}>",
+ 'Installed-Size' => '7',
+ 'Section' => 'libs',
+ 'Priority' => 'optional',
+ 'Multi-Arch' => 'same',
+ 'Homepage' => FFaker::Internet.http_url,
+ 'Description' => <<~EOF.rstrip
+ Some mostly empty lib
+ Used in GitLab tests.
+
+ Testing another paragraph.
+ EOF
+ }
+ end
+ end
+
+ trait(:deb_dev) do
+ file_type { 'deb' }
+ component { 'main' }
+ architecture { 'amd64' }
+ fields do
+ {
+ 'Package' => 'sample-dev',
+ 'Source' => "#{package_file.package.name} (#{package_file.package.version})",
+ 'Version' => '1.2.3~binary',
+ 'Architecture' => 'amd64',
+ 'Maintainer' => "#{FFaker::Name.name} <#{FFaker::Internet.email}>",
+ 'Installed-Size' => '7',
+ 'Depends' => 'libsample0 (= 1.2.3~binary)',
+ 'Section' => 'libdevel',
+ 'Priority' => 'optional',
+ 'Multi-Arch' => 'same',
+ 'Homepage' => FFaker::Internet.http_url,
+ 'Description' => <<~EOF.rstrip
+ Some mostly empty development files
+ Used in GitLab tests.
+
+ Testing another paragraph.
+ EOF
+ }
+ end
end
trait(:udeb) do
diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb
index e49e3f36635..b02af85dbeb 100644
--- a/spec/factories/packages/package_file.rb
+++ b/spec/factories/packages/package_file.rb
@@ -125,9 +125,9 @@ FactoryBot.define do
trait(:source) do
file_name { 'sample_1.2.3~alpha2.tar.xz' }
- file_md5 { 'd79b34f58f61ff4ad696d9bd0b8daa68' }
- file_sha1 { '5f8bba5574eb01ac3b1f5e2988e8c29307788236' }
- file_sha256 { 'b5a599e88e7cbdda3bde808160a21ba1dd1ec76b2ec8d4912aae769648d68362' }
+ file_md5 { 'd5ca476e4229d135a88f9c729c7606c9' }
+ file_sha1 { 'c5cfc111ea924842a89a06d5673f07dfd07de8ca' }
+ file_sha256 { '40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da' }
transient do
file_metadatum_trait { :source }
@@ -136,9 +136,9 @@ FactoryBot.define do
trait(:dsc) do
file_name { 'sample_1.2.3~alpha2.dsc' }
- file_md5 { '3b0817804f669e16cdefac583ad88f0e' }
- file_sha1 { '32ecbd674f0bfd310df68484d87752490685a8d6' }
- file_sha256 { '844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba' }
+ file_md5 { 'ceccb6bb3e45ce6550b24234d4023e0f' }
+ file_sha1 { '375ba20ea1789e1e90d469c3454ce49a431d0442' }
+ file_sha256 { '81fc156ba937cdb6215362cc4bf6b8dc47be9b4253ba0f1a4ab10c7ea0c4c4e5' }
transient do
file_metadatum_trait { :dsc }
@@ -156,14 +156,14 @@ FactoryBot.define do
end
end
- trait(:deb2) do
+ trait(:deb_dev) do
file_name { 'sample-dev_1.2.3~binary_amd64.deb' }
- file_md5 { 'd2afbd28e4d74430d22f9504e18bfdf5' }
- file_sha1 { 'f81e4f66c8c6bb899653a3340c157965ee69634f' }
- file_sha256 { '9fbeee2191ce4dab5288fad5ecac1bd369f58fef9a992a880eadf0caf25f086d' }
+ file_md5 { '5fafc04dcae1525e1367b15413e5a5c7' }
+ file_sha1 { 'fcd5220b1501ec150ccf37f06e4da919a8612be4' }
+ file_sha256 { 'b8aa8b73a14bc1e0012d4c5309770f5160a8ea7f9dfe6f45222ea6b8a3c35325' }
transient do
- file_metadatum_trait { :deb }
+ file_metadatum_trait { :deb_dev }
end
end
@@ -180,9 +180,9 @@ FactoryBot.define do
trait(:buildinfo) do
file_name { 'sample_1.2.3~alpha2_amd64.buildinfo' }
- file_md5 { '4e085dd67c120ca967ec314f65770a42' }
- file_sha1 { '0d47e899f3cc67a2253a4629456ff927e0db5c60' }
- file_sha256 { 'f9900d3c94e94b329232668dcbef3dba2d96c07147b15b6dc0533452e4dd8a43' }
+ file_md5 { '12a5ac4f16ad75f8741327ac23b4c0d7' }
+ file_sha1 { '661f7507efa6fdd3763c95581d0baadb978b7ef5' }
+ file_sha256 { 'd0c169e9caa5b303a914b27b5adf69768fe6687d4925905b7d0cd9c0f9d4e56c' }
transient do
file_metadatum_trait { :buildinfo }
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f4f1e1bcbda..cc561ef65a2 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -47,6 +47,7 @@ FactoryBot.define do
import_last_error { nil }
forward_deployment_enabled { nil }
restrict_user_defined_variables { nil }
+ ci_job_token_scope_enabled { nil }
end
before(:create) do |project, evaluator|
@@ -88,6 +89,7 @@ FactoryBot.define do
project.merge_trains_enabled = evaluator.merge_trains_enabled unless evaluator.merge_trains_enabled.nil?
project.keep_latest_artifact = evaluator.keep_latest_artifact unless evaluator.keep_latest_artifact.nil?
project.restrict_user_defined_variables = evaluator.restrict_user_defined_variables unless evaluator.restrict_user_defined_variables.nil?
+ project.ci_job_token_scope_enabled = evaluator.ci_job_token_scope_enabled unless evaluator.ci_job_token_scope_enabled.nil?
if evaluator.import_status
import_state = project.import_state || project.build_import_state
@@ -242,6 +244,8 @@ FactoryBot.define do
branch_name: evaluator.create_branch)
end
+
+ project.track_project_repository
end
end
diff --git a/spec/factories/user_details.rb b/spec/factories/user_details.rb
index 3442f057c44..d3cf0d48577 100644
--- a/spec/factories/user_details.rb
+++ b/spec/factories/user_details.rb
@@ -4,5 +4,6 @@ FactoryBot.define do
factory :user_detail do
user
job_title { 'VP of Sales' }
+ pronouns { nil }
end
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 787e0540fda..80e94fa1628 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -66,6 +66,7 @@ RSpec.describe 'factories' do
# 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
evidence
exported_protected_branch
fork_network_member
@@ -74,6 +75,7 @@ RSpec.describe 'factories' do
milestone_release
namespace
project_broken_repo
+ project_repository
prometheus_alert
prometheus_alert_event
prometheus_metric
@@ -84,6 +86,7 @@ RSpec.describe 'factories' do
release
release_link
self_managed_prometheus_alert_event
+ shard
users_star_project
wiki_page
wiki_page_meta
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 603e757096f..5596ad7bf21 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Admin Appearance' do
it 'create new appearance' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
fill_in 'appearance_title', with: 'MyCompany'
fill_in 'appearance_description', with: 'dev server'
@@ -17,7 +17,7 @@ RSpec.describe 'Admin Appearance' do
fill_in 'appearance_profile_image_guidelines', with: 'Custom profile image guidelines'
click_button 'Update appearance settings'
- expect(current_path).to eq admin_appearances_path
+ expect(current_path).to eq admin_application_settings_appearances_path
expect(page).to have_content 'Appearance'
expect(page).to have_field('appearance_title', with: 'MyCompany')
@@ -31,7 +31,7 @@ RSpec.describe 'Admin Appearance' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
click_link "Sign-in page"
expect_custom_sign_in_appearance(appearance)
@@ -41,7 +41,7 @@ RSpec.describe 'Admin Appearance' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
click_link "New project page"
expect_custom_new_project_appearance(appearance)
@@ -55,7 +55,7 @@ RSpec.describe 'Admin Appearance' do
context 'when system header and footer messages are empty' do
it 'shows custom system header and footer fields' do
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
expect(page).to have_field('appearance_header_message', with: '')
expect(page).to have_field('appearance_footer_message', with: '')
@@ -70,7 +70,7 @@ RSpec.describe 'Admin Appearance' do
end
it 'shows custom system header and footer fields' do
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
expect(page).to have_field('appearance_header_message', with: appearance.header_message)
expect(page).to have_field('appearance_footer_message', with: appearance.footer_message)
@@ -99,7 +99,7 @@ RSpec.describe 'Admin Appearance' do
before do
sign_in(create(:admin))
gitlab_enable_admin_mode_sign_in(admin)
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
fill_in 'appearance_profile_image_guidelines', with: 'Custom profile image guidelines, please :smile:!'
click_button 'Update appearance settings'
end
@@ -115,7 +115,7 @@ RSpec.describe 'Admin Appearance' do
it 'appearance logo' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
attach_file(:appearance_logo, logo_fixture)
click_button 'Update appearance settings'
@@ -128,7 +128,7 @@ RSpec.describe 'Admin Appearance' do
it 'header logos' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
attach_file(:appearance_header_logo, logo_fixture)
click_button 'Update appearance settings'
@@ -141,7 +141,7 @@ RSpec.describe 'Admin Appearance' do
it 'Favicon' do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
- visit admin_appearances_path
+ visit admin_application_settings_appearances_path
attach_file(:appearance_favicon, logo_fixture)
click_button 'Update appearance settings'
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 3fed402267c..a501efd82ed 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -37,24 +37,6 @@ RSpec.describe 'Admin::Hooks' do
expect(page).to have_content('foo.rb')
expect(page).to have_content('bar.clj')
end
-
- context 'deprecation warning' do
- it 'shows warning for plugins directory' do
- allow(Gitlab::FileHook).to receive(:files).and_return(['plugins/foo.rb'])
-
- visit admin_hooks_path
-
- expect(page).to have_content('Plugins directory is deprecated and will be removed in 14.0')
- end
-
- it 'does not show warning for file_hooks directory' do
- allow(Gitlab::FileHook).to receive(:files).and_return(['file_hooks/foo.rb'])
-
- visit admin_hooks_path
-
- expect(page).not_to have_content('Plugins directory is deprecated and will be removed in 14.0')
- end
- end
end
describe 'New Hook' do
diff --git a/spec/features/admin/admin_mode/logout_spec.rb b/spec/features/admin/admin_mode/logout_spec.rb
index 664eb51e58f..efb4baa8164 100644
--- a/spec/features/admin/admin_mode/logout_spec.rb
+++ b/spec/features/admin/admin_mode/logout_spec.rb
@@ -5,28 +5,32 @@ require 'spec_helper'
RSpec.describe 'Admin Mode Logout', :js do
include TermsHelper
include UserLoginHelper
+ include Spec::Support::Helpers::Features::TopNavSpecHelpers
let(:user) { create(:admin) }
shared_examples 'combined_menu: feature flag examples' do
before do
- gitlab_sign_in(user)
+ # TODO: This used to use gitlab_sign_in, instead of sign_in, but that is buggy. See
+ # this issue to look into why: https://gitlab.com/gitlab-org/gitlab/-/issues/331851
+ sign_in(user)
gitlab_enable_admin_mode_sign_in(user)
visit admin_root_path
end
it 'disable removes admin mode and redirects to root page' do
- pending_on_combined_menu_flag
-
gitlab_disable_admin_mode
expect(current_path).to eq root_path
- expect(page).to have_link(href: new_admin_session_path)
+
+ open_top_nav
+
+ within_top_nav do
+ expect(page).to have_link(href: new_admin_session_path)
+ end
end
it 'disable shows flash notice' do
- pending_on_combined_menu_flag
-
gitlab_disable_admin_mode
expect(page).to have_selector('.flash-notice')
@@ -38,17 +42,20 @@ RSpec.describe 'Admin Mode Logout', :js do
end
it 'disable removes admin mode and redirects to root page' do
- pending_on_combined_menu_flag
-
gitlab_disable_admin_mode
expect(current_path).to eq root_path
- expect(page).to have_link(href: new_admin_session_path)
+
+ open_top_nav
+
+ within_top_nav do
+ expect(page).to have_link(href: new_admin_session_path)
+ end
end
end
end
- context 'with combined_menu: feature flag on' do
+ context 'with combined_menu feature flag on' do
let(:needs_rewrite_for_combined_menu_flag_on) { true }
before do
@@ -67,8 +74,4 @@ RSpec.describe 'Admin Mode Logout', :js do
it_behaves_like 'combined_menu: feature flag examples'
end
-
- def pending_on_combined_menu_flag
- pending 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587' if needs_rewrite_for_combined_menu_flag_on
- end
end
diff --git a/spec/features/admin/admin_mode_spec.rb b/spec/features/admin/admin_mode_spec.rb
index 4df035b13e8..9fd83f4af6d 100644
--- a/spec/features/admin/admin_mode_spec.rb
+++ b/spec/features/admin/admin_mode_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Admin mode' do
include MobileHelpers
+ include Spec::Support::Helpers::Features::TopNavSpecHelpers
include StubENV
let(:admin) { create(:admin) }
@@ -21,6 +22,7 @@ RSpec.describe 'Admin mode' do
context 'when not in admin mode' do
it 'has no leave admin mode button' do
visit new_admin_session_path
+ open_top_nav
page.within('.navbar-sub-nav') do
expect(page).not_to have_link(href: destroy_admin_session_path)
@@ -28,12 +30,11 @@ RSpec.describe 'Admin mode' do
end
it 'can open pages not in admin scope' do
- pending_on_combined_menu_flag
-
visit new_admin_session_path
+ open_top_nav_projects
- page.within('.navbar-sub-nav') do
- find_all('a', text: 'Projects').first.click
+ within_top_nav do
+ click_link('Your projects')
end
expect(page).to have_current_path(dashboard_projects_path)
@@ -78,71 +79,66 @@ RSpec.describe 'Admin mode' do
end
it 'contains link to leave admin mode' do
- pending_on_combined_menu_flag
+ open_top_nav
- page.within('.navbar-sub-nav') do
+ within_top_nav do
expect(page).to have_link(href: destroy_admin_session_path)
end
end
it 'can leave admin mode using main dashboard link', :js do
- pending_on_combined_menu_flag
+ gitlab_disable_admin_mode
- page.within('.navbar-sub-nav') do
- click_on 'Leave Admin Mode'
+ open_top_nav
+ within_top_nav do
expect(page).to have_link(href: new_admin_session_path)
end
end
it 'can leave admin mode using dropdown menu on smaller screens', :js do
- pending_on_combined_menu_flag
+ skip('pending responsive development under :combined_menu feature flag') if Feature.enabled?(:combined_menu, default_enabled: :yaml)
resize_screen_xs
visit root_dashboard_path
- find('.header-more').click
+ find('.header-more').click unless Feature.enabled?(:combined_menu, default_enabled: :yaml)
- page.within '.navbar-sub-nav' do
- click_on 'Leave Admin Mode'
+ gitlab_disable_admin_mode
- find('.header-more').click
+ open_top_nav
+ find('.header-more').click unless Feature.enabled?(:combined_menu, default_enabled: :yaml)
- expect(page).to have_link(href: new_admin_session_path)
- end
+ expect(page).to have_link(href: new_admin_session_path)
end
it 'can open pages not in admin scope' do
- pending_on_combined_menu_flag
-
- page.within('.navbar-sub-nav') do
- find_all('a', text: 'Projects').first.click
+ open_top_nav_projects
- expect(page).to have_current_path(dashboard_projects_path)
+ within_top_nav do
+ click_link('Your projects')
end
+
+ expect(page).to have_current_path(dashboard_projects_path)
end
context 'nav bar' do
it 'shows admin dashboard links on bigger screen' do
- pending_on_combined_menu_flag
-
visit root_dashboard_path
+ open_top_nav
- page.within '.navbar' do
- expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
- expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
- end
+ link_text = Feature.enabled?(:combined_menu, default_enabled: :yaml) ? 'Admin' : 'Admin Area'
+ expect(page).to have_link(text: link_text, href: admin_root_path, visible: true)
+ expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
end
it 'relocates admin dashboard links to dropdown list on smaller screen', :js do
- pending_on_combined_menu_flag
+ skip('pending responsive development under :combined_menu feature flag') if Feature.enabled?(:combined_menu, default_enabled: :yaml)
resize_screen_xs
visit root_dashboard_path
- page.within '.navbar' do
- expect(page).not_to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
- end
+ expect(page).not_to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
find('.header-more').click
@@ -159,11 +155,11 @@ RSpec.describe 'Admin mode' do
end
it 'can leave admin mode', :js do
- pending_on_combined_menu_flag
+ gitlab_disable_admin_mode
- page.within('.navbar-sub-nav') do
- click_on 'Leave Admin Mode'
+ open_top_nav
+ within_top_nav do
expect(page).to have_link(href: new_admin_session_path)
end
end
@@ -179,16 +175,15 @@ RSpec.describe 'Admin mode' do
it 'shows no admin mode buttons in navbar' do
visit admin_root_path
+ open_top_nav
- page.within('.navbar-sub-nav') do
- expect(page).not_to have_link(href: new_admin_session_path)
- expect(page).not_to have_link(href: destroy_admin_session_path)
- end
+ expect(page).not_to have_link(href: new_admin_session_path)
+ expect(page).not_to have_link(href: destroy_admin_session_path)
end
end
end
- context 'with combined_menu: feature flag on' do
+ context 'with combined_menu feature flag on', :js do
let(:needs_rewrite_for_combined_menu_flag_on) { true }
before do
@@ -207,8 +202,4 @@ RSpec.describe 'Admin mode' do
it_behaves_like 'combined_menu: feature flag examples'
end
-
- def pending_on_combined_menu_flag
- pending 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587' if needs_rewrite_for_combined_menu_flag_on
- end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 4e0dcbdf075..d7a267fec69 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -17,6 +17,10 @@ RSpec.describe "Admin Runners" do
describe "Runners page" do
let(:pipeline) { create(:ci_pipeline) }
+ before do
+ stub_feature_flags(runner_list_view_vue_ui: false)
+ end
+
context "when there are runners" do
it 'has all necessary texts' do
runner = create(:ci_runner, contacted_at: Time.now)
@@ -240,7 +244,7 @@ RSpec.describe "Admin Runners" do
it 'shows the label and does not show the project count' do
visit admin_runners_path
- within "#runner_#{runner.id}" do
+ within "[data-testid='runner-row-#{runner.id}']" do
expect(page).to have_selector '.badge', text: 'group'
expect(page).to have_text 'n/a'
end
@@ -253,7 +257,7 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
- within "#runner_#{runner.id}" do
+ within "[data-testid='runner-row-#{runner.id}']" do
expect(page).to have_selector '.badge', text: 'shared'
expect(page).to have_text 'n/a'
end
@@ -267,12 +271,36 @@ RSpec.describe "Admin Runners" do
visit admin_runners_path
- within "#runner_#{runner.id}" do
+ within "[data-testid='runner-row-#{runner.id}']" do
expect(page).to have_selector '.badge', text: 'specific'
expect(page).to have_text '1'
end
end
end
+
+ describe 'runners registration token' do
+ let!(:token) { Gitlab::CurrentSettings.runners_registration_token }
+
+ before do
+ visit admin_runners_path
+ end
+
+ it 'has a registration token' do
+ expect(page.find('[data-testid="registration_token"]')).to have_content(token)
+ end
+
+ describe 'reset registration token' do
+ let(:page_token) { find('[data-testid="registration_token"]').text }
+
+ before do
+ click_button 'Reset registration token'
+ end
+
+ it 'changes registration token' do
+ expect(page_token).not_to eq token
+ end
+ end
+ end
end
describe "Runner show page" do
@@ -381,28 +409,4 @@ RSpec.describe "Admin Runners" do
end
end
end
-
- describe 'runners registration token' do
- let!(:token) { Gitlab::CurrentSettings.runners_registration_token }
-
- before do
- visit admin_runners_path
- end
-
- it 'has a registration token' do
- expect(page.find('#registration_token')).to have_content(token)
- end
-
- describe 'reload registration token' do
- let(:page_token) { find('#registration_token').text }
-
- before do
- click_button 'Reset registration token'
- end
-
- it 'changes registration token' do
- expect(page_token).not_to eq token
- end
- end
- end
end
diff --git a/spec/features/admin/admin_search_settings_spec.rb b/spec/features/admin/admin_search_settings_spec.rb
index cd61a1db6f3..989cb7cc787 100644
--- a/spec/features/admin/admin_search_settings_spec.rb
+++ b/spec/features/admin/admin_search_settings_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Admin searches application settings', :js do
context 'in appearances page' do
before do
- visit(admin_appearances_path)
+ visit(admin_application_settings_appearances_path)
end
it_behaves_like 'cannot search settings'
diff --git a/spec/features/admin/admin_sees_background_migrations_spec.rb b/spec/features/admin/admin_sees_background_migrations_spec.rb
new file mode 100644
index 00000000000..d848a8352bc
--- /dev/null
+++ b/spec/features/admin/admin_sees_background_migrations_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Admin > Admin sees background migrations" do
+ let_it_be(:admin) { create(:admin) }
+
+ let_it_be(:active_migration) { create(:batched_background_migration, table_name: 'active', status: :active) }
+ let_it_be(:failed_migration) { create(:batched_background_migration, table_name: 'failed', status: :failed, total_tuple_count: 100) }
+ let_it_be(:finished_migration) { create(:batched_background_migration, table_name: 'finished', status: :finished) }
+
+ before_all do
+ create(:batched_background_migration_job, batched_migration: failed_migration, batch_size: 30, status: :succeeded)
+ end
+
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ end
+
+ it 'can navigate to background migrations' do
+ visit admin_root_path
+
+ within '.nav-sidebar' do
+ link = find_link 'Background Migrations'
+
+ link.click
+
+ expect(page).to have_current_path(admin_background_migrations_path)
+ expect(link).to have_ancestor(:css, 'li.active')
+ end
+ end
+
+ it 'can view queued migrations' do
+ visit admin_background_migrations_path
+
+ within '#content-body' do
+ expect(page).to have_selector('tbody tr', count: 1)
+
+ expect(page).to have_content(active_migration.job_class_name)
+ expect(page).to have_content(active_migration.table_name)
+ expect(page).to have_content('0.00%')
+ expect(page).to have_content(active_migration.status.humanize)
+ end
+ end
+
+ it 'can view failed migrations' do
+ visit admin_background_migrations_path
+
+ within '#content-body' do
+ tab = find_link 'Failed'
+ tab.click
+
+ expect(page).to have_current_path(admin_background_migrations_path(tab: 'failed'))
+ expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo')
+
+ expect(page).to have_selector('tbody tr', count: 1)
+
+ expect(page).to have_content(failed_migration.job_class_name)
+ expect(page).to have_content(failed_migration.table_name)
+ expect(page).to have_content('30.00%')
+ expect(page).to have_content(failed_migration.status.humanize)
+ end
+ end
+
+ it 'can view finished migrations' do
+ visit admin_background_migrations_path
+
+ within '#content-body' do
+ tab = find_link 'Finished'
+ tab.click
+
+ expect(page).to have_current_path(admin_background_migrations_path(tab: 'finished'))
+ expect(tab[:class]).to include('gl-tab-nav-item-active', 'gl-tab-nav-item-active-indigo')
+
+ expect(page).to have_selector('tbody tr', count: 1)
+
+ expect(page).to have_content(finished_migration.job_class_name)
+ expect(page).to have_content(finished_migration.table_name)
+ expect(page).to have_content('100.00%')
+ expect(page).to have_content(finished_migration.status.humanize)
+ end
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 0a7113a5559..c289c18126d 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -8,9 +8,11 @@ RSpec.describe 'Admin updates settings' do
include UsageDataHelpers
let(:admin) { create(:admin) }
+ let(:dot_com?) { false }
context 'application setting :admin_mode is enabled', :request_store do
before do
+ allow(Gitlab).to receive(:com?).and_return(dot_com?)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
@@ -127,6 +129,37 @@ RSpec.describe 'Admin updates settings' do
expect(user_internal_regex['placeholder']).to eq 'Regex pattern'
end
+ context 'Dormant users' 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
+ end
+
+ context 'when not Gitlab.com' do
+ let(:dot_com?) { false }
+
+ it 'change Dormant users' do
+ expect(page).to have_unchecked_field('Deactivate dormant users after 90 days of inactivity')
+ expect(current_settings.deactivate_dormant_users).to be_falsey
+
+ page.within('.as-account-limit') do
+ check 'application_setting_deactivate_dormant_users'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+
+ page.refresh
+
+ expect(current_settings.deactivate_dormant_users).to be_truthy
+ expect(page).to have_checked_field('Deactivate dormant users after 90 days of inactivity')
+ end
+ end
+ end
+
context 'Change Sign-up restrictions' do
context 'Require Admin approval for new signup setting' do
it 'changes the setting', :js do
@@ -315,19 +348,6 @@ RSpec.describe 'Admin updates settings' do
visit integrations_admin_application_settings_path
end
- it 'allows user to dismiss deprecation notice' do
- expect(page).to have_content('Some settings have moved')
-
- click_button 'Dismiss'
- wait_for_requests
-
- expect(page).not_to have_content('Some settings have moved')
-
- visit integrations_admin_application_settings_path
-
- expect(page).not_to have_content('Some settings have moved')
- end
-
it 'shows integrations table' do
expect(page).to have_selector '[data-testid="inactive-integrations-table"]'
end
diff --git a/spec/features/admin/clusters/applications_spec.rb b/spec/features/admin/clusters/applications_spec.rb
deleted file mode 100644
index e083e4fee4c..00000000000
--- a/spec/features/admin/clusters/applications_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
-
-RSpec.describe 'Instance-level Cluster Applications', :js do
- include GoogleApi::CloudPlatformHelpers
-
- let(:user) { create(:admin) }
-
- before do
- sign_in(user)
- gitlab_enable_admin_mode_sign_in(user)
- end
-
- describe 'Installing applications' do
- include_examples "installing applications on a cluster" do
- let(:cluster_path) { admin_cluster_path(cluster) }
- let(:cluster_factory_args) { [:instance] }
- end
- end
-end
diff --git a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
new file mode 100644
index 00000000000..6f091d37995
--- /dev/null
+++ b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User activates the instance-level Mattermost Slash Command integration', :js do
+ include_context 'instance integration activation'
+
+ before do
+ stub_mattermost_setting(enabled: true)
+ visit_instance_integration('Mattermost slash commands')
+ end
+
+ let(:edit_path) { edit_admin_application_settings_integration_path(:mattermost_slash_commands) }
+
+ include_examples 'user activates the Mattermost Slash Command integration'
+end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index 01341398135..3599658ee56 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -356,27 +356,19 @@ RSpec.describe 'Admin::Users::User' do
end
end
- [true, false].each do |vue_admin_users|
- context "with vue_admin_users feature flag set to #{vue_admin_users}", js: vue_admin_users do
- before do
- stub_feature_flags(vue_admin_users: vue_admin_users)
- end
-
- describe 'GET /admin/users' do
- context 'user pending approval' do
- it 'shows user info', :aggregate_failures do
- user = create(:user, :blocked_pending_approval)
+ describe 'GET /admin/users', :js do
+ context 'user pending approval' do
+ it 'shows user info', :aggregate_failures do
+ user = create(:user, :blocked_pending_approval)
- visit admin_users_path
- click_link 'Pending approval'
- click_link user.name
+ visit admin_users_path
+ click_link 'Pending approval'
+ click_link user.name
- expect(page).to have_content(user.name)
- expect(page).to have_content('Pending approval')
- expect(page).to have_link('Approve user')
- expect(page).to have_link('Reject request')
- end
- end
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('Pending approval')
+ expect(page).to have_link('Approve user')
+ expect(page).to have_link('Reject request')
end
end
end
diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb
index d3931373ee3..187fa6fc2a4 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -11,294 +11,318 @@ RSpec.describe 'Admin::Users' do
gitlab_enable_admin_mode_sign_in(current_user)
end
- [true, false].each do |vue_admin_users|
- context "with vue_admin_users feature flag set to #{vue_admin_users}", js: vue_admin_users do
- before do
- stub_feature_flags(vue_admin_users: vue_admin_users)
- end
+ describe 'GET /admin/users', :js do
+ before do
+ visit admin_users_path
+ end
- describe 'GET /admin/users' do
- before do
- visit admin_users_path
- end
+ it "is ok" do
+ expect(current_path).to eq(admin_users_path)
+ end
- it "is ok" do
- expect(current_path).to eq(admin_users_path)
- end
+ it "has users list" do
+ current_user.reload
- it "has users list" do
- current_user.reload
+ expect(page).to have_content(current_user.email)
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content(current_user.created_at.strftime('%e %b, %Y'))
+ expect(page).to have_content(user.email)
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('Projects')
- expect(page).to have_content(current_user.email)
- expect(page).to have_content(current_user.name)
- expect(page).to have_content(current_user.created_at.strftime('%e %b, %Y'))
- expect(page).to have_content(user.email)
- expect(page).to have_content(user.name)
- expect(page).to have_content('Projects')
+ click_user_dropdown_toggle(user.id)
- click_user_dropdown_toggle(user.id)
+ expect(page).to have_button('Block')
+ expect(page).to have_button('Deactivate')
+ expect(page).to have_button('Delete user')
+ expect(page).to have_button('Delete user and contributions')
+ end
- expect(page).to have_button('Block')
- expect(page).to have_button('Deactivate')
- expect(page).to have_button('Delete user')
- expect(page).to have_button('Delete user and contributions')
- end
+ it 'clicking edit user takes us to edit page', :aggregate_failures do
+ page.within("[data-testid='user-actions-#{user.id}']") do
+ click_link 'Edit'
+ end
- it 'clicking edit user takes us to edit page', :aggregate_failures do
- page.within("[data-testid='user-actions-#{user.id}']") do
- click_link 'Edit'
- end
+ expect(page).to have_content('Name')
+ expect(page).to have_content('Password')
+ end
- expect(page).to have_content('Name')
- expect(page).to have_content('Password')
- end
+ describe 'view extra user information' do
+ it 'shows the user popover on hover', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/11290' do
+ expect(page).not_to have_selector('#__BV_popover_1__')
- describe 'view extra user information' do
- it 'shows the user popover on hover', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/11290' do
- expect(page).not_to have_selector('#__BV_popover_1__')
+ first_user_link = page.first('.js-user-link')
+ first_user_link.hover
- first_user_link = page.first('.js-user-link')
- first_user_link.hover
+ expect(page).to have_selector('#__BV_popover_1__')
+ end
+ end
- expect(page).to have_selector('#__BV_popover_1__')
- end
- end
+ context 'user project count' do
+ before do
+ project = create(:project)
+ project.add_maintainer(current_user)
+ end
- context 'user project count' do
- before do
- project = create(:project)
- project.add_maintainer(current_user)
- end
+ it 'displays count of users projects' do
+ visit admin_users_path
- it 'displays count of users projects' do
- visit admin_users_path
+ expect(page.find("[data-testid='user-project-count-#{current_user.id}']").text).to eq("1")
+ end
+ end
+
+ describe 'tabs' do
+ it 'has multiple tabs to filter users' do
+ expect(page).to have_link('Active', href: admin_users_path)
+ expect(page).to have_link('Admins', href: admin_users_path(filter: 'admins'))
+ expect(page).to have_link('2FA Enabled', href: admin_users_path(filter: 'two_factor_enabled'))
+ expect(page).to have_link('2FA Disabled', href: admin_users_path(filter: 'two_factor_disabled'))
+ expect(page).to have_link('External', href: admin_users_path(filter: 'external'))
+ expect(page).to have_link('Blocked', href: admin_users_path(filter: 'blocked'))
+ expect(page).to have_link('Deactivated', href: admin_users_path(filter: 'deactivated'))
+ expect(page).to have_link('Without projects', href: admin_users_path(filter: 'wop'))
+ end
- expect(page.find("[data-testid='user-project-count-#{current_user.id}']").text).to eq("1")
- end
+ context '`Pending approval` tab' do
+ before do
+ visit admin_users_path
end
- describe 'tabs' do
- it 'has multiple tabs to filter users' do
- expect(page).to have_link('Active', href: admin_users_path)
- expect(page).to have_link('Admins', href: admin_users_path(filter: 'admins'))
- expect(page).to have_link('2FA Enabled', href: admin_users_path(filter: 'two_factor_enabled'))
- expect(page).to have_link('2FA Disabled', href: admin_users_path(filter: 'two_factor_disabled'))
- expect(page).to have_link('External', href: admin_users_path(filter: 'external'))
- expect(page).to have_link('Blocked', href: admin_users_path(filter: 'blocked'))
- expect(page).to have_link('Banned', href: admin_users_path(filter: 'banned'))
- expect(page).to have_link('Deactivated', href: admin_users_path(filter: 'deactivated'))
- expect(page).to have_link('Without projects', href: admin_users_path(filter: 'wop'))
- end
-
- context '`Pending approval` tab' do
- before do
- visit admin_users_path
- end
-
- it 'shows the `Pending approval` tab' do
- expect(page).to have_link('Pending approval', href: admin_users_path(filter: 'blocked_pending_approval'))
- end
- end
+ it 'shows the `Pending approval` tab' do
+ expect(page).to have_link('Pending approval', href: admin_users_path(filter: 'blocked_pending_approval'))
end
+ end
+ end
- describe 'search and sort' do
- before_all do
- create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago)
- create(:user, name: 'Foo Baz', last_activity_on: 2.days.ago)
- create(:user, name: 'Dmitriy')
- end
+ describe 'search and sort' do
+ before_all do
+ create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago)
+ create(:user, name: 'Foo Baz', last_activity_on: 2.days.ago)
+ create(:user, name: 'Dmitriy')
+ end
- it 'searches users by name' do
- visit admin_users_path(search_query: 'Foo')
+ it 'searches users by name' do
+ visit admin_users_path(search_query: 'Foo')
- expect(page).to have_content('Foo Bar')
- expect(page).to have_content('Foo Baz')
- expect(page).not_to have_content('Dmitriy')
- end
+ expect(page).to have_content('Foo Bar')
+ expect(page).to have_content('Foo Baz')
+ expect(page).not_to have_content('Dmitriy')
+ end
- it 'sorts users by name' do
- visit admin_users_path
+ it 'sorts users by name' do
+ visit admin_users_path
- sort_by('Name')
+ sort_by('Name')
- expect(first_row.text).to include('Dmitriy')
- expect(second_row.text).to include('Foo Bar')
- end
+ expect(first_row.text).to include('Dmitriy')
+ expect(second_row.text).to include('Foo Bar')
+ end
- it 'sorts search results only' do
- visit admin_users_path(search_query: 'Foo')
+ it 'sorts search results only' do
+ visit admin_users_path(search_query: 'Foo')
- sort_by('Name')
- expect(page).not_to have_content('Dmitriy')
- expect(first_row.text).to include('Foo Bar')
- expect(second_row.text).to include('Foo Baz')
- end
+ sort_by('Name')
+ expect(page).not_to have_content('Dmitriy')
+ expect(first_row.text).to include('Foo Bar')
+ expect(second_row.text).to include('Foo Baz')
+ end
- it 'searches with respect of sorting' do
- visit admin_users_path(sort: 'Name')
+ it 'searches with respect of sorting' do
+ visit admin_users_path(sort: 'Name')
- fill_in :search_query, with: 'Foo'
- click_button('Search users')
+ fill_in :search_query, with: 'Foo'
+ click_button('Search users')
- expect(first_row.text).to include('Foo Bar')
- expect(second_row.text).to include('Foo Baz')
- end
+ expect(first_row.text).to include('Foo Bar')
+ expect(second_row.text).to include('Foo Baz')
+ end
- it 'sorts users by recent last activity' do
- visit admin_users_path(search_query: 'Foo')
+ it 'sorts users by recent last activity' do
+ visit admin_users_path(search_query: 'Foo')
- sort_by('Recent last activity')
+ sort_by('Recent last activity')
- expect(first_row.text).to include('Foo Baz')
- expect(second_row.text).to include('Foo Bar')
- end
+ expect(first_row.text).to include('Foo Baz')
+ expect(second_row.text).to include('Foo Bar')
+ end
- it 'sorts users by oldest last activity' do
- visit admin_users_path(search_query: 'Foo')
+ it 'sorts users by oldest last activity' do
+ visit admin_users_path(search_query: 'Foo')
- sort_by('Oldest last activity')
+ sort_by('Oldest last activity')
- expect(first_row.text).to include('Foo Bar')
- expect(second_row.text).to include('Foo Baz')
- end
+ expect(first_row.text).to include('Foo Bar')
+ expect(second_row.text).to include('Foo Baz')
+ end
+ end
+
+ describe 'Two-factor Authentication filters' do
+ it 'counts users who have enabled 2FA' do
+ create(:user, :two_factor)
+
+ visit admin_users_path
+
+ page.within('.filter-two-factor-enabled small') do
+ expect(page).to have_content('1')
end
+ end
- describe 'Two-factor Authentication filters' do
- it 'counts users who have enabled 2FA' do
- create(:user, :two_factor)
+ it 'filters by users who have enabled 2FA' do
+ user = create(:user, :two_factor)
- visit admin_users_path
+ visit admin_users_path
+ click_link '2FA Enabled'
- page.within('.filter-two-factor-enabled small') do
- expect(page).to have_content('1')
- end
- end
+ expect(page).to have_content(user.email)
+ end
- it 'filters by users who have enabled 2FA' do
- user = create(:user, :two_factor)
+ it 'counts users who have not enabled 2FA' do
+ visit admin_users_path
- visit admin_users_path
- click_link '2FA Enabled'
+ page.within('.filter-two-factor-disabled small') do
+ expect(page).to have_content('2') # Including admin
+ end
+ end
- expect(page).to have_content(user.email)
- end
+ it 'filters by users who have not enabled 2FA' do
+ visit admin_users_path
+ click_link '2FA Disabled'
- it 'counts users who have not enabled 2FA' do
- visit admin_users_path
+ expect(page).to have_content(user.email)
+ end
+ end
- page.within('.filter-two-factor-disabled small') do
- expect(page).to have_content('2') # Including admin
- end
- end
+ describe 'Pending approval filter' do
+ it 'counts users who are pending approval' do
+ create_list(:user, 2, :blocked_pending_approval)
- it 'filters by users who have not enabled 2FA' do
- visit admin_users_path
- click_link '2FA Disabled'
+ visit admin_users_path
- expect(page).to have_content(user.email)
- end
+ page.within('.filter-blocked-pending-approval small') do
+ expect(page).to have_content('2')
end
+ end
- describe 'Pending approval filter' do
- it 'counts users who are pending approval' do
- create_list(:user, 2, :blocked_pending_approval)
+ it 'filters by users who are pending approval' do
+ user = create(:user, :blocked_pending_approval)
- visit admin_users_path
+ visit admin_users_path
+ click_link 'Pending approval'
- page.within('.filter-blocked-pending-approval small') do
- expect(page).to have_content('2')
- end
- end
+ expect(page).to have_content(user.email)
+ end
+ end
- it 'filters by users who are pending approval' do
- user = create(:user, :blocked_pending_approval)
+ context 'when blocking/unblocking a user' do
+ it 'shows confirmation and allows blocking and unblocking', :js do
+ expect(page).to have_content(user.email)
- visit admin_users_path
- click_link 'Pending approval'
+ click_action_in_user_dropdown(user.id, 'Block')
- expect(page).to have_content(user.email)
- end
- end
+ wait_for_requests
- context 'when blocking/unblocking a user' do
- it 'shows confirmation and allows blocking and unblocking', :js do
- expect(page).to have_content(user.email)
+ expect(page).to have_content('Block user')
+ expect(page).to have_content('Blocking user has the following effects')
+ expect(page).to have_content('User will not be able to login')
+ expect(page).to have_content('Owned groups will be left')
- click_action_in_user_dropdown(user.id, 'Block')
+ find('.modal-footer button', text: 'Block').click
- wait_for_requests
+ wait_for_requests
- expect(page).to have_content('Block user')
- expect(page).to have_content('Blocking user has the following effects')
- expect(page).to have_content('User will not be able to login')
- expect(page).to have_content('Owned groups will be left')
+ expect(page).to have_content('Successfully blocked')
+ expect(page).not_to have_content(user.email)
- find('.modal-footer button', text: 'Block').click
+ click_link 'Blocked'
- wait_for_requests
+ wait_for_requests
- expect(page).to have_content('Successfully blocked')
- expect(page).not_to have_content(user.email)
+ expect(page).to have_content(user.email)
- click_link 'Blocked'
+ click_action_in_user_dropdown(user.id, 'Unblock')
- wait_for_requests
+ expect(page).to have_content('Unblock user')
+ expect(page).to have_content('You can always block their account again if needed.')
- expect(page).to have_content(user.email)
+ find('.modal-footer button', text: 'Unblock').click
- click_action_in_user_dropdown(user.id, 'Unblock')
+ wait_for_requests
- expect(page).to have_content('Unblock user')
- expect(page).to have_content('You can always block their account again if needed.')
+ expect(page).to have_content('Successfully unblocked')
+ expect(page).not_to have_content(user.email)
+ end
+ end
- find('.modal-footer button', text: 'Unblock').click
+ context 'when deactivating/re-activating a user' do
+ it 'shows confirmation and allows deactivating and re-activating', :js do
+ expect(page).to have_content(user.email)
- wait_for_requests
+ click_action_in_user_dropdown(user.id, 'Deactivate')
- expect(page).to have_content('Successfully unblocked')
- expect(page).not_to have_content(user.email)
- end
- end
+ expect(page).to have_content('Deactivate user')
+ expect(page).to have_content('Deactivating a user has the following effects')
+ expect(page).to have_content('The user will be logged out')
+ expect(page).to have_content('Personal projects, group and user history will be left intact')
- context 'when deactivating/re-activating a user' do
- it 'shows confirmation and allows deactivating and re-activating', :js do
- expect(page).to have_content(user.email)
+ find('.modal-footer button', text: 'Deactivate').click
- click_action_in_user_dropdown(user.id, 'Deactivate')
+ wait_for_requests
- expect(page).to have_content('Deactivate user')
- expect(page).to have_content('Deactivating a user has the following effects')
- expect(page).to have_content('The user will be logged out')
- expect(page).to have_content('Personal projects, group and user history will be left intact')
+ expect(page).to have_content('Successfully deactivated')
+ expect(page).not_to have_content(user.email)
- find('.modal-footer button', text: 'Deactivate').click
+ click_link 'Deactivated'
- wait_for_requests
+ wait_for_requests
- expect(page).to have_content('Successfully deactivated')
- expect(page).not_to have_content(user.email)
+ expect(page).to have_content(user.email)
- click_link 'Deactivated'
+ click_action_in_user_dropdown(user.id, 'Activate')
- wait_for_requests
+ expect(page).to have_content('Activate user')
+ expect(page).to have_content('You can always deactivate their account again if needed.')
- expect(page).to have_content(user.email)
+ find('.modal-footer button', text: 'Activate').click
- click_action_in_user_dropdown(user.id, 'Activate')
+ wait_for_requests
- expect(page).to have_content('Activate user')
- expect(page).to have_content('You can always deactivate their account again if needed.')
+ expect(page).to have_content('Successfully activated')
+ expect(page).not_to have_content(user.email)
+ end
+ end
- find('.modal-footer button', text: 'Activate').click
+ describe 'internal users' do
+ context 'when showing a `Ghost User`' do
+ let_it_be(:ghost_user) { create(:user, :ghost) }
- wait_for_requests
+ it 'does not render actions dropdown' do
+ expect(page).not_to have_css("[data-testid='user-actions-#{ghost_user.id}'] [data-testid='dropdown-toggle']")
+ end
+ end
+
+ context 'when showing a `Bot User`' do
+ let_it_be(:bot_user) { create(:user, user_type: :alert_bot) }
- expect(page).to have_content('Successfully activated')
- expect(page).not_to have_content(user.email)
- end
+ it 'does not render actions dropdown' do
+ expect(page).not_to have_css("[data-testid='user-actions-#{bot_user.id}'] [data-testid='dropdown-toggle']")
end
end
end
+
+ context 'user group count', :js do
+ before do
+ group = create(:group)
+ group.add_developer(current_user)
+ project = create(:project, group: create(:group))
+ project.add_reporter(current_user)
+ end
+
+ it 'displays count of the users authorized groups' do
+ wait_for_requests
+
+ expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
+ end
+ end
end
describe 'GET /admin/users/new' do
@@ -548,32 +572,6 @@ RSpec.describe 'Admin::Users' do
end
end
- # TODO: Move to main GET /admin/users block once feature flag is removed. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/290737
- context 'with vue_admin_users feature flag enabled', :js do
- before do
- stub_feature_flags(vue_admin_users: true)
- end
-
- describe 'GET /admin/users' do
- context 'user group count', :js do
- before do
- group = create(:group)
- group.add_developer(current_user)
- project = create(:project, group: create(:group))
- project.add_reporter(current_user)
- end
-
- it 'displays count of the users authorized groups' do
- visit admin_users_path
-
- wait_for_requests
-
- expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
- end
- end
- end
- end
-
def click_user_dropdown_toggle(user_id)
page.within("[data-testid='user-actions-#{user_id}']") do
find("[data-testid='dropdown-toggle']").click
diff --git a/spec/features/alert_management/alert_management_list_spec.rb b/spec/features/alert_management/alert_management_list_spec.rb
index 44ed2f3d60c..aeaadacb38d 100644
--- a/spec/features/alert_management/alert_management_list_spec.rb
+++ b/spec/features/alert_management/alert_management_list_spec.rb
@@ -55,4 +55,28 @@ RSpec.describe 'Alert Management index', :js do
it_behaves_like 'alert page with title, filtered search, and table'
end
end
+
+ describe 'managed_alerts_deprecation feature flag' do
+ subject { page }
+
+ before do
+ stub_feature_flags(managed_alerts_deprecation: feature_flag_value)
+ sign_in(developer)
+
+ visit project_alert_management_index_path(project)
+ wait_for_requests
+ end
+
+ context 'feature flag on' do
+ let(:feature_flag_value) { true }
+
+ it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: true) }
+ end
+
+ context 'feature flag off' do
+ let(:feature_flag_value) { false }
+
+ it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: false) }
+ end
+ end
end
diff --git a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
index 6675abd6b42..60f2f776595 100644
--- a/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
+++ b/spec/features/alerts_settings/user_views_alerts_settings_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'Alert integrations settings form', :js do
it 'shows the alerts setting form title' do
page.within('#js-alert-management-settings') do
- expect(find('h4')).to have_content('Alert integrations')
+ expect(find('h4')).to have_content('Alerts')
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 5d9bb8d8087..02bb7574fb0 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -147,14 +147,23 @@ RSpec.describe 'Project issue boards', :js do
end
it 'infinite scrolls list' do
- create_list(:labeled_issue, 50, project: project, labels: [planning])
+ create_list(:labeled_issue, 30, project: project, labels: [planning])
visit_project_board_path_without_query_limit(project, board)
page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('58')
+ expect(page.find('.board-header')).to have_content('38')
+ expect(page).to have_selector('.board-card', count: 10)
+ expect(page).to have_content('Showing 10 of 38 issues')
+
+ find('.board .board-list')
+
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ end
+
expect(page).to have_selector('.board-card', count: 20)
- expect(page).to have_content('Showing 20 of 58 issues')
+ expect(page).to have_content('Showing 20 of 38 issues')
find('.board .board-list')
@@ -162,8 +171,8 @@ RSpec.describe 'Project issue boards', :js do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
- expect(page).to have_selector('.board-card', count: 40)
- expect(page).to have_content('Showing 40 of 58 issues')
+ expect(page).to have_selector('.board-card', count: 30)
+ expect(page).to have_content('Showing 30 of 38 issues')
find('.board .board-list')
@@ -171,7 +180,7 @@ RSpec.describe 'Project issue boards', :js do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
- expect(page).to have_selector('.board-card', count: 58)
+ expect(page).to have_selector('.board-card', count: 38)
expect(page).to have_content('Showing all issues')
end
end
@@ -464,7 +473,7 @@ RSpec.describe 'Project issue boards', :js do
end
it 'infinite scrolls list with label filter' do
- create_list(:labeled_issue, 50, project: project, labels: [planning, testing])
+ create_list(:labeled_issue, 30, project: project, labels: [planning, testing])
set_filter("label", testing.title)
click_filter_link(testing.title)
@@ -475,9 +484,18 @@ RSpec.describe 'Project issue boards', :js do
wait_for_requests
page.within(find('.board:nth-child(2)')) do
- expect(page.find('.board-header')).to have_content('51')
+ expect(page.find('.board-header')).to have_content('31')
+ expect(page).to have_selector('.board-card', count: 10)
+ expect(page).to have_content('Showing 10 of 31 issues')
+
+ find('.board .board-list')
+
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
+ end
+
expect(page).to have_selector('.board-card', count: 20)
- expect(page).to have_content('Showing 20 of 51 issues')
+ expect(page).to have_content('Showing 20 of 31 issues')
find('.board .board-list')
@@ -485,15 +503,15 @@ RSpec.describe 'Project issue boards', :js do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
- expect(page).to have_selector('.board-card', count: 40)
- expect(page).to have_content('Showing 40 of 51 issues')
+ expect(page).to have_selector('.board-card', count: 30)
+ expect(page).to have_content('Showing 30 of 31 issues')
find('.board .board-list')
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
- expect(page).to have_selector('.board-card', count: 51)
+ expect(page).to have_selector('.board-card', count: 31)
expect(page).to have_content('Showing all issues')
end
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 87d29eed68d..57f2bf26752 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -157,7 +157,7 @@ RSpec.describe 'Issue Boards', :js do
end
it 'moves to bottom of another list' do
- drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020)
+ drag(list_from_index: 1, list_to_index: 2, to_index: 3, duration: 1020)
wait_for_requests
diff --git a/spec/features/boards/multi_select_spec.rb b/spec/features/boards/multi_select_spec.rb
index ca322355b8f..057464326fa 100644
--- a/spec/features/boards/multi_select_spec.rb
+++ b/spec/features/boards/multi_select_spec.rb
@@ -41,9 +41,9 @@ RSpec.describe 'Multi Select Issue', :js do
before do
project.add_maintainer(user)
- # multi-drag disabled with feature flag for now
+ # Multi select drag&drop support is temporarily disabled
# https://gitlab.com/gitlab-org/gitlab/-/issues/289797
- stub_feature_flags(graphql_board_lists: false)
+ stub_feature_flags(graphql_board_lists: false, board_multi_select: project)
sign_in(user)
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 129d03d17f3..e055e8092d4 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -120,6 +120,32 @@ RSpec.describe 'Issue Boards new issue', :js do
expect(page).to have_content 'Label 1'
end
end
+
+ it 'allows creating an issue in newly created list' do
+ click_button 'Create list'
+ wait_for_all_requests
+
+ click_button 'Select a label'
+ find('label', text: label.title).click
+ click_button 'Add to board'
+
+ wait_for_all_requests
+
+ page.within('.board:nth-child(2)') do
+ click_button('New issue')
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('new issue')
+ click_button 'Create issue'
+ end
+
+ wait_for_all_requests
+
+ page.within('.board-card') do
+ expect(page).to have_content 'new issue'
+ end
+ end
+ end
end
context 'unauthorized user' do
@@ -128,8 +154,8 @@ RSpec.describe 'Issue Boards new issue', :js do
wait_for_requests
end
- it 'displays new issue button in open list' do
- expect(first('.board')).to have_button('New issue', count: 1)
+ it 'does not display new issue button in open list' do
+ expect(first('.board')).not_to have_button('New issue')
end
it 'does not display new issue button in label list' do
@@ -166,8 +192,8 @@ RSpec.describe 'Issue Boards new issue', :js do
context 'when backlog list already exists' do
let_it_be(:backlog_list) { create(:backlog_list, board: group_board) }
- it 'displays new issue button in open list' do
- expect(first('.board')).to have_button('New issue', count: 1)
+ it 'does not display new issue button in open list' do
+ expect(first('.board')).not_to have_button('New issue')
end
it 'does not display new issue button in label list' do
diff --git a/spec/features/boards/sidebar_assignee_spec.rb b/spec/features/boards/sidebar_assignee_spec.rb
index d6adefea6e3..63553cec89b 100644
--- a/spec/features/boards/sidebar_assignee_spec.rb
+++ b/spec/features/boards/sidebar_assignee_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project issue boards sidebar assignee', :js do
+RSpec.describe 'Project issue boards sidebar assignee', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332230' do
include BoardHelpers
let_it_be(:user) { create(:user) }
diff --git a/spec/features/boards/sidebar_milestones_spec.rb b/spec/features/boards/sidebar_milestones_spec.rb
index 54182781a30..be7435263b1 100644
--- a/spec/features/boards/sidebar_milestones_spec.rb
+++ b/spec/features/boards/sidebar_milestones_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
wait_for_requests
- page.within('.value') do
+ page.within('[data-testid="select-milestone"]') do
expect(page).to have_content(milestone.title)
end
end
@@ -56,7 +56,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
wait_for_requests
- page.within('.value') do
+ page.within('[data-testid="select-milestone"]') do
expect(page).not_to have_content(milestone.title)
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 1281d890ef7..21da92c9f43 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -15,10 +15,9 @@ RSpec.describe 'Contributions Calendar', :js do
issue_title = 'Bug in old browser'
issue_params = { title: issue_title }
- def get_cell_color_selector(contributions)
- activity_colors = ["#ededed", "rgb(172, 213, 242)", "rgb(127, 168, 201)", "rgb(82, 123, 160)", "rgb(37, 78, 119)"]
+ def get_cell_level_selector(contributions)
# We currently don't actually test the cases with contributions >= 20
- activity_colors_index =
+ activity_level_index =
if contributions > 0 && contributions < 10
1
elsif contributions >= 10 && contributions < 20
@@ -31,7 +30,7 @@ RSpec.describe 'Contributions Calendar', :js do
0
end
- ".user-contrib-cell[fill='#{activity_colors[activity_colors_index]}']"
+ ".user-contrib-cell:not(.contrib-legend)[data-level='#{activity_level_index}']"
end
def get_cell_date_selector(contributions, date)
@@ -42,7 +41,7 @@ RSpec.describe 'Contributions Calendar', :js do
"#{contributions} #{'contribution'.pluralize(contributions)}"
end
- "#{get_cell_color_selector(contributions)}[title='#{contribution_text}<br /><span class=\"gl-text-gray-300\">#{date}</span>']"
+ "#{get_cell_level_selector(contributions)}[title='#{contribution_text}<br /><span class=\"gl-text-gray-300\">#{date}</span>']"
end
def push_code_contribution
@@ -137,7 +136,7 @@ RSpec.describe 'Contributions Calendar', :js do
include_context 'visit user page'
it 'displays calendar activity square for 1 contribution', :sidekiq_might_not_need_inline do
- expect(find('#js-overview')).to have_selector(get_cell_color_selector(contribution_count), count: 1)
+ expect(find('#js-overview')).to have_selector(get_cell_level_selector(contribution_count), count: 1)
today = Date.today.strftime(date_format)
expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
@@ -187,7 +186,7 @@ RSpec.describe 'Contributions Calendar', :js do
include_context 'visit user page'
it 'displays calendar activity squares for both days', :sidekiq_might_not_need_inline do
- expect(find('#js-overview')).to have_selector(get_cell_color_selector(1), count: 2)
+ expect(find('#js-overview')).to have_selector(get_cell_level_selector(1), count: 2)
end
it 'displays calendar activity square for yesterday', :sidekiq_might_not_need_inline do
diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb
index 84a18a45d35..cba8aaef1ef 100644
--- a/spec/features/clusters/cluster_detail_page_spec.rb
+++ b/spec/features/clusters/cluster_detail_page_spec.rb
@@ -31,30 +31,6 @@ RSpec.describe 'Clusterable > Show page' do
expect(page).to have_content('Kubernetes cluster was successfully updated.')
end
- context 'when there is a cluster with ingress and external ip', :js do
- before do
- cluster.create_application_ingress!(external_ip: '192.168.1.100')
-
- visit cluster_path
- end
-
- it 'shows help text with the domain as an alternative to custom domain', :js do
- within '.js-cluster-details-form' do
- expect(find(cluster_ingress_help_text_selector).text).to include('192.168.1.100')
- end
- end
- end
-
- context 'when there is no ingress' do
- it 'alternative to custom domain is not shown' do
- visit cluster_path
-
- within '.js-cluster-details-form' do
- expect(page).not_to have_selector(cluster_ingress_help_text_selector)
- end
- end
- end
-
it 'does not show the environments tab' do
visit cluster_path
diff --git a/spec/features/clusters/cluster_health_dashboard_spec.rb b/spec/features/clusters/cluster_health_dashboard_spec.rb
index 862f34768c4..20c07f4d6ac 100644
--- a/spec/features/clusters/cluster_health_dashboard_spec.rb
+++ b/spec/features/clusters/cluster_health_dashboard_spec.rb
@@ -27,8 +27,8 @@ RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory
expect(page).to have_css('.cluster-health-graphs')
end
- context 'no prometheus installed' do
- it 'shows install prometheus message' do
+ context 'no prometheus available' do
+ it 'shows enable Prometheus message' do
visit cluster_path
click_link 'Health'
@@ -37,9 +37,9 @@ RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory
end
end
- context 'when there is cluster with installed prometheus' do
+ context 'when there is cluster with enabled prometheus' do
before do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, enabled: true, cluster: cluster)
stub_kubeclient_discover(cluster.platform.api_url)
end
@@ -82,12 +82,12 @@ RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory
def stub_empty_response
stub_prometheus_request(/prometheus-prometheus-server/, status: 204, body: {})
- stub_prometheus_request(/prometheus\/api\/v1/, status: 204, body: {})
+ stub_prometheus_request(%r{prometheus/api/v1}, status: 204, body: {})
end
def stub_connected
stub_prometheus_request(/prometheus-prometheus-server/, body: prometheus_values_body)
- stub_prometheus_request(/prometheus\/api\/v1/, body: prometheus_values_body)
+ stub_prometheus_request(%r{prometheus/api/v1}, body: prometheus_values_body)
end
end
end
diff --git a/spec/features/clusters/installing_applications_shared_examples.rb b/spec/features/clusters/installing_applications_shared_examples.rb
deleted file mode 100644
index c422aa2be72..00000000000
--- a/spec/features/clusters/installing_applications_shared_examples.rb
+++ /dev/null
@@ -1,252 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples "installing applications for a cluster" do
- before do
- # Reduce interval from 10 seconds which is too long for an automated test
- stub_const("#{Clusters::ClustersController}::STATUS_POLLING_INTERVAL", 500)
-
- visit cluster_path
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, *cluster_factory_args) }
-
- it 'user is unable to install applications' do
- expect(page).not_to have_text('Helm')
- expect(page).not_to have_text('Install')
- end
- end
-
- context 'when cluster is created' do
- let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) }
-
- before do
- page.within('.js-edit-cluster-form') do
- click_link 'Applications'
- end
- end
-
- it 'user can install applications' do
- wait_for_requests
-
- application_row = '.js-cluster-application-row-ingress'
-
- page.within(application_row) do
- expect(page).not_to have_css('.js-cluster-application-install-button[disabled]')
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
- end
- end
-
- it 'does not show the Helm application' do
- expect(page).not_to have_selector(:css, '.js-cluster-application-row-helm')
- end
-
- context 'when user installs Knative' do
- context 'on an abac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, *cluster_factory_args) }
-
- it 'shows info block and not be installable' do
- page.within('.js-cluster-application-row-knative') do
- expect(page).to have_css('.rbac-notice')
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- end
- end
- end
-
- context 'on an rbac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, *cluster_factory_args) }
-
- it 'does not show callout block and be installable' do
- page.within('.js-cluster-application-row-knative') do
- expect(page).not_to have_css('p', text: 'You must have an RBAC-enabled cluster', visible: :all)
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
- end
- end
-
- describe 'when user clicks install button' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- page.within('.js-cluster-application-row-knative') do
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
-
- page.find('.js-knative-domainname').set("domain.example.org")
-
- click_button 'Install'
-
- wait_for_requests
-
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_knative.make_installing!
- Clusters::Cluster.last.application_knative.make_installed!
- Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1')
- end
- end
-
- it 'shows status transition' do
- page.within('.js-cluster-application-row-knative') do
- expect(page).to have_field('Knative Domain Name:', with: 'domain.example.org')
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster')
- expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes'
- end
-
- it 'can then update the domain' do
- page.within('.js-cluster-application-row-knative') do
- expect(ClusterPatchAppWorker).to receive(:perform_async)
-
- expect(page).to have_field('Knative Domain Name:', with: 'domain.example.org')
-
- page.find('.js-knative-domainname').set("new.domain.example.org")
-
- click_button 'Save changes'
-
- wait_for_requests
-
- expect(page).to have_field('Knative Domain Name:', with: 'new.domain.example.org')
- end
- end
- end
- end
- end
-
- context 'when user installs Cert Manager' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
- end
-
- it 'shows status transition' do
- page.within('.js-cluster-application-row-cert_manager') do
- click_button 'Install'
- wait_for_requests
-
- expect(page).to have_field('Issuer Email', with: cluster.user.email)
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_cert_manager.make_installing!
-
- expect(page).to have_field('Issuer Email', with: cluster.user.email)
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_cert_manager.make_installed!
-
- expect(page).to have_field('Issuer Email', with: cluster.user.email)
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Cert-Manager was successfully installed on your Kubernetes cluster')
- end
-
- it 'installs with custom email' do
- custom_email = 'new_email@example.org'
-
- page.within('.js-cluster-application-row-cert_manager') do
- # Wait for the polling to finish
- wait_for_requests
-
- page.find('.js-email').set(custom_email)
- click_button 'Install'
- wait_for_requests
-
- expect(page).to have_field('Issuer Email', with: custom_email)
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_cert_manager.make_installing!
-
- expect(page).to have_field('Issuer Email', with: custom_email)
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_cert_manager.make_installed!
-
- expect(page).to have_field('Issuer Email', with: custom_email)
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
- end
- end
-
- context 'when user installs Elastic Stack' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
-
- page.within('.js-cluster-application-row-elastic_stack') do
- click_button 'Install'
- end
-
- wait_for_requests
- end
-
- it 'shows status transition' do
- page.within('.js-cluster-application-row-elastic_stack') do
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_elastic_stack.make_installing!
-
- expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_elastic_stack.make_installed!
-
- expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
- end
-
- expect(page).to have_content('Elastic Stack was successfully installed on your Kubernetes cluster')
- end
- end
-
- context 'when user installs Ingress' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
- allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
-
- page.within('.js-cluster-application-row-ingress') do
- expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
- page.find(:css, '.js-cluster-application-install-button').click
-
- wait_for_requests
- end
- end
-
- it 'shows the status transition' do
- page.within('.js-cluster-application-row-ingress') do
- # FE sends request and gets the response, then the buttons is "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- Clusters::Cluster.last.application_ingress.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
-
- # The application becomes installed but we keep waiting for external IP address
- Clusters::Cluster.last.application_ingress.make_installed!
-
- expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installed')
- expect(page).to have_selector('.js-no-endpoint-message')
- expect(page).to have_selector('.js-ingress-ip-loading-icon')
-
- # We receive the external IP address and display
- Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
-
- expect(page).not_to have_css('button', exact_text: 'Install', visible: :all)
- expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all)
- expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
- expect(page).not_to have_css('p', text: 'The endpoint is in the process of being assigned', visible: :all)
- expect(page.find('.js-endpoint').value).to eq('192.168.1.100')
- end
-
- expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
- end
- end
- end
-end
-
-RSpec.shared_examples "installing applications on a cluster" do
- it_behaves_like "installing applications for a cluster", false
- it_behaves_like "installing applications for a cluster", true
-end
diff --git a/spec/features/contextual_sidebar_spec.rb b/spec/features/contextual_sidebar_spec.rb
index 8ea1ebac6b7..39881a28b11 100644
--- a/spec/features/contextual_sidebar_spec.rb
+++ b/spec/features/contextual_sidebar_spec.rb
@@ -3,17 +3,17 @@
require 'spec_helper'
RSpec.describe 'Contextual sidebar', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
+
+ let(:user) { project.owner }
before do
- project.add_maintainer(user)
sign_in(user)
visit project_path(project)
end
- it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded' do
+ it 'shows flyout navs when collapsed or expanded apart from on the active item when expanded', :aggregate_failures do
expect(page).not_to have_selector('.js-sidebar-collapsed')
find('.rspec-link-pipelines').hover
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 233a93c2054..8c7564535b5 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
it 'shows active stage with empty message' do
- expect(page).to have_selector('.stage-nav-item.active', text: 'Issue')
+ expect(page).to have_selector('.gl-path-active-item-indigo', text: 'Issue')
expect(page).to have_content("We don't have enough data to show this stage.")
end
end
@@ -171,7 +171,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
def click_stage(stage_name)
- find('.stage-nav li', text: stage_name).click
+ find('.gl-path-nav-list-item', text: stage_name).click
wait_for_requests
end
end
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index a1fb0beda70..aa767d75c00 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -2,6 +2,9 @@
require 'spec_helper'
+# TODO: This entire spec file can be deleted once the combined_menu feature is fully rolled
+# out and the flag is removed, because it will then be irrelevant (there will be no more tabs).
+# Feature flag removal issue: https://gitlab.com/gitlab-org/gitlab/-/issues/324086
RSpec.describe 'Dashboard Active Tab', :js do
shared_examples 'combined_menu: feature flag examples' do
before do
@@ -10,8 +13,6 @@ RSpec.describe 'Dashboard Active Tab', :js do
shared_examples 'page has active tab' do |title|
it "#{title} tab" do
- pending_on_combined_menu_flag
-
subject
expect(page).to have_selector('.navbar-sub-nav li.active', count: 1)
@@ -32,27 +33,11 @@ RSpec.describe 'Dashboard Active Tab', :js do
end
end
- context 'with combined_menu: feature flag on' do
- let(:needs_rewrite_for_combined_menu_flag_on) { true }
-
- before do
- stub_feature_flags(combined_menu: true)
- end
-
- it_behaves_like 'combined_menu: feature flag examples'
- end
-
context 'with combined_menu feature flag off' do
- let(:needs_rewrite_for_combined_menu_flag_on) { false }
-
before do
stub_feature_flags(combined_menu: false)
end
it_behaves_like 'combined_menu: feature flag examples'
end
-
- def pending_on_combined_menu_flag
- pending 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587' if needs_rewrite_for_combined_menu_flag_on
- end
end
diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
index 0620f819332..3dd993b4bb5 100644
--- a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
+++ b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'The group dashboard' do
include ExternalAuthorizationServiceHelpers
+ include Spec::Support::Helpers::Features::TopNavSpecHelpers
let(:user) { create(:user) }
@@ -14,11 +15,11 @@ RSpec.describe 'The group dashboard' do
describe 'The top navigation' do
it 'has all the expected links' do
- pending_on_combined_menu_flag
-
visit dashboard_groups_path
- within('.navbar') do
+ open_top_nav
+
+ within_top_nav do
expect(page).to have_button('Projects')
expect(page).to have_button('Groups')
expect(page).to have_link('Activity')
@@ -28,12 +29,12 @@ RSpec.describe 'The group dashboard' do
end
it 'hides some links when an external authorization service is enabled' do
- pending_on_combined_menu_flag
-
enable_external_authorization_service_check
visit dashboard_groups_path
- within('.navbar') do
+ open_top_nav
+
+ within_top_nav do
expect(page).to have_button('Projects')
expect(page).to have_button('Groups')
expect(page).not_to have_link('Activity')
@@ -44,7 +45,7 @@ RSpec.describe 'The group dashboard' do
end
end
- context 'with combined_menu: feature flag on' do
+ context 'with combined_menu feature flag on', :js do
let(:needs_rewrite_for_combined_menu_flag_on) { true }
before do
@@ -63,8 +64,4 @@ RSpec.describe 'The group dashboard' do
it_behaves_like 'combined_menu: feature flag examples'
end
-
- def pending_on_combined_menu_flag
- pending 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587' if needs_rewrite_for_combined_menu_flag_on
- end
end
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
index bc6f449edc5..02cbdc7c777 100644
--- a/spec/features/dashboard/group_spec.rb
+++ b/spec/features/dashboard/group_spec.rb
@@ -16,6 +16,8 @@ RSpec.describe 'Dashboard Group' do
it 'creates new group', :js do
visit dashboard_groups_path
find('[data-testid="new-group-button"]').click
+ click_link 'Create group'
+
new_name = 'Samurai'
fill_in 'group_name', with: new_name
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 5f60832dbc9..7439bfd334b 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -15,8 +15,6 @@ RSpec.describe 'Dashboard shortcuts', :js do
end
it 'navigate to tabs' do
- pending_on_combined_menu_flag
-
find('body').send_keys([:shift, 'I'])
check_page_title('Issues')
@@ -40,6 +38,10 @@ RSpec.describe 'Dashboard shortcuts', :js do
find('body').send_keys([:shift, 'A'])
check_page_title('Activity')
+
+ find('body').send_keys([:shift, 'L'])
+
+ check_page_title('Milestones')
end
end
@@ -49,8 +51,6 @@ RSpec.describe 'Dashboard shortcuts', :js do
end
it 'navigate to tabs' do
- pending_on_combined_menu_flag
-
find('body').send_keys([:shift, 'G'])
find('.nothing-here-block')
@@ -73,9 +73,7 @@ RSpec.describe 'Dashboard shortcuts', :js do
end
end
- context 'with combined_menu: feature flag on' do
- let(:needs_rewrite_for_combined_menu_flag_on) { true }
-
+ context 'with combined_menu feature flag on' do
before do
stub_feature_flags(combined_menu: true)
end
@@ -84,16 +82,10 @@ RSpec.describe 'Dashboard shortcuts', :js do
end
context 'with combined_menu feature flag off' do
- let(:needs_rewrite_for_combined_menu_flag_on) { false }
-
before do
stub_feature_flags(combined_menu: false)
end
it_behaves_like 'combined_menu: feature flag examples'
end
-
- def pending_on_combined_menu_flag
- pending 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587' if needs_rewrite_for_combined_menu_flag_on
- end
end
diff --git a/spec/features/frequently_visited_projects_and_groups_spec.rb b/spec/features/frequently_visited_projects_and_groups_spec.rb
index 9110c7ad65a..5ea42ce39e3 100644
--- a/spec/features/frequently_visited_projects_and_groups_spec.rb
+++ b/spec/features/frequently_visited_projects_and_groups_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Frequently visited items', :js do
+ include Spec::Support::Helpers::Features::TopNavSpecHelpers
+
let_it_be(:user) { create(:user) }
shared_examples 'combined_menu: feature flag examples' do
@@ -14,9 +16,8 @@ RSpec.describe 'Frequently visited items', :js do
let_it_be(:project) { create(:project, :public) }
it 'increments localStorage counter when visiting the project' do
- pending_on_combined_menu_flag
-
visit project_path(project)
+ open_top_nav_projects
frequent_projects = nil
@@ -34,9 +35,8 @@ RSpec.describe 'Frequently visited items', :js do
let_it_be(:group) { create(:group, :public) }
it 'increments localStorage counter when visiting the group' do
- pending_on_combined_menu_flag
-
visit group_path(group)
+ open_top_nav_groups
frequent_groups = nil
@@ -51,7 +51,7 @@ RSpec.describe 'Frequently visited items', :js do
end
end
- context 'with combined_menu: feature flag on' do
+ context 'with combined_menu feature flag on' do
let(:needs_rewrite_for_combined_menu_flag_on) { true }
before do
@@ -70,8 +70,4 @@ RSpec.describe 'Frequently visited items', :js do
it_behaves_like 'combined_menu: feature flag examples'
end
-
- def pending_on_combined_menu_flag
- pending 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587' if needs_rewrite_for_combined_menu_flag_on
- end
end
diff --git a/spec/features/groups/clusters/applications_spec.rb b/spec/features/groups/clusters/applications_spec.rb
deleted file mode 100644
index 324ef24efc4..00000000000
--- a/spec/features/groups/clusters/applications_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
-
-RSpec.describe 'Group-level Cluster Applications', :js do
- include GoogleApi::CloudPlatformHelpers
-
- let(:group) { create(:group) }
- let(:user) { create(:user) }
-
- before do
- group.add_maintainer(user)
- sign_in(user)
- end
-
- describe 'Installing applications' do
- include_examples "installing applications on a cluster" do
- let(:cluster_path) { group_cluster_path(group, cluster) }
- let(:cluster_factory_args) { [:group, groups: [group]] }
- end
- end
-end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 00ad1006037..161a8a7a203 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -153,6 +153,26 @@ RSpec.describe 'Edit group settings' do
end
end
+ describe 'prevent sharing outside group hierarchy setting' do
+ it 'updates the setting' do
+ visit edit_group_path(group)
+
+ check 'group_prevent_sharing_groups_outside_hierarchy'
+
+ expect { save_permissions_group }.to change {
+ group.reload.namespace_settings.prevent_sharing_groups_outside_hierarchy
+ }.to(true)
+ end
+
+ it 'is not present for a subgroup' do
+ subgroup = create(:group, parent: group)
+ visit edit_group_path(subgroup)
+
+ expect(page).to have_text "Permissions"
+ expect(page).not_to have_selector('#group_prevent_sharing_groups_outside_hierarchy')
+ end
+ end
+
def update_path(new_group_path)
visit edit_group_path(group)
diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb
index 73de49101ea..563c8f429f8 100644
--- a/spec/features/groups/import_export/connect_instance_spec.rb
+++ b/spec/features/groups/import_export/connect_instance_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
visit new_group_path
- find('#import-group-tab').click
+ click_link 'Import group'
end
context 'when the user provides valid credentials' do
@@ -24,7 +24,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do
pat = 'demo-pat'
stub_path = 'stub-group'
total = 37
- stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=40&search=" % { url: source_url }).to_return(
+ stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=50&search=" % { url: source_url }).to_return(
body: [{
id: 2595438,
web_url: 'https://gitlab.com/groups/auto-breakfast',
diff --git a/spec/features/groups/import_export/import_file_spec.rb b/spec/features/groups/import_export/import_file_spec.rb
index 7018f3b1086..08295a3392a 100644
--- a/spec/features/groups/import_export/import_file_spec.rb
+++ b/spec/features/groups/import_export/import_file_spec.rb
@@ -28,9 +28,9 @@ RSpec.describe 'Import/Export - Group Import', :js do
group_name = 'Test Group Import'
visit new_group_path
+ click_link 'Import group'
- fill_in :group_name, with: group_name
- find('#import-group-tab').click
+ fill_in :import_group_name, with: group_name
expect(page).to have_content 'Import group from file'
attach_file(file) do
@@ -51,9 +51,9 @@ RSpec.describe 'Import/Export - Group Import', :js do
context 'when modifying the pre-filled path' do
it 'successfully imports the group' do
visit new_group_path
+ click_link 'Import group'
- fill_in :group_name, with: 'Test Group Import'
- find('#import-group-tab').click
+ fill_in :import_group_name, with: 'Test Group Import'
fill_in :import_group_path, with: 'custom-path'
attach_file(file) do
@@ -74,7 +74,7 @@ RSpec.describe 'Import/Export - Group Import', :js do
it 'suggests a unique path' do
visit new_group_path
- find('#import-group-tab').click
+ click_link 'Import group'
fill_in :import_group_path, with: 'test-group-import'
expect(page).to have_content 'Group path is already taken. Suggestions: test-group-import1'
@@ -87,9 +87,9 @@ RSpec.describe 'Import/Export - Group Import', :js do
it 'displays an error' do
visit new_group_path
+ click_link 'Import group'
- fill_in :group_name, with: 'Test Group Import'
- find('#import-group-tab').click
+ fill_in :import_group_name, with: 'Test Group Import'
attach_file(file) do
find('.js-filepicker-button').click
end
diff --git a/spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb
new file mode 100644
index 00000000000..7703268af39
--- /dev/null
+++ b/spec/features/groups/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User activates the group-level Mattermost Slash Command integration', :js do
+ include_context 'group integration activation'
+
+ before do
+ stub_mattermost_setting(enabled: true)
+ visit_group_integration('Mattermost slash commands')
+ end
+
+ let(:edit_path) { edit_group_settings_integration_path(group, :mattermost_slash_commands) }
+
+ include_examples 'user activates the Mattermost Slash Command integration'
+end
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index 40cd54c1e33..2dfcd941b4f 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -143,6 +143,82 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
end
end
+ describe 'group search results' do
+ let_it_be(:group, refind: true) { create(:group) }
+ let_it_be(:group_within_hierarchy) { create(:group, parent: group) }
+ let_it_be(:group_outside_hierarchy) { create(:group) }
+
+ before_all do
+ group.add_owner(user)
+ group_within_hierarchy.add_owner(user)
+ group_outside_hierarchy.add_owner(user)
+ end
+
+ context 'when sharing with groups outside the hierarchy is enabled' do
+ context 'when the invite members group modal is disabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'shows groups within and outside the hierarchy in search results' do
+ visit group_group_members_path(group)
+
+ click_on 'Invite group'
+ click_on 'Search for a group'
+
+ expect(page).to have_text group_within_hierarchy.name
+ expect(page).to have_text group_outside_hierarchy.name
+ end
+ end
+
+ context 'when the invite members group modal is enabled' do
+ it 'shows groups within and outside the hierarchy in search results' do
+ visit group_group_members_path(group)
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+
+ expect(page).to have_text group_within_hierarchy.name
+ expect(page).to have_text group_outside_hierarchy.name
+ end
+ end
+ end
+
+ context 'when sharing with groups outside the hierarchy is disabled' do
+ before do
+ group.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
+ end
+
+ context 'when the invite members group modal is disabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it 'shows only groups within the hierarchy in search results' do
+ visit group_group_members_path(group)
+
+ click_on 'Invite group'
+ click_on 'Search for a group'
+
+ expect(page).to have_text group_within_hierarchy.name
+ expect(page).not_to have_text group_outside_hierarchy.name
+ end
+ end
+
+ context 'when the invite members group modal is enabled' do
+ it 'shows only groups within the hierarchy in search results' do
+ visit group_group_members_path(group)
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+
+ expect(page).to have_text group_within_hierarchy.name
+ expect(page).not_to have_text group_outside_hierarchy.name
+ end
+ end
+ end
+ end
+
def add_group(id, role)
page.click_link 'Invite group'
page.within ".invite-group-form" do
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index c5e6479ec51..ee18298e894 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -3,20 +3,19 @@
require 'spec_helper'
RSpec.describe 'Groups > Members > Manage members' do
- include Select2Helper
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
- let(:user1) { create(:user, name: 'John Doe') }
- let(:user2) { create(:user, name: 'Mary Jane') }
- let(:group) { create(:group) }
+ let_it_be(:user1) { create(:user, name: 'John Doe') }
+ let_it_be(:user2) { create(:user, name: 'Mary Jane') }
+ let_it_be(:group) { create(:group) }
before do
sign_in(user1)
end
shared_examples 'includes the correct Invite link' do |should_include, should_not_include|
- it 'includes either the form or the modal trigger' do
+ it 'includes either the form or the modal trigger', :aggregate_failures do
group.add_owner(user1)
visit group_group_members_path(group)
@@ -27,12 +26,12 @@ RSpec.describe 'Groups > Members > Manage members' do
end
shared_examples 'does not include either invite modal or either invite form' do
- it 'does not include either of the invite members or invite group modal buttons' do
+ it 'does not include either of the invite members or invite group modal buttons', :aggregate_failures do
expect(page).not_to have_selector '.js-invite-members-modal'
expect(page).not_to have_selector '.js-invite-group-modal'
end
- it 'does not include either of the invite users or invite group forms' do
+ it 'does not include either of the invite users or invite group forms', :aggregate_failures do
expect(page).not_to have_selector '.invite-users-form'
expect(page).not_to have_selector '.invite-group-form'
end
@@ -66,7 +65,7 @@ RSpec.describe 'Groups > Members > Manage members' do
end
end
- it 'add user to group', :js do
+ it 'add user to group', :js, :snowplow, :aggregate_failures do
group.add_owner(user1)
visit group_group_members_path(group)
@@ -77,6 +76,14 @@ RSpec.describe 'Groups > Members > Manage members' do
expect(page).to have_content(user2.name)
expect(page).to have_button('Reporter')
end
+
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: 'group-members-page',
+ property: 'existing_user',
+ user: user1
+ )
end
it 'do not disclose email addresses', :js do
@@ -143,11 +150,13 @@ RSpec.describe 'Groups > Members > Manage members' do
wait_for_requests
- expect(page).not_to have_content(user2.name)
- expect(group.users).not_to include(user2)
+ aggregate_failures do
+ expect(page).not_to have_content(user2.name)
+ expect(group.users).not_to include(user2)
+ end
end
- it 'add yourself to group when already an owner', :js do
+ it 'add yourself to group when already an owner', :js, :aggregate_failures do
group.add_owner(user1)
visit group_group_members_path(group)
@@ -160,7 +169,7 @@ RSpec.describe 'Groups > Members > Manage members' do
end
end
- it 'invite user to group', :js do
+ it 'invite user to group', :js, :snowplow do
group.add_owner(user1)
visit group_group_members_path(group)
@@ -170,14 +179,24 @@ RSpec.describe 'Groups > Members > Manage members' do
expect(page).to have_link 'Invited'
click_link 'Invited'
- page.within(members_table) do
- expect(page).to have_content('test@example.com')
- expect(page).to have_content('Invited')
- expect(page).to have_button('Reporter')
+ aggregate_failures do
+ page.within(members_table) do
+ expect(page).to have_content('test@example.com')
+ expect(page).to have_content('Invited')
+ expect(page).to have_button('Reporter')
+ end
+
+ expect_snowplow_event(
+ category: 'Members::InviteService',
+ action: 'create_member',
+ label: 'group-members-page',
+ property: 'net_new_user',
+ user: user1
+ )
end
end
- context 'as a guest', :js do
+ context 'when user is a guest' do
before do
group.add_guest(user1)
group.add_developer(user2)
@@ -187,7 +206,7 @@ RSpec.describe 'Groups > Members > Manage members' do
it_behaves_like 'does not include either invite modal or either invite form'
- it 'does not include a button on the members page list to manage or remove the existing member', :js do
+ it 'does not include a button on the members page list to manage or remove the existing member', :js, :aggregate_failures do
page.within(second_row) do
# Can not modify user2 role
expect(page).not_to have_button 'Developer'
@@ -198,7 +217,7 @@ RSpec.describe 'Groups > Members > Manage members' do
end
end
- context 'As a guest when the :invite_members_group_modal feature flag is disabled', :js do
+ context 'when user is a guest and the :invite_members_group_modal feature flag is disabled' do
before do
stub_feature_flags(invite_members_group_modal: false)
group.add_guest(user1)
@@ -209,7 +228,7 @@ RSpec.describe 'Groups > Members > Manage members' do
it_behaves_like 'does not include either invite modal or either invite form'
- it 'does not include a button on the members page list to manage or remove the existing member', :js do
+ it 'does not include a button on the members page list to manage or remove the existing member', :js, :aggregate_failures do
page.within(second_row) do
# Can not modify user2 role
expect(page).not_to have_button 'Developer'
diff --git a/spec/features/groups/members/tabs_spec.rb b/spec/features/groups/members/tabs_spec.rb
index 2f95e9fa6d3..2e9f332c0d6 100644
--- a/spec/features/groups/members/tabs_spec.rb
+++ b/spec/features/groups/members/tabs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Groups > Members > Tabs' do
+RSpec.describe 'Groups > Members > Tabs', :js do
using RSpec::Parameterized::TableSyntax
shared_examples 'active "Members" tab' do
@@ -56,7 +56,7 @@ RSpec.describe 'Groups > Members > Tabs' do
it_behaves_like 'active "Members" tab'
end
- context 'when searching "Invited"', :js do
+ context 'when searching "Invited"' do
before do
visit group_group_members_path(group)
@@ -86,7 +86,7 @@ RSpec.describe 'Groups > Members > Tabs' do
end
end
- context 'when using "Invited" pagination', :js do
+ context 'when using "Invited" pagination' do
before do
visit group_group_members_path(group)
diff --git a/spec/features/groups/milestones/gfm_autocomplete_spec.rb b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
index 85a14123294..1fec6091f1e 100644
--- a/spec/features/groups/milestones/gfm_autocomplete_spec.rb
+++ b/spec/features/groups/milestones/gfm_autocomplete_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe 'GFM autocomplete', :js do
fill_in 'Description', with: User.reference_prefix
wait_for_requests
expect(find_autocomplete_menu).to be_visible
+ expect_autocomplete_entry(user.name)
expect_autocomplete_entry(group.name)
fill_in 'Description', with: Label.reference_prefix
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index b46d4dae87a..70a19445c89 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe 'Group navbar' do
let(:structure) do
[
+ group_context_nav_item,
group_information_nav_item,
{
nav_item: _('Issues'),
@@ -37,6 +38,13 @@ RSpec.describe 'Group navbar' do
nil
end
+ let(:group_context_nav_item) do
+ {
+ nav_item: "#{group.name[0, 1].upcase} #{group.name}",
+ nav_sub_items: []
+ }
+ end
+
before do
insert_package_nav(_('Kubernetes'))
@@ -79,6 +87,10 @@ RSpec.describe 'Group navbar' do
end
context 'when feature flag :sidebar_refactor is disabled' do
+ let(:group_context_nav_item) do
+ nil
+ end
+
let(:group_information_nav_item) do
{
nav_item: _('Group overview'),
diff --git a/spec/features/groups/settings/user_searches_in_settings_spec.rb b/spec/features/groups/settings/user_searches_in_settings_spec.rb
index 6d7a3871bb1..a01514714dd 100644
--- a/spec/features/groups/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/groups/settings/user_searches_in_settings_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'User searches group settings', :js do
visit group_settings_integrations_path(group)
end
- it_behaves_like 'can highlight results', 'set default configuration'
+ it_behaves_like 'can highlight results', 'Project integration management'
end
context 'in Repository page' do
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index bcccadf7710..5f8079f0436 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -15,9 +15,10 @@ RSpec.describe 'Group' do
end
end
- describe 'create a group' do
+ describe 'create a group', :js do
before do
visit new_group_path
+ click_link 'Create group'
end
describe 'as a non-admin' do
@@ -50,13 +51,14 @@ RSpec.describe 'Group' do
fill_in 'Group URL', with: 'space group'
click_button 'Create group'
- expect(current_path).to eq(groups_path)
- expect(page).to have_namespace_error_message
+ expect(current_path).to eq(new_group_path)
+ expect(page).to have_text('Please choose a group URL with no special characters.')
end
end
describe 'with .atom at end of group path' do
it 'renders new group form with validation errors' do
+ fill_in 'Group name', with: 'test-group'
fill_in 'Group URL', with: 'atom_group.atom'
click_button 'Create group'
@@ -67,6 +69,7 @@ RSpec.describe 'Group' do
describe 'with .git at end of group path' do
it 'renders new group form with validation errors' do
+ fill_in 'Group name', with: 'test-group'
fill_in 'Group URL', with: 'git_group.git'
click_button 'Create group'
@@ -109,6 +112,7 @@ RSpec.describe 'Group' do
stub_mattermost_setting(enabled: mattermost_enabled)
visit new_group_path
+ click_link 'Create group'
end
context 'Mattermost enabled' do
@@ -119,7 +123,7 @@ RSpec.describe 'Group' do
end
it 'unchecks the checkbox by default' do
- expect(find('#group_create_chat_team')['checked']).to eq(false)
+ expect(find('#group_create_chat_team')).not_to be_checked
end
it 'updates the team URL on graph path update', :js do
@@ -147,6 +151,7 @@ RSpec.describe 'Group' do
stub_application_setting(recaptcha_enabled: true)
allow(Gitlab::Recaptcha).to receive(:load_configurations!)
visit new_group_path
+ click_link 'Create group'
end
it 'renders recaptcha' do
@@ -159,6 +164,7 @@ RSpec.describe 'Group' do
stub_feature_flags(recaptcha_on_top_level_group_creation: false)
stub_application_setting(recaptcha_enabled: true)
visit new_group_path
+ click_link 'Create group'
end
it 'does not render recaptcha' do
@@ -167,30 +173,30 @@ RSpec.describe 'Group' do
end
end
- describe 'create a nested group' do
+ describe 'create a nested group', :js do
let_it_be(:group) { create(:group, path: 'foo') }
context 'as admin' do
let(:user) { create(:admin) }
before do
- visit new_group_path(group, parent_id: group.id)
+ visit new_group_path(parent_id: group.id)
end
context 'when admin mode is enabled', :enable_admin_mode do
it 'creates a nested group' do
+ click_link 'Create group'
fill_in 'Group name', with: 'bar'
- fill_in 'Group URL', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
- expect(page).to have_content("Group 'bar' was successfully created.")
+ expect(page).to have_selector 'h1', text: 'bar'
end
end
context 'when admin mode is disabled' do
it 'is not allowed' do
- expect(page).to have_gitlab_http_status(:not_found)
+ expect(page).not_to have_button('Create group')
end
end
end
@@ -203,14 +209,14 @@ RSpec.describe 'Group' do
sign_out(:user)
sign_in(user)
- visit new_group_path(group, parent_id: group.id)
+ visit new_group_path(parent_id: group.id)
+ click_link 'Create group'
fill_in 'Group name', with: 'bar'
- fill_in 'Group URL', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
- expect(page).to have_content("Group 'bar' was successfully created.")
+ expect(page).to have_selector 'h1', text: 'bar'
end
end
@@ -221,7 +227,7 @@ RSpec.describe 'Group' do
end
context 'when creating subgroup' do
- let(:path) { new_group_path(group, parent_id: group.id) }
+ let(:path) { new_group_path(parent_id: group.id) }
it 'does not render recaptcha' do
visit path
@@ -237,6 +243,7 @@ RSpec.describe 'Group' do
before do
group.add_owner(user)
visit new_group_path(parent_id: group.id)
+ click_link 'Create group'
end
it 'shows a message if group url is available' do
@@ -255,14 +262,15 @@ RSpec.describe 'Group' do
end
end
- it 'checks permissions to avoid exposing groups by parent_id' do
+ it 'checks permissions to avoid exposing groups by parent_id', :js do
group = create(:group, :private, path: 'secret-group')
sign_out(:user)
sign_in(create(:user))
visit new_group_path(parent_id: group.id)
- expect(page).not_to have_content('secret-group')
+ expect(page).to have_title('Not Found')
+ expect(page).to have_content('Page Not Found')
end
describe 'group edit', :js do
diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb
index 96f8cf0062c..b704a0515c8 100644
--- a/spec/features/incidents/incident_details_spec.rb
+++ b/spec/features/incidents/incident_details_spec.rb
@@ -49,4 +49,42 @@ RSpec.describe 'Incident details', :js do
end
end
end
+
+ context 'when an incident `issue_type` is edited by a signed in user' do
+ it 'routes the user to the incident details page when the `issue_type` is set to incident' do
+ wait_for_requests
+ project_path = "/#{project.full_path}"
+ click_button 'Edit title and description'
+ wait_for_requests
+
+ page.within('[data-testid="issuable-form"]') do
+ click_button 'Incident'
+ click_button 'Issue'
+ click_button 'Save changes'
+
+ wait_for_requests
+
+ expect(page).to have_current_path("#{project_path}/-/issues/#{incident.iid}")
+ end
+ end
+ end
+
+ context 'when incident details are edited by a signed in user' do
+ it 'routes the user to the incident details page when the `issue_type` is set to incident' do
+ wait_for_requests
+ project_path = "/#{project.full_path}"
+ click_button 'Edit title and description'
+ wait_for_requests
+
+ page.within('[data-testid="issuable-form"]') do
+ click_button 'Incident'
+ click_button 'Issue'
+ click_button 'Save changes'
+
+ wait_for_requests
+
+ expect(page).to have_current_path("#{project_path}/-/issues/#{incident.iid}")
+ end
+ end
+ end
end
diff --git a/spec/features/issues/csv_spec.rb b/spec/features/issues/csv_spec.rb
index d41a41c4383..51e0d54ca5e 100644
--- a/spec/features/issues/csv_spec.rb
+++ b/spec/features/issues/csv_spec.rb
@@ -16,9 +16,7 @@ RSpec.describe 'Issues csv', :js do
def request_csv(params = {})
visit project_issues_path(project, params)
- page.within('.nav-controls') do
- find('[data-testid="export-csv-button"]').click
- end
+ click_button 'Export as CSV'
click_on 'Export issues'
end
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 1c8da227412..a942a1a44f6 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe 'Issue Detail', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project, author: user) }
+ let(:incident) { create(:incident, project: project, author: user) }
context 'when user displays the issue' do
before do
@@ -21,10 +22,8 @@ RSpec.describe 'Issue Detail', :js do
end
context 'when user displays the issue as an incident' do
- let(:issue) { create(:incident, project: project, author: user) }
-
before do
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, incident)
wait_for_requests
end
@@ -58,9 +57,9 @@ RSpec.describe 'Issue Detail', :js do
visit project_issue_path(project, issue)
wait_for_requests
- page.find('.js-issuable-edit').click
+ click_button 'Edit title and description'
fill_in 'issuable-title', with: 'issue title'
- click_button 'Save'
+ click_button 'Save changes'
wait_for_requests
Users::DestroyService.new(user).execute(user)
@@ -74,4 +73,58 @@ RSpec.describe 'Issue Detail', :js do
end
end
end
+
+ describe 'user updates `issue_type` via the issue type dropdown' do
+ context 'when an issue `issue_type` is edited by a signed in user' do
+ before do
+ sign_in(user)
+
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ it 'routes the user to the incident details page when the `issue_type` is set to incident' do
+ open_issue_edit_form
+
+ page.within('[data-testid="issuable-form"]') do
+ update_type_select('Issue', 'Incident')
+
+ expect(page).to have_current_path(project_issues_incident_path(project, issue))
+ end
+ end
+ end
+
+ context 'when an incident `issue_type` is edited by a signed in user' do
+ before do
+ sign_in(user)
+
+ visit project_issue_path(project, incident)
+ wait_for_requests
+ end
+
+ it 'routes the user to the issue details page when the `issue_type` is set to issue' do
+ open_issue_edit_form
+
+ page.within('[data-testid="issuable-form"]') do
+ update_type_select('Incident', 'Issue')
+
+ expect(page).to have_current_path(project_issue_path(project, incident))
+ end
+ end
+ end
+ end
+
+ def update_type_select(from, to)
+ click_button from
+ click_button to
+ click_button 'Save changes'
+
+ wait_for_requests
+ end
+
+ def open_issue_edit_form
+ wait_for_requests
+ click_button 'Edit title and description'
+ wait_for_requests
+ end
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index d147476f1ab..d828b1c1f0c 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Issue Sidebar' do
stub_feature_flags(issue_assignees_widget: false)
end
- include_examples 'issuable invite members experiments' do
+ include_examples 'issuable invite members' do
let(:issuable_path) { project_issue_path(project, issue2) }
end
@@ -266,7 +266,7 @@ RSpec.describe 'Issue Sidebar' do
let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) }
before do
- page.within('.block.milestone > .title') do
+ page.within('[data-testid="milestone_title"]') do
click_on 'Edit'
end
end
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index e2e204f03db..4a77e850d51 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe "User creates issue" do
end
end
- context "when signed in as guest" do
+ context "when signed in as guest", :js do
before do
project.add_guest(user)
sign_in(user)
@@ -38,41 +38,19 @@ RSpec.describe "User creates issue" do
visit(new_project_issue_path(project))
end
- it "creates issue", :js do
- page.within(".issue-form") do
- expect(page).to have_no_content("Assign to")
- .and have_no_content("Labels")
- .and have_no_content("Milestone")
-
- expect(page.find('#issue_title')['placeholder']).to eq 'Title'
- expect(page.find('#issue_description')['placeholder']).to eq 'Write a description or drag your files here…'
+ context 'available metadata' do
+ it 'allows guest to set issue metadata' do
+ page.within(".issue-form") do
+ expect(page).to have_content("Title")
+ .and have_content("Description")
+ .and have_content("Type")
+ .and have_content("Assignee")
+ .and have_content("Milestone")
+ .and have_content("Labels")
+ .and have_content("Due date")
+ .and have_content("This issue is confidential and should only be visible to team members with at least Reporter access.")
+ end
end
-
- issue_title = "500 error on profile"
-
- fill_in("Title", with: issue_title)
- first('.js-md').click
- first('.rspec-issuable-form-description').native.send_keys('Description')
-
- click_button("Create issue")
-
- expect(page).to have_content(issue_title)
- .and have_content(user.name)
- .and have_content(project.name)
- expect(page).to have_selector('strong', text: 'Description')
- end
-
- it 'does not render the issue type dropdown' do
- expect(page).not_to have_selector('.s-issuable-type-filter-dropdown-wrap')
- end
- end
-
- context "when signed in as developer", :js do
- before do
- project.add_developer(user)
- sign_in(user)
-
- visit(new_project_issue_path(project))
end
context "when previewing" do
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index cb4a5a32762..c59cc99467c 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -187,7 +187,8 @@ RSpec.describe "Issues > User edits issue", :js do
click_link 'Edit'
click_link 'Unassigned'
- first('.title').click
+
+ close_dropdown_menu_if_visible
expect(page).to have_content 'None - assign yourself'
end
@@ -227,7 +228,7 @@ RSpec.describe "Issues > User edits issue", :js do
close_dropdown_menu_if_visible
- page.within '.value .assign-yourself' do
+ page.within '[data-testid="no-value"]' do
expect(page).to have_content "None"
end
end
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index bbb7e8a028d..2921eea7641 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -62,11 +62,11 @@ RSpec.describe 'User interacts with awards' do
page.within('.awards') do
expect(page).to have_selector('[data-testid="award-button"]')
- expect(page.find('[data-testid="award-button"].is-active .js-counter')).to have_content('1')
- expect(page).to have_css('[data-testid="award-button"].is-active[title="You"]')
+ expect(page.find('[data-testid="award-button"].selected .js-counter')).to have_content('1')
+ expect(page).to have_css('[data-testid="award-button"].selected[title="You"]')
expect do
- page.find('[data-testid="award-button"].is-active').click
+ page.find('[data-testid="award-button"].selected').click
wait_for_requests
end.to change { page.all('[data-testid="award-button"]').size }.from(3).to(2)
end
@@ -205,7 +205,7 @@ RSpec.describe 'User interacts with awards' do
it 'adds award to issue' do
first('[data-testid="award-button"]').click
- expect(page).to have_selector('[data-testid="award-button"].is-active')
+ expect(page).to have_selector('[data-testid="award-button"].selected')
expect(first('[data-testid="award-button"]')).to have_content '1'
visit project_issue_path(project, issue)
@@ -215,7 +215,7 @@ RSpec.describe 'User interacts with awards' do
it 'removes award from issue' do
first('[data-testid="award-button"]').click
- find('[data-testid="award-button"].is-active').click
+ find('[data-testid="award-button"].selected').click
expect(first('[data-testid="award-button"]')).to have_content '0'
diff --git a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
index 2b1c25174c2..4580378dc8a 100644
--- a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
+++ b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
@@ -16,11 +16,11 @@ RSpec.describe 'Issues > User resets their incoming email token' do
end
it 'changes incoming email address token', :js do
- page.find('[data-testid="issuable-email-modal-btn"]').click
+ click_button 'Email a new issue to this project'
page.within '#issuable-email-modal' do
previous_token = page.find('input[type="text"]').value
- page.find('[data-testid="incoming-email-token-reset"]').click
+ find('[data-testid="reset_email_token_link"]').click
wait_for_requests
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 35f4b415463..9809bb34d26 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -5,35 +5,70 @@ require "spec_helper"
RSpec.describe "User toggles subscription", :js do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
+ let(:user2) { create(:user) }
let(:issue) { create(:issue, project: project, author: user) }
- before do
- project.add_developer(user)
- sign_in(user)
+ context 'user is not logged in' do
+ before do
+ visit(project_issue_path(project, issue))
+ end
- visit(project_issue_path(project, issue))
+ it 'does not display the Notification toggle' do
+ expect(page).not_to have_button('Notifications')
+ end
end
- it "unsubscribes from issue" do
- subscription_button = find('[data-testid="subscription-toggle"]')
+ context 'user is logged in' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(project_issue_path(project, issue))
+ end
+
+ it 'unsubscribes from issue' do
+ subscription_button = find('[data-testid="subscription-toggle"]')
+
+ # Check we're subscribed.
+ expect(subscription_button).to have_css("button.is-checked")
+
+ # Toggle subscription.
+ find('[data-testid="subscription-toggle"]').click
+ wait_for_requests
- # Check we're subscribed.
- expect(subscription_button).to have_css("button.is-checked")
+ # Check we're unsubscribed.
+ expect(subscription_button).to have_css("button:not(.is-checked)")
+ end
- # Toggle subscription.
- find('[data-testid="subscription-toggle"]').click
- wait_for_requests
+ context 'when project emails are disabled' do
+ let(:project) { create(:project_empty_repo, :public, emails_disabled: true) }
- # Check we're unsubscribed.
- expect(subscription_button).to have_css("button:not(.is-checked)")
+ it 'is disabled' do
+ expect(page).to have_content('Disabled by project owner')
+ expect(page).to have_button('Notifications', class: 'is-disabled')
+ end
+ end
end
- context 'when project emails are disabled' do
- let(:project) { create(:project_empty_repo, :public, emails_disabled: true) }
+ context 'user is logged in without edit permission' do
+ before do
+ sign_in(user2)
+
+ visit(project_issue_path(project, issue))
+ end
+
+ it 'subscribes to issue' do
+ subscription_button = find('[data-testid="subscription-toggle"]')
+
+ # Check we're not subscribed.
+ expect(subscription_button).to have_css("button:not(.is-checked)")
+
+ # Toggle subscription.
+ find('[data-testid="subscription-toggle"]').click
+ wait_for_requests
- it 'is disabled' do
- expect(page).to have_content('Disabled by project owner')
- expect(page).to have_button('Notifications', class: 'is-disabled')
+ # Check we're subscribed.
+ expect(subscription_button).to have_css("button.is-checked")
end
end
end
diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index 207678e07c3..c4994838d26 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Mermaid rendering', :js do
+ let_it_be(:project) { create(:project, :public) }
+
it 'renders Mermaid diagrams correctly' do
description = <<~MERMAID
```mermaid
@@ -14,7 +16,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
- project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -36,7 +37,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
- project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -44,10 +44,33 @@ RSpec.describe 'Mermaid rendering', :js do
wait_for_requests
wait_for_mermaid
- expected = '<text style=""><tspan xml:space="preserve" dy="1em" x="1">Line 1</tspan><tspan xml:space="preserve" dy="1em" x="1">Line 2</tspan></text>'
+ # From https://github.com/mermaid-js/mermaid/blob/d3f8f03a7d03a052e1fe0251d5a6d8d1f48d67ee/src/dagre-wrapper/createLabel.js#L79-L82
+ expected = %(<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">Line 1<br>Line 2</div>)
expect(page.html.scan(expected).count).to be(4)
end
+ it 'does not allow XSS in HTML labels' do
+ description = <<~MERMAID
+ ```mermaid
+ graph LR;
+ A-->CLICK_HERE_AND_GET_BONUS;
+ click A alert "aaa"
+ click CLICK_HERE_AND_GET_BONUS "javascript:alert%28%64%6f%63%75%6d%65%6e%74%2e%64%6f%6d%61%69%6e%29" "Here is the XSS"
+ ```
+ MERMAID
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+ wait_for_mermaid
+
+ # From https://github.com/mermaid-js/mermaid/blob/d3f8f03a7d03a052e1fe0251d5a6d8d1f48d67ee/src/dagre-wrapper/createLabel.js#L79-L82
+ expected = %(<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;">CLICK_HERE_AND_GET_BONUS</div>)
+ expect(page.html).to include(expected)
+ end
+
it 'renders only 2 Mermaid blocks and', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/234081' do
description = <<~MERMAID
```mermaid
@@ -64,7 +87,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
- project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -94,7 +116,6 @@ RSpec.describe 'Mermaid rendering', :js do
</details>
MERMAID
- project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -108,7 +129,37 @@ RSpec.describe 'Mermaid rendering', :js do
expect(svg[:style]).to match(/max-width/)
expect(svg[:width].to_i).to eq(100)
- expect(svg[:height].to_i).to be_within(5).of(220)
+ expect(svg[:height].to_i).to be_within(5).of(236)
+ end
+ end
+
+ it 'renders V2 state diagrams' do
+ description = <<~MERMAID
+ ```mermaid
+ stateDiagram-v2
+ [*] --> Idle
+ Idle --> Active : CONTINUE
+ state Active {
+ [*] --> Run
+ Run--> Stop: CONTINUE
+ Stop--> Run: CONTINUE
+
+ Run: Run
+ Run: entry/start
+ Run: check
+ }
+ ```
+ MERMAID
+
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+ wait_for_mermaid
+
+ page.within('.description') do
+ expect(page).to have_selector('svg')
end
end
@@ -123,7 +174,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
- project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -144,7 +194,6 @@ RSpec.describe 'Mermaid rendering', :js do
```
MERMAID
- project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -183,8 +232,6 @@ RSpec.describe 'Mermaid rendering', :js do
description *= 51
- project = create(:project, :public)
-
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
@@ -200,6 +247,36 @@ RSpec.describe 'Mermaid rendering', :js do
expect(page).to have_selector('.js-lazy-render-mermaid-container')
end
end
+
+ it 'renders without any limits on wiki page', :js do
+ graph_edges = "A-->B;B-->A;"
+
+ description = <<~MERMAID
+ ```mermaid
+ graph LR
+ #{graph_edges}
+ ```
+ MERMAID
+
+ description *= 51
+
+ project = create(:project, :public)
+
+ wiki_page = build(:wiki_page, { container: project, content: description })
+ wiki_page.create message: 'mermaid test commit' # rubocop:disable Rails/SaveBang
+ wiki_page = project.wiki.find_page(wiki_page.slug)
+
+ visit project_wiki_path(project, wiki_page)
+
+ wait_for_requests
+ wait_for_mermaid
+
+ page.within('.js-wiki-page-content') do
+ expect(page).not_to have_selector('.lazy-alert-shown')
+
+ expect(page).not_to have_selector('.js-lazy-render-mermaid-container')
+ end
+ end
end
def wait_for_mermaid
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index 9716c660fa9..f9781f6c702 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st
allow(Prometheus::ProxyService).to receive(:new).and_call_original
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
stub_kubeclient_discover(cluster.platform.api_url)
stub_prometheus_request(/prometheus-prometheus-server/, body: prometheus_values_body)
stub_prometheus_request(/prometheus\/api\/v1/, body: prometheus_values_body)
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index d36abf86518..75912238501 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'create a merge request, allowing commits from members who can me
wait_for_requests
- expect(page).to have_content('Allows commits from members who can merge to the target branch')
+ expect(page).to have_content('Members who can merge are allowed to add commits.')
end
it 'shows a message when one of the projects is private', :sidekiq_might_not_need_inline do
@@ -59,7 +59,7 @@ RSpec.describe 'create a merge request, allowing commits from members who can me
visit_new_merge_request
- expect(page).not_to have_content('Allows commits from members who can merge to the target branch')
+ expect(page).not_to have_content('The fork project allows commits from members who can write to the target branch.')
end
end
@@ -81,7 +81,7 @@ RSpec.describe 'create a merge request, allowing commits from members who can me
it 'hides the option from members' do
visit edit_project_merge_request_path(target_project, merge_request)
- expect(page).not_to have_content('Allows commits from members who can merge to the target branch')
+ expect(page).not_to have_content('The fork project allows commits from members who can write to the target branch.')
end
end
end
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index 6f46cc20cba..240b8f996c8 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Merge request > User awards emoji', :js do
it 'adds award to merge request' do
first('[data-testid="award-button"]').click
- expect(page).to have_selector('[data-testid="award-button"].is-active')
+ expect(page).to have_selector('[data-testid="award-button"].selected')
expect(first('[data-testid="award-button"]')).to have_content '1'
visit project_merge_request_path(project, merge_request)
@@ -27,7 +27,7 @@ RSpec.describe 'Merge request > User awards emoji', :js do
it 'removes award from merge request' do
first('[data-testid="award-button"]').click
- find('[data-testid="award-button"].is-active').click
+ find('[data-testid="award-button"].selected').click
expect(first('[data-testid="award-button"]')).to have_content '0'
visit project_merge_request_path(project, merge_request)
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 7d55a72c2b1..1087be3d8c6 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -68,14 +68,14 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js do
end
end
- context 'with invite members experiment considerations' do
+ context 'with invite members considerations' do
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
- include_examples 'issuable invite members experiments' do
+ include_examples 'issuable invite members' do
let(:issuable_path) { project_merge_request_path(project, merge_request) }
end
end
diff --git a/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
new file mode 100644
index 00000000000..45ee914de9d
--- /dev/null
+++ b/spec/features/merge_request/user_edits_reviewers_sidebar_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > User edits reviewers sidebar', :js do
+ context 'with invite members considerations' do
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:project) { merge_request.project }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when a privileged user can invite in reviewer dropdown' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'shows a link for inviting members and launches invite modal' do
+ visit project_merge_request_path(project, merge_request)
+
+ reviewer_edit_link.click
+
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_link('Invite Members')
+ expect(page).to have_selector('[data-track-event="click_invite_members"]')
+ expect(page).to have_selector('[data-track-label="edit_reviewer"]')
+ end
+
+ click_link 'Invite Members'
+
+ expect(page).to have_content("You're inviting members to the")
+ end
+ end
+
+ context 'when user cannot invite members in reviewer dropdown' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'shows author in assignee dropdown and no invite link' do
+ visit project_merge_request_path(project, merge_request)
+
+ reviewer_edit_link.click
+
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ expect(page).not_to have_link('Invite Members')
+ end
+ end
+ end
+
+ def reviewer_edit_link
+ find('.block.reviewer .edit-link')
+ 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 64a357de1f7..bca6e6ceba5 100644
--- a/spec/features/merge_request/user_merges_immediately_spec.rb
+++ b/spec/features/merge_request/user_merges_immediately_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
Sidekiq::Testing.fake! do
click_button 'Merge immediately'
- expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
+ expect(find('.accept-merge-request.btn-confirm')).to have_content('Merge in progress')
wait_for_requests
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 85eb956033b..d555519eb43 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -25,6 +25,8 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
}
end
+ let_it_be(:runner) { create(:ci_runner, :online) }
+
before do
stub_application_setting(auto_devops_enabled: false)
stub_ci_pipeline_yaml_file(YAML.dump(config))
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 d9b5ec17a4a..a85700fc721 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
before do
create(:service, project: project,
active: true,
- type: 'CiService',
+ type: 'DroneCiService',
category: 'ci')
visit project_merge_request_path(project, merge_request)
@@ -154,9 +154,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows information about blocked pipeline' do
- expect(page).to have_content("Pipeline blocked")
+ expect(page).to have_content("Merge blocked")
expect(page).to have_content(
- "The pipeline for this merge request requires a manual action")
+ "pipeline must succeed. It's waiting for a manual action to continue.")
expect(page).to have_css('.ci-status-icon-manual')
end
end
@@ -274,10 +274,10 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'has info button when MWBS button' do
+ it 'has confirm button when MWBS button' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- expect(page).to have_selector('.accept-merge-request.btn-info')
+ expect(page).to have_selector('.accept-merge-request.btn-confirm')
end
end
@@ -432,7 +432,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
it 'user cannot remove source branch', :sidekiq_might_not_need_inline do
expect(page).not_to have_field('remove-source-branch-input')
- expect(page).to have_content('Deletes source branch')
+ expect(page).to have_content('The source branch will be deleted')
end
end
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 8999c4d6656..34ae082750b 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -129,8 +129,8 @@ RSpec.describe 'Merge request > User sees versions', :js do
)
expect(page).to have_content '4 files'
- additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
- deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
+ additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-addition-line"]').text
+ deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-deletion-line"]').text
expect(additions_content).to eq '15'
expect(deletions_content).to eq '6'
@@ -152,8 +152,8 @@ RSpec.describe 'Merge request > User sees versions', :js do
end
it 'show diff between new and old version' do
- additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
- deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
+ additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-addition-line"]').text
+ deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-deletion-line"]').text
expect(page).to have_content '4 files'
expect(additions_content).to eq '15'
diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb
index a0b3067994c..d5061657c59 100644
--- a/spec/features/merge_request/user_views_diffs_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_spec.rb
@@ -66,6 +66,7 @@ RSpec.describe 'User views diffs', :js do
expect(page).to have_button('Expand all')
click_button 'Expand all'
+ wait_for_requests
expect(page).not_to have_button('Expand all')
end
diff --git a/spec/features/merge_requests/user_exports_as_csv_spec.rb b/spec/features/merge_requests/user_exports_as_csv_spec.rb
index 725b8366d04..351e714b612 100644
--- a/spec/features/merge_requests/user_exports_as_csv_spec.rb
+++ b/spec/features/merge_requests/user_exports_as_csv_spec.rb
@@ -12,15 +12,9 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js do
visit(project_merge_requests_path(project))
end
- subject { page.find('.nav-controls') }
-
- it { is_expected.to have_selector '[data-testid="export-csv-button"]' }
-
context 'button is clicked' do
before do
- page.within('.nav-controls') do
- find('[data-testid="export-csv-button"]').click
- end
+ click_button 'Export as CSV'
end
it 'shows a success message' do
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
new file mode 100644
index 00000000000..dfe3e76f172
--- /dev/null
+++ b/spec/features/nav/top_nav_responsive_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'top nav responsive', :js do
+ include MobileHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ before do
+ stub_feature_flags(combined_menu: true)
+
+ 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')
+
+ expect(page).to have_no_css('.top-nav-responsive')
+ end
+ end
+
+ context 'when 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
+ click_button('New...')
+
+ 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
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 84ea9495f08..0f453f1c1e5 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe 'Profile account page', :js do
within('.feed-token-reset') do
previous_token = find("#feed_token").value
- accept_confirm { click_link('reset it') }
+ accept_confirm { find('[data-testid="reset_feed_token_link"]').click }
expect(find('#feed_token').value).not_to eq(previous_token)
end
@@ -89,7 +89,7 @@ RSpec.describe 'Profile account page', :js do
within('.incoming-email-token-reset') do
previous_token = find('#incoming_email_token').value
- accept_confirm { click_link('reset it') }
+ accept_confirm { find('[data-testid="reset_email_token_link"]').click }
expect(find('#incoming_email_token').value).not_to eq(previous_token)
end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index c85657c89d5..379c25d6002 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -22,6 +22,10 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
find("#feed_token").value
end
+ def feed_token_description
+ "Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs."
+ end
+
def disallow_personal_access_token_saves!
allow(PersonalAccessTokens::CreateService).to receive(:new).and_return(pat_create_service)
@@ -123,8 +127,9 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
visit profile_personal_access_tokens_path
- expect(page).to have_content("Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar, and is included in those feed URLs.")
expect(feed_token).to eq(user.feed_token)
+
+ expect(page).to have_content(feed_token_description)
end
end
@@ -133,8 +138,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
visit profile_personal_access_tokens_path
- expect(page).not_to have_content("Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar, and is included in those feed URLs.")
- expect(page).not_to have_css("#feed_token")
+ expect(page).to have_no_content(feed_token_description)
+ expect(page).to have_no_css("#feed_token")
end
end
end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index dddca15ae24..d941988d12f 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -8,6 +8,8 @@ RSpec.describe 'User edit profile' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(improved_emoji_picker: false)
+
sign_in(user)
visit(profile_path)
end
diff --git a/spec/features/profiles/user_search_settings_spec.rb b/spec/features/profiles/user_search_settings_spec.rb
index 64a8556e349..0b05e6c9489 100644
--- a/spec/features/profiles/user_search_settings_spec.rb
+++ b/spec/features/profiles/user_search_settings_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'User searches their settings', :js do
visit profile_path
end
- it_behaves_like 'can search settings', 'Public Avatar', 'Main settings'
+ it_behaves_like 'can search settings', 'Public avatar', 'Main settings'
end
context 'in preferences page' do
diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb
index 96a321037a9..b333f64aa87 100644
--- a/spec/features/projects/active_tabs_spec.rb
+++ b/spec/features/projects/active_tabs_spec.rb
@@ -18,12 +18,11 @@ RSpec.describe 'Project active tab' do
end
context 'on project Home' do
- context 'when feature flag :sidebar_refactor is enabled' do
- before do
- visit project_path(project)
- end
+ it 'activates Project scope menu' do
+ visit project_path(project)
- it_behaves_like 'page has active tab', 'Project'
+ expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(find('.sidebar-top-level-items > li.active')).to have_content(project.name)
end
context 'when feature flag :sidebar_refactor is disabled' do
@@ -36,11 +35,23 @@ RSpec.describe 'Project active tab' do
it_behaves_like 'page has active tab', 'Project'
it_behaves_like 'page has active sub tab', 'Details'
end
+ end
- context 'on project Home/Activity' do
+ context 'on Project information' do
+ context 'default link' do
before do
visit project_path(project)
- click_tab('Activity')
+
+ click_link('Project information', match: :first)
+ end
+
+ it_behaves_like 'page has active tab', 'Project'
+ it_behaves_like 'page has active sub tab', 'Activity'
+ end
+
+ context 'on Project information/Activity' do
+ before do
+ visit activity_project_path(project)
end
it_behaves_like 'page has active tab', 'Project'
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index bfc924b5d9b..9d8f9872a1a 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe 'Pipeline Badge' do
visit pipeline_project_badges_path(project, ref: ref, format: :svg)
expect(page.status_code).to eq(200)
- expect(page.response_headers['Cache-Control']).to include 'no-cache'
+ expect(page.response_headers['Cache-Control']).to eq('no-store')
end
end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 3598aa2f423..595304789a6 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -172,7 +172,7 @@ RSpec.describe 'File blob', :js do
end
end
- context 'sucessfully change ref of similar name' do
+ context 'successfully change ref of similar name' do
before do
project.repository.create_branch('dev')
project.repository.create_branch('development')
@@ -182,14 +182,32 @@ RSpec.describe 'File blob', :js do
visit_blob('files/js/application.js', ref: 'development')
switch_ref_to('dev')
- expect(page.find('.file-title-name').text).to eq('application.js')
+ aggregate_failures do
+ expect(page.find('.file-title-name').text).to eq('application.js')
+ expect(page).not_to have_css('flash-container')
+ end
end
it 'switch ref from shorter to longer ref name' do
visit_blob('files/js/application.js', ref: 'dev')
switch_ref_to('development')
+ aggregate_failures do
+ expect(page.find('.file-title-name').text).to eq('application.js')
+ expect(page).not_to have_css('flash-container')
+ end
+ end
+ end
+
+ it 'successfully changes ref when the ref name matches the project name' do
+ project.repository.create_branch(project.name)
+
+ visit_blob('files/js/application.js', ref: project.name)
+ switch_ref_to('master')
+
+ aggregate_failures do
expect(page.find('.file-title-name').text).to eq('application.js')
+ expect(page).not_to have_css('flash-container')
end
end
end
diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb
index 53994ec018e..3b8f49accc5 100644
--- a/spec/features/projects/branches/user_deletes_branch_spec.rb
+++ b/spec/features/projects/branches/user_deletes_branch_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe "User deletes branch", :js do
sign_in(user)
end
- it "deletes branch" do
+ it "deletes branch", :js do
visit(project_branches_path(project))
branch_search = find('input[data-testid="branch-search"]')
@@ -21,11 +21,38 @@ RSpec.describe "User deletes branch", :js do
branch_search.native.send_keys(:enter)
page.within(".js-branch-improve\\/awesome") do
- accept_alert { find(".btn-danger").click }
+ find('.js-delete-branch-button').click
+ end
+
+ page.within '.modal-footer' do
+ click_button 'Yes, delete branch'
end
wait_for_requests
- expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden)
+ expect(page).to have_content('Branch was deleted')
+ end
+
+ context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
+ before do
+ stub_feature_flags(delete_branch_confirmation_modals: false)
+ end
+
+ it "deletes branch" do
+ visit(project_branches_path(project))
+
+ branch_search = find('input[data-testid="branch-search"]')
+
+ branch_search.set('improve/awesome')
+ branch_search.native.send_keys(:enter)
+
+ page.within(".js-branch-improve\\/awesome") do
+ accept_alert { click_link(title: 'Delete branch') }
+ end
+
+ wait_for_requests
+
+ expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden)
+ end
end
end
diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb
index 19d96579785..d0c0a0860d9 100644
--- a/spec/features/projects/branches/user_views_branches_spec.rb
+++ b/spec/features/projects/branches/user_views_branches_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe "User views branches" do
+RSpec.describe "User views branches", :js do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
@@ -10,9 +10,12 @@ RSpec.describe "User views branches" do
sign_in(user)
end
- context "all branches" do
+ context "all branches", :js do
before do
visit(project_branches_path(project))
+ branch_search = find('input[data-testid="branch-search"]')
+ branch_search.set('master')
+ branch_search.native.send_keys(:enter)
end
it "shows branches" do
@@ -20,6 +23,10 @@ RSpec.describe "User views branches" do
expect(page.all(".graph-side")).to all( have_content(/\d+/) )
end
+
+ it "displays a disabled button with a tooltip for the default branch that cannot be deleted", :js do
+ expect(page).to have_button('The default branch cannot be deleted', disabled: true)
+ end
end
context "protected branches" do
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index f805416b03d..0a79719f14a 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -88,10 +88,7 @@ RSpec.describe 'Branches' do
it 'shows filtered branches', :js do
visit project_branches_path(project)
- branch_search = find('input[data-testid="branch-search"]')
-
- branch_search.set('fix')
- branch_search.native.send_keys(:enter)
+ search_for_branch('fix')
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
@@ -99,13 +96,14 @@ RSpec.describe 'Branches' do
end
describe 'Delete unprotected branch on Overview' do
- it 'removes branch after confirmation', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/239019' do
+ it 'removes branch after confirmation', :js do
visit project_branches_filtered_path(project, state: 'all')
expect(all('.all-branches').last).to have_selector('li', count: 20)
- accept_confirm { first('.js-branch-item .btn-danger').click }
- expect(all('.all-branches').last).to have_selector('li', count: 19)
+ delete_branch_and_confirm
+
+ expect(page).to have_content('Branch was deleted')
end
end
@@ -151,10 +149,7 @@ RSpec.describe 'Branches' do
it 'shows filtered branches', :js do
visit project_branches_filtered_path(project, state: 'all')
- branch_search = find('input[data-testid="branch-search"]')
-
- branch_search.set('fix')
- branch_search.native.send_keys(:enter)
+ search_for_branch('fix')
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
@@ -165,17 +160,39 @@ RSpec.describe 'Branches' do
it 'removes branch after confirmation', :js do
visit project_branches_filtered_path(project, state: 'all')
- branch_search = find('input[data-testid="branch-search"]')
+ search_for_branch('fix')
- branch_search.set('fix')
- branch_search.native.send_keys(:enter)
+ expect(all('.all-branches').last).to have_selector('li', count: 1)
- expect(page).to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 1)
- accept_confirm { find('.js-branch-fix .btn-danger').click }
+ delete_branch_and_confirm
+
+ expect(page).to have_content('Branch was deleted')
+
+ page.refresh
+
+ search_for_branch('fix')
expect(page).not_to have_content('fix')
- expect(find('.all-branches')).to have_selector('li', count: 0)
+ expect(all('.all-branches').last).to have_selector('li', count: 0)
+ end
+
+ context 'when the delete_branch_confirmation_modals feature flag is disabled' do
+ it 'removes branch after confirmation', :js do
+ stub_feature_flags(delete_branch_confirmation_modals: false)
+
+ visit project_branches_filtered_path(project, state: 'all')
+
+ search_for_branch('fix')
+
+ expect(page).to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 1)
+ accept_confirm do
+ within('.js-branch-item', match: :first) { click_link(title: 'Delete branch') }
+ end
+
+ expect(page).not_to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 0)
+ end
end
end
@@ -323,4 +340,18 @@ RSpec.describe 'Branches' do
def create_file(message: 'message', branch_name:)
repository.create_file(user, generate(:branch), 'content', message: message, branch_name: branch_name)
end
+
+ def search_for_branch(name)
+ branch_search = find('input[data-testid="branch-search"]')
+ branch_search.set(name)
+ branch_search.native.send_keys(:enter)
+ end
+
+ def delete_branch_and_confirm
+ find('.js-delete-branch-button', match: :first).click
+
+ within '.modal-footer' do
+ click_button 'Yes, delete branch'
+ end
+ end
end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
deleted file mode 100644
index 74b477dd85d..00000000000
--- a/spec/features/projects/clusters/applications_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative '../../../../spec/features/clusters/installing_applications_shared_examples'
-
-RSpec.describe 'Project-level Cluster Applications', :js do
- include GoogleApi::CloudPlatformHelpers
-
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- describe 'Installing applications' do
- include_examples "installing applications on a cluster" do
- let(:cluster_path) { project_cluster_path(project, cluster) }
- let(:cluster_factory_args) { [projects: [project]] }
- end
- end
-end
diff --git a/spec/features/projects/confluence/user_views_confluence_page_spec.rb b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
index 7ec724ed55d..ece2f82f5c6 100644
--- a/spec/features/projects/confluence/user_views_confluence_page_spec.rb
+++ b/spec/features/projects/confluence/user_views_confluence_page_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'User views the Confluence page' do
end
it 'shows the page when the Confluence integration is enabled' do
- service = create(:confluence_service, project: project)
+ service = create(:confluence_integration, project: project)
visit project_wikis_confluence_path(project)
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 1d7be7fa7a3..fea054de64e 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -329,11 +329,11 @@ RSpec.describe 'Environment' do
expect(page).to have_button('Stop')
end
- it 'user deletes the branch with running environment' do
+ it 'user deletes the branch with running environment', :js do
visit project_branches_filtered_path(project, state: 'all', search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do
- page.within('.js-branch-feature') { find('a.btn-danger').click }
+ page.within('.js-branch-feature') { find('.js-delete-branch-button').click }
end
visit_environment(environment)
@@ -341,6 +341,24 @@ RSpec.describe 'Environment' do
expect(page).not_to have_button('Stop')
end
+ context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
+ before do
+ stub_feature_flags(delete_branch_confirmation_modals: false)
+ end
+
+ it 'user deletes the branch with running environment' do
+ visit project_branches_filtered_path(project, state: 'all', search: 'feature')
+
+ remove_branch_with_hooks(project, user, 'feature') do
+ within('.js-branch-feature') { click_link(title: 'Delete branch') }
+ end
+
+ visit_environment(environment)
+
+ expect(page).not_to have_button('Stop')
+ end
+ end
+
##
# This is a workaround for problem described in #24543
#
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index de7ff1c473d..0dd4bd55d46 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe 'Environments page', :js do
context 'when cluster is not reachable' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let!(:application_prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:integration_prometheus) { create(:clusters_integrations_prometheus, cluster: cluster) }
before do
allow_next_instance_of(Kubeclient::Client) do |instance|
diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb
index eaef3e6ca28..5019e45593c 100644
--- a/spec/features/projects/environments_pod_logs_spec.rb
+++ b/spec/features/projects/environments_pod_logs_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe 'Environment > Pod Logs', :js, :kubeclient do
dropdown_items = find(".dropdown-menu").all(".dropdown-item")
expect(dropdown_items.first).to have_content(environment.name)
- expect(dropdown_items.size).to eq(3)
+ expect(dropdown_items.size).to eq(2)
end
end
diff --git a/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb b/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb
index 2a81c706525..37d6f299883 100644
--- a/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb
+++ b/spec/features/projects/feature_flag_user_lists/user_deletes_feature_flag_user_list_spec.rb
@@ -17,12 +17,13 @@ RSpec.describe 'User deletes feature flag user list', :js do
end
it 'deletes the list' do
- visit(project_feature_flags_path(project, scope: 'userLists'))
+ visit(project_feature_flags_user_lists_path(project, scope: 'userLists'))
delete_user_list_button.click
delete_user_list_modal_confirmation_button.click
- expect(page).to have_text('Lists 0')
+ expect(page).to have_text('Lists')
+ expect(page).not_to have_selector('[data-testid="ffUserListName"]')
end
end
@@ -34,7 +35,7 @@ RSpec.describe 'User deletes feature flag user list', :js do
end
it 'does not delete the list' do
- visit(project_feature_flags_path(project, scope: 'userLists'))
+ visit(project_feature_flags_user_lists_path(project, scope: 'userLists'))
delete_user_list_button.click
delete_user_list_modal_confirmation_button.click
diff --git a/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb b/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
index 50fc7bb0753..d922bc1f4a0 100644
--- a/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
+++ b/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb
@@ -18,65 +18,21 @@ RSpec.describe 'User sees feature flag list', :js do
context 'with legacy feature flags' do
before do
- create_flag(project, 'ci_live_trace', false).tap do |feature_flag|
+ create_flag(project, 'ci_live_trace', false, version: :legacy_flag).tap do |feature_flag|
create_scope(feature_flag, 'review/*', true)
end
- create_flag(project, 'drop_legacy_artifacts', false)
- create_flag(project, 'mr_train', true).tap do |feature_flag|
+ create_flag(project, 'drop_legacy_artifacts', false, version: :legacy_flag)
+ create_flag(project, 'mr_train', true, version: :legacy_flag).tap do |feature_flag|
create_scope(feature_flag, 'production', false)
end
end
- it 'user sees the first flag' do
- visit(project_feature_flags_path(project))
-
- within_feature_flag_row(1) do
- expect(page.find('.js-feature-flag-id')).to have_content('^1')
- expect(page.find('.feature-flag-name')).to have_content('ci_live_trace')
- expect_status_toggle_button_not_to_be_checked
-
- within_feature_flag_scopes do
- expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(1)')).to have_content('*')
- expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(2)')).to have_content('review/*')
- end
- end
- end
-
- it 'user sees the second flag' do
- visit(project_feature_flags_path(project))
-
- within_feature_flag_row(2) do
- expect(page.find('.js-feature-flag-id')).to have_content('^2')
- expect(page.find('.feature-flag-name')).to have_content('drop_legacy_artifacts')
- expect_status_toggle_button_not_to_be_checked
-
- within_feature_flag_scopes do
- expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(1)')).to have_content('*')
- end
- end
- end
-
- it 'user sees the third flag' do
- visit(project_feature_flags_path(project))
-
- within_feature_flag_row(3) do
- expect(page.find('.js-feature-flag-id')).to have_content('^3')
- expect(page.find('.feature-flag-name')).to have_content('mr_train')
- expect_status_toggle_button_to_be_checked
-
- within_feature_flag_scopes do
- expect(page.find('[data-qa-selector="feature-flag-scope-info-badge"]:nth-child(1)')).to have_content('*')
- expect(page.find('[data-qa-selector="feature-flag-scope-muted-badge"]:nth-child(2)')).to have_content('production')
- end
- end
- end
-
- it 'user sees the status toggle disabled' do
+ it 'shows empty page' do
visit(project_feature_flags_path(project))
- within_feature_flag_row(1) do
- expect_status_toggle_button_to_be_disabled
- end
+ expect(page).to have_text 'Get started with feature flags'
+ expect(page).to have_selector('.btn-confirm', text: 'New feature flag')
+ expect(page).to have_selector('[data-qa-selector="configure_feature_flags_button"]', text: 'Configure')
end
end
diff --git a/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb b/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
index a435e565ff1..9c03a26abc8 100644
--- a/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
+++ b/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb
@@ -73,16 +73,16 @@ RSpec.describe 'User updates feature flag', :js do
context 'with a legacy feature flag' do
let!(:feature_flag) do
create_flag(project, 'ci_live_trace', true,
- description: 'For live trace feature')
+ description: 'For live trace feature',
+ version: :legacy_flag)
end
let!(:scope) { create_scope(feature_flag, 'review/*', true) }
- it 'the user cannot edit the flag' do
+ it 'shows not found error' do
visit(edit_project_feature_flag_path(project, feature_flag))
- expect(page).to have_text 'This feature flag is read-only, and it will be removed in 14.0.'
- expect(page).to have_css('button.js-ff-submit.disabled')
+ expect(page).to have_text 'Page Not Found'
end
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 363fe8c35fe..2e5a5cef0fd 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'Edit Project Settings' do
context 'When external issue tracker is enabled and issues enabled on project settings' do
it 'does not hide issues tab and hides labels tab' do
allow_next_instance_of(Project) do |instance|
- allow(instance).to receive(:external_issue_tracker).and_return(JiraService.new)
+ allow(instance).to receive(:external_issue_tracker).and_return(Integrations::Jira.new)
end
visit project_path(project)
@@ -58,7 +58,7 @@ RSpec.describe 'Edit Project Settings' do
project.issues_enabled = false
project.save!
allow_next_instance_of(Project) do |instance|
- allow(instance).to receive(:external_issue_tracker).and_return(JiraService.new)
+ allow(instance).to receive(:external_issue_tracker).and_return(Integrations::Jira.new)
end
end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index af228764c17..25836514981 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -30,14 +30,14 @@ RSpec.describe 'Import/Export - project import integration test', :js do
it 'user imports an exported project successfully', :sidekiq_might_not_need_inline do
visit new_project_path
- click_import_project_tab
+ click_import_project
click_link 'GitLab export'
fill_in :name, with: 'Test Project Name', visible: true
fill_in :path, with: 'test-project-path', visible: true
attach_file('file', file)
- expect { click_on 'Import project' }.to change { Project.count }.by(1)
+ expect { click_button 'Import project' }.to change { Project.count }.by(1)
project = Project.last
expect(project).not_to be_nil
@@ -49,11 +49,11 @@ RSpec.describe 'Import/Export - project import integration test', :js do
visit new_project_path
- click_import_project_tab
+ click_import_project
click_link 'GitLab export'
fill_in :name, with: project.name, visible: true
attach_file('file', file)
- click_on 'Import project'
+ click_button 'Import project'
page.within('.flash-container') do
expect(page).to have_content('Project could not be imported')
@@ -61,7 +61,7 @@ RSpec.describe 'Import/Export - project import integration test', :js do
end
end
- def click_import_project_tab
+ def click_import_project
find('[data-qa-selector="import_project_link"]').click
end
end
diff --git a/spec/features/projects/infrastructure_registry_spec.rb b/spec/features/projects/infrastructure_registry_spec.rb
new file mode 100644
index 00000000000..9cab4ebeb3a
--- /dev/null
+++ b/spec/features/projects/infrastructure_registry_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Infrastructure Registry' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+ end
+
+ context 'when feature is not available' do
+ before do
+ stub_feature_flags(infrastructure_registry_page: false)
+ end
+
+ it 'gives 404' do
+ visit_project_infrastructure_registry
+
+ expect(status_code).to eq(404)
+ end
+ end
+
+ context 'when feature is available', :js do
+ before do
+ visit_project_infrastructure_registry
+ end
+
+ context 'when there are packages' do
+ let_it_be(:terraform_module) { create(:terraform_module_package, project: project, created_at: 1.day.ago, version: '1.0.0') }
+ let_it_be(:terraform_module2) { create(:terraform_module_package, project: project, created_at: 2.days.ago, version: '2.0.0') }
+ let_it_be(:packages) { [terraform_module, terraform_module2] }
+
+ it_behaves_like 'packages list'
+
+ context 'deleting a package' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:terraform_module) { create(:terraform_module_package, project: project) }
+
+ it 'allows you to delete a module', :aggregate_failures do
+ # this is still using the package copy in the UI too
+ click_button('Remove package')
+ click_button('Delete package')
+
+ expect(page).to have_content 'Package deleted successfully'
+ expect(page).not_to have_content(terraform_module.name)
+ end
+ end
+ end
+
+ it 'displays the empty message' do
+ expect(page).to have_content('You have no Terraform modules in your project')
+ end
+ end
+
+ def visit_project_infrastructure_registry
+ visit project_infrastructure_registry_index_path(project)
+ end
+end
diff --git a/spec/features/projects/services/user_activates_flowdock_spec.rb b/spec/features/projects/integrations/user_activates_flowdock_spec.rb
index 4a4d7bbecfd..4a4d7bbecfd 100644
--- a/spec/features/projects/services/user_activates_flowdock_spec.rb
+++ b/spec/features/projects/integrations/user_activates_flowdock_spec.rb
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/integrations/user_activates_jira_spec.rb
index 10f84aae93f..10f84aae93f 100644
--- a/spec/features/projects/services/user_activates_jira_spec.rb
+++ b/spec/features/projects/integrations/user_activates_jira_spec.rb
diff --git a/spec/features/projects/services/user_activates_pivotaltracker_spec.rb b/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
index 83f66d4fa7b..83f66d4fa7b 100644
--- a/spec/features/projects/services/user_activates_pivotaltracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_pivotaltracker_spec.rb
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index 4caf3e947c7..6ce6834b5d5 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -32,6 +32,14 @@ RSpec.describe 'Project > Members > Invite group', :js do
expect(page).to have_selector(expected_invite_group_selector)
end
+
+ it 'does not display either the form or the button when visiting the page not signed in' do
+ project = create(:project, namespace: create(:group))
+
+ visit project_project_members_path(project)
+
+ expect(page).not_to have_selector(expected_invite_group_selector)
+ end
end
describe 'Share with group lock' do
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index f1fc579bb8a..25598146604 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -6,17 +6,17 @@ RSpec.describe 'Project members list', :js do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
- let(:user1) { create(:user, name: 'John Doe') }
- let(:user2) { create(:user, name: 'Mary Jane') }
- let(:group) { create(:group) }
- let(:project) { create(:project, :internal, namespace: group) }
+ let_it_be(:user1) { create(:user, name: 'John Doe') }
+ let_it_be(:user2) { create(:user, name: 'Mary Jane') }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :internal, namespace: group) }
before do
sign_in(user1)
group.add_owner(user1)
end
- it 'show members from project and group' do
+ it 'show members from project and group', :aggregate_failures do
project.add_developer(user2)
visit_members_page
@@ -25,7 +25,7 @@ RSpec.describe 'Project members list', :js do
expect(second_row).to have_content(user2.name)
end
- it 'show user once if member of both group and project' do
+ it 'show user once if member of both group and project', :aggregate_failures do
project.add_developer(user1)
visit_members_page
@@ -47,7 +47,7 @@ RSpec.describe 'Project members list', :js do
end
end
- it 'add user to project' do
+ it 'add user to project', :snowplow, :aggregate_failures do
visit_members_page
invite_member(user2.name, role: 'Reporter')
@@ -55,9 +55,17 @@ RSpec.describe 'Project members list', :js do
page.within find_member_row(user2) do
expect(page).to have_button('Reporter')
end
+
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: 'project-members-page',
+ property: 'existing_user',
+ user: user1
+ )
end
- it 'uses ProjectMember access_level_roles for the invite members modal access option' do
+ it 'uses ProjectMember access_level_roles for the invite members modal access option', :aggregate_failures do
visit_members_page
click_on 'Invite members'
@@ -95,7 +103,7 @@ RSpec.describe 'Project members list', :js do
expect(members_table).not_to have_content(other_user.name)
end
- it 'invite user to project' do
+ it 'invite user to project', :snowplow, :aggregate_failures do
visit_members_page
invite_member('test@example.com', role: 'Reporter')
@@ -105,6 +113,28 @@ RSpec.describe 'Project members list', :js do
page.within find_invited_member_row('test@example.com') do
expect(page).to have_button('Reporter')
end
+
+ expect_snowplow_event(
+ category: 'Members::InviteService',
+ action: 'create_member',
+ label: 'project-members-page',
+ property: 'net_new_user',
+ user: user1
+ )
+ end
+
+ context 'as a signed out visitor viewing a public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ before do
+ sign_out(user1)
+ end
+
+ it 'does not show the Invite members button when not signed in' do
+ visit_members_page
+
+ expect(page).not_to have_button('Invite members')
+ end
end
context 'project bots' do
@@ -114,7 +144,7 @@ RSpec.describe 'Project members list', :js do
project.add_maintainer(project_bot)
end
- it 'does not show form used to change roles and "Expiration date" or the remove user button' do
+ it 'does not show form used to change roles and "Expiration date" or the remove user button', :aggregate_failures do
visit_members_page
page.within find_member_row(project_bot) do
diff --git a/spec/features/projects/members/tabs_spec.rb b/spec/features/projects/members/tabs_spec.rb
index 471be26e126..5611e7ee810 100644
--- a/spec/features/projects/members/tabs_spec.rb
+++ b/spec/features/projects/members/tabs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Tabs' do
+RSpec.describe 'Projects > Members > Tabs', :js do
include Spec::Support::Helpers::Features::MembersHelpers
using RSpec::Parameterized::TableSyntax
@@ -44,7 +44,7 @@ RSpec.describe 'Projects > Members > Tabs' do
end
end
- context 'when searching "Groups"', :js do
+ context 'when searching "Groups"' do
before do
click_link 'Groups'
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 7073741a92d..94543290050 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe 'Projects > Members > User requests access', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
let(:maintainer) { project.owner }
before do
@@ -47,6 +48,8 @@ RSpec.describe 'Projects > Members > User requests access', :js do
expect(project.requesters.exists?(user_id: user)).to be_truthy
+ click_link 'Project information'
+
page.within('.nav-sidebar') do
click_link('Members')
end
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index ee5bf99fd75..bce11e6bc8a 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -17,6 +17,10 @@ RSpec.describe 'Project navbar' do
end
context 'when sidebar refactor feature flag is disabled' do
+ let(:project_context_nav_item) do
+ nil
+ end
+
before do
stub_feature_flags(sidebar_refactor: false)
insert_package_nav(_('Operations'))
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index a1523f9eb08..c57432ae94e 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'New project', :js do
include Select2Helper
+ include Spec::Support::Helpers::Features::TopNavSpecHelpers
shared_examples 'combined_menu: feature flag examples' do
context 'as a user' do
@@ -45,34 +46,39 @@ RSpec.describe 'New project', :js do
end
it 'when in control it renders "project" in the new projects dropdown' do
- pending_on_combined_menu_flag
-
stub_experiments(new_repo: :control)
visit new_project_path
- find('#nav-projects-dropdown').click
-
- page.within('#nav-projects-dropdown') do
- expect(page).to have_selector('a', text: 'Create blank project')
- expect(page).to have_selector('a', text: 'Import project')
- expect(page).to have_no_selector('a', text: 'Create blank project/repository')
- expect(page).to have_no_selector('a', text: 'Import project/repository')
+ open_top_nav_projects
+
+ within_top_nav do
+ if Feature.enabled?(:combined_menu, default_enabled: :yaml)
+ expect(page).to have_selector('a', text: 'Create new project')
+ expect(page).to have_no_selector('a', text: 'Create blank project/repository')
+ else
+ expect(page).to have_selector('a', text: 'Create blank project')
+ expect(page).to have_selector('a', text: 'Import project')
+ expect(page).to have_no_selector('a', text: 'Create blank project/repository')
+ expect(page).to have_no_selector('a', text: 'Import project/repository')
+ end
end
end
it 'when in candidate it renders "project/repository" in the new projects dropdown' do
- pending_on_combined_menu_flag
-
stub_experiments(new_repo: :candidate)
visit new_project_path
- find('#nav-projects-dropdown').click
+ open_top_nav_projects
- page.within('#nav-projects-dropdown') do
- expect(page).to have_selector('a', text: 'Create blank project/repository')
- expect(page).to have_selector('a', text: 'Import project/repository')
+ within_top_nav do
+ if Feature.enabled?(:combined_menu, default_enabled: :yaml)
+ expect(page).to have_selector('a', text: 'Create new project')
+ else
+ expect(page).to have_selector('a', text: 'Create blank project/repository')
+ expect(page).to have_selector('a', text: 'Import project/repository')
+ end
end
end
end
@@ -356,16 +362,6 @@ RSpec.describe 'New project', :js do
expect(git_import_instructions).to have_content 'Git repository URL'
end
- it 'reports error if repo URL does not end with .git' do
- fill_in 'project_import_url', with: 'http://foo/bar'
- fill_in 'project_name', with: 'import-project-without-git-suffix'
- fill_in 'project_path', with: 'import-project-without-git-suffix'
-
- click_button 'Create project'
-
- expect(page).to have_text('Please provide a valid URL ending with .git')
- end
-
it 'keeps "Import project" tab open after form validation error' do
collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace)
@@ -422,7 +418,7 @@ RSpec.describe 'New project', :js do
end
end
- context 'with combined_menu: feature flag on' do
+ context 'with combined_menu feature flag on' do
let(:needs_rewrite_for_combined_menu_flag_on) { true }
before do
@@ -441,8 +437,4 @@ RSpec.describe 'New project', :js do
it_behaves_like 'combined_menu: feature flag examples'
end
-
- def pending_on_combined_menu_flag
- pending 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587' if needs_rewrite_for_combined_menu_flag_on
- end
end
diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb
index 412ba17cf20..71d4cce2784 100644
--- a/spec/features/projects/pages/user_edits_settings_spec.rb
+++ b/spec/features/projects/pages/user_edits_settings_spec.rb
@@ -39,12 +39,6 @@ RSpec.describe 'Pages edits pages settings', :js do
end
end
- it 'renders first deployment warning' do
- visit project_pages_path(project)
-
- expect(page).to have_content('It may take up to 30 minutes before the site is available after the first deployment.')
- end
-
shared_examples 'does not render access control warning' do
it 'does not render access control warning' do
visit project_pages_path(project)
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index d8a55fc7f3b..fcb1b6a0015 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -19,129 +19,147 @@ RSpec.describe 'User views releases', :js do
project.add_guest(guest)
end
- context('when the user is a maintainer') do
- before do
- sign_in(maintainer)
-
- visit project_releases_path(project)
- end
+ shared_examples 'releases index page' do
+ context('when the user is a maintainer') do
+ before do
+ sign_in(maintainer)
- it 'sees the release' do
- page.within("##{release_v1.tag}") do
- expect(page).to have_content(release_v1.name)
- expect(page).to have_content(release_v1.tag)
- expect(page).not_to have_content('Upcoming Release')
+ visit project_releases_path(project)
end
- end
-
- context 'when there is a link as an asset' do
- let!(:release_link) { create(:release_link, release: release_v1, url: url ) }
- let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
- let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release_v1) << "/downloads#{release_link.filepath}" }
- it 'sees the link' do
- page.within("##{release_v1.tag} .js-assets-list") do
- expect(page).to have_link release_link.name, href: direct_asset_link
- expect(page).not_to have_css('[data-testid="external-link-indicator"]')
+ it 'sees the release' do
+ page.within("##{release_v1.tag}") do
+ expect(page).to have_content(release_v1.name)
+ expect(page).to have_content(release_v1.tag)
+ expect(page).not_to have_content('Upcoming Release')
end
end
- context 'when there is a link redirect' do
- let!(:release_link) { create(:release_link, release: release_v1, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: url) }
+ context 'when there is a link as an asset' do
+ let!(:release_link) { create(:release_link, release: release_v1, url: url ) }
let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
+ let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release_v1) << "/downloads#{release_link.filepath}" }
- it 'sees the link', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/329301' do
+ it 'sees the link' do
page.within("##{release_v1.tag} .js-assets-list") do
expect(page).to have_link release_link.name, href: direct_asset_link
expect(page).not_to have_css('[data-testid="external-link-indicator"]')
end
end
- end
- context 'when url points to external resource' do
- let(:url) { 'http://google.com/download' }
+ context 'when there is a link redirect' do
+ let!(:release_link) { create(:release_link, release: release_v1, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: url) }
+ let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" }
- it 'sees that the link is external resource', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/329302' do
- page.within("##{release_v1.tag} .js-assets-list") do
- expect(page).to have_css('[data-testid="external-link-indicator"]')
+ it 'sees the link', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/329301' do
+ page.within("##{release_v1.tag} .js-assets-list") do
+ expect(page).to have_link release_link.name, href: direct_asset_link
+ expect(page).not_to have_css('[data-testid="external-link-indicator"]')
+ end
+ end
+ end
+
+ context 'when url points to external resource' do
+ let(:url) { 'http://google.com/download' }
+
+ it 'sees that the link is external resource', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/329302' do
+ page.within("##{release_v1.tag} .js-assets-list") do
+ expect(page).to have_css('[data-testid="external-link-indicator"]')
+ end
end
end
end
- end
- context 'with an upcoming release' do
- it 'sees the upcoming tag' do
- page.within("##{release_v3.tag}") do
- expect(page).to have_content('Upcoming Release')
+ context 'with an upcoming release' do
+ it 'sees the upcoming tag' do
+ page.within("##{release_v3.tag}") do
+ expect(page).to have_content('Upcoming Release')
+ end
end
end
- end
- context 'with a tag containing a slash' do
- it 'sees the release' do
- page.within("##{release_v2.tag.parameterize}") do
- expect(page).to have_content(release_v2.name)
- expect(page).to have_content(release_v2.tag)
+ context 'with a tag containing a slash' do
+ it 'sees the release' do
+ page.within("##{release_v2.tag.parameterize}") do
+ expect(page).to have_content(release_v2.name)
+ expect(page).to have_content(release_v2.tag)
+ end
end
end
- end
- context 'sorting' do
- def sort_page(by:, direction:)
- within '[data-testid="releases-sort"]' do
- find('.dropdown-toggle').click
+ context 'sorting' do
+ def sort_page(by:, direction:)
+ within '[data-testid="releases-sort"]' do
+ find('.dropdown-toggle').click
- click_button(by, class: 'dropdown-item')
+ click_button(by, class: 'dropdown-item')
- find('.sorting-direction-button').click if direction == :ascending
+ find('.sorting-direction-button').click if direction == :ascending
+ end
end
- end
- shared_examples 'releases sort order' do
- it "sorts the releases #{description}" do
- card_titles = page.all('.release-block .card-title', minimum: expected_releases.count)
+ shared_examples 'releases sort order' do
+ it "sorts the releases #{description}" do
+ card_titles = page.all('.release-block .card-title', minimum: expected_releases.count)
- card_titles.each_with_index do |title, index|
- expect(title).to have_content(expected_releases[index].name)
+ card_titles.each_with_index do |title, index|
+ expect(title).to have_content(expected_releases[index].name)
+ end
end
end
- end
- context "when the page is sorted by the default sort order" do
- let(:expected_releases) { [release_v3, release_v2, release_v1] }
+ context "when the page is sorted by the default sort order" do
+ let(:expected_releases) { [release_v3, release_v2, release_v1] }
- it_behaves_like 'releases sort order'
- end
+ it_behaves_like 'releases sort order'
+ end
- context "when the page is sorted by created_at ascending " do
- let(:expected_releases) { [release_v2, release_v1, release_v3] }
+ context "when the page is sorted by created_at ascending " do
+ let(:expected_releases) { [release_v2, release_v1, release_v3] }
- before do
- sort_page by: 'Created date', direction: :ascending
+ before do
+ sort_page by: 'Created date', direction: :ascending
+ end
+
+ it_behaves_like 'releases sort order'
end
+ end
+ end
+
+ context('when the user is a guest') do
+ before do
+ sign_in(guest)
+ end
+
+ it 'renders release info except for Git-related data' do
+ visit project_releases_path(project)
+
+ within('.release-block', match: :first) do
+ expect(page).to have_content(release_v3.description)
- it_behaves_like 'releases sort order'
+ # The following properties (sometimes) include Git info,
+ # so they are not rendered for Guest users
+ expect(page).not_to have_content(release_v3.name)
+ expect(page).not_to have_content(release_v3.tag)
+ expect(page).not_to have_content(release_v3.commit.short_id)
+ end
end
end
end
- context('when the user is a guest') do
+ context 'when the releases_index_apollo_client feature flag is enabled' do
before do
- sign_in(guest)
+ stub_feature_flags(releases_index_apollo_client: true)
end
- it 'renders release info except for Git-related data' do
- visit project_releases_path(project)
-
- within('.release-block', match: :first) do
- expect(page).to have_content(release_v3.description)
+ it_behaves_like 'releases index page'
+ end
- # The following properties (sometimes) include Git info,
- # so they are not rendered for Guest users
- expect(page).not_to have_content(release_v3.name)
- expect(page).not_to have_content(release_v3.tag)
- expect(page).not_to have_content(release_v3.commit.short_id)
- end
+ context 'when the releases_index_apollo_client feature flag is disabled' do
+ before do
+ stub_feature_flags(releases_index_apollo_client: false)
end
+
+ it_behaves_like 'releases index page'
end
end
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index a0b06d7e2a1..db8c2a24f2f 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -25,7 +25,6 @@ RSpec.describe 'Functions', :js do
end
it 'sees an empty state require Knative installation' do
- expect(page).to have_link('Install Knative')
expect(page).to have_selector('.empty-state')
end
end
diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/services/disable_triggers_spec.rb
index d9e200cf563..c6413685f38 100644
--- a/spec/features/projects/services/disable_triggers_spec.rb
+++ b/spec/features/projects/services/disable_triggers_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'Disable individual triggers', :js do
let(:service_name) { 'Jenkins' }
it 'shows trigger checkboxes' do
- event_count = JenkinsService.supported_events.count
+ event_count = Integrations::Jenkins.supported_events.count
expect(page).to have_content "Trigger"
expect(page).to have_css(checkbox_selector, visible: :all, count: event_count)
diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
index 54a501e89a2..b2ca0424b6d 100644
--- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
@@ -14,35 +14,10 @@ RSpec.describe 'Set up Mattermost slash commands', :js do
context 'mattermost service is enabled' do
let(:mattermost_enabled) { true }
- it 'shows a help message' do
- expect(page).to have_content("Use this service to perform common")
- end
-
- it 'shows a token placeholder' do
- token_placeholder = find_field('service_token')['placeholder']
-
- expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
- end
-
- it 'redirects to the integrations page after saving but not activating' do
- token = ('a'..'z').to_a.join
-
- fill_in 'service_token', with: token
- click_active_checkbox
- click_save_integration
-
- expect(current_path).to eq(edit_project_service_path(project, :mattermost_slash_commands))
- expect(page).to have_content('Mattermost slash commands settings saved, but not active.')
- end
-
- it 'redirects to the integrations page after activating' do
- token = ('a'..'z').to_a.join
-
- fill_in 'service_token', with: token
- click_save_integration
+ describe 'activation' do
+ let(:edit_path) { edit_project_service_path(project, :mattermost_slash_commands) }
- expect(current_path).to eq(edit_project_service_path(project, :mattermost_slash_commands))
- expect(page).to have_content('Mattermost slash commands settings saved and active.')
+ include_examples 'user activates the Mattermost Slash Command integration'
end
it 'shows the add to mattermost button' do
@@ -109,7 +84,7 @@ RSpec.describe 'Set up Mattermost slash commands', :js do
end
it 'shows an error alert with the error message if there is an error requesting teams' do
- allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [[], 'test mattermost error message'] }
+ allow_any_instance_of(Integrations::MattermostSlashCommands).to receive(:list_teams) { [[], 'test mattermost error message'] }
click_link 'Add to Mattermost'
@@ -138,7 +113,7 @@ RSpec.describe 'Set up Mattermost slash commands', :js do
def stub_teams(count: 0)
teams = create_teams(count)
- allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [teams, nil] }
+ allow_any_instance_of(Integrations::MattermostSlashCommands).to receive(:list_teams) { [teams, nil] }
teams
end
diff --git a/spec/features/projects/services/user_activates_slack_notifications_spec.rb b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
index 0cba1ee1c4c..dec83ff1489 100644
--- a/spec/features/projects/services/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'User activates Slack notifications', :js do
end
context 'when service is already configured' do
- let(:service) { SlackService.new }
+ let(:service) { Integrations::Slack.new }
let(:project) { create(:project, slack_service: service) }
before do
diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb
index 64138e0aeca..971a747e64f 100644
--- a/spec/features/projects/settings/monitor_settings_spec.rb
+++ b/spec/features/projects/settings/monitor_settings_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
visit project_settings_operations_path(project)
wait_for_requests
- click_expand_incident_management_button
+ click_settings_tab
end
it 'renders form for incident management' do
@@ -60,22 +60,24 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
click_on('bug')
save_form
- click_expand_incident_management_button
+ click_settings_tab
expect(find_field(create_issue)).to be_checked
expect(page).to have_selector(:id, 'alert-integration-settings-issue-template', text: 'bug')
+
+ click_settings_tab
expect(find_field(send_email)).not_to be_checked
end
- def click_expand_incident_management_button
- within '.qa-incident-management-settings' do
- click_button('Expand')
+ def click_settings_tab
+ within '[data-testid="alert-integration-settings"]' do
+ click_link 'Alert settings'
end
end
def save_form
- page.within ".qa-incident-management-settings" do
- click_on 'Save changes'
+ page.within '[data-testid="alert-integration-settings"]' do
+ click_button 'Save changes'
end
end
end
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 6a2769d11fd..1cc54b71d4a 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -24,14 +24,14 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'shows available section' do
subject
- settings_block = find('#js-registry-policies')
+ settings_block = find('[data-testid="registry-settings-app"]')
expect(settings_block).to have_text 'Clean up image tags'
end
it 'saves cleanup policy submit the form' do
subject
- within '#js-registry-policies' do
+ within '[data-testid="registry-settings-app"]' do
select('Every day', from: 'Run cleanup')
select('50 tags per image name', from: 'Keep the most recent:')
fill_in('Keep tags matching:', with: 'stable')
@@ -49,7 +49,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not save cleanup policy submit form with invalid regex' do
subject
- within '#js-registry-policies' do
+ within '[data-testid="registry-settings-app"]' do
fill_in('Remove tags matching:', with: '*-production')
submit_button = find('[data-testid="save-button"')
@@ -80,7 +80,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'displays the expected result' do
subject
- within '#js-registry-policies' do
+ within '[data-testid="registry-settings-app"]' do
case result
when :available_section
expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
@@ -98,7 +98,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not exists' do
subject
- expect(page).not_to have_selector('#js-registry-policies')
+ expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
end
end
@@ -108,7 +108,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not exists' do
subject
- expect(page).not_to have_selector('#js-registry-policies')
+ expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
end
end
end
diff --git a/spec/features/projects/settings/service_desk_setting_spec.rb b/spec/features/projects/settings/service_desk_setting_spec.rb
index 50451075db5..91355d8f625 100644
--- a/spec/features/projects/settings/service_desk_setting_spec.rb
+++ b/spec/features/projects/settings/service_desk_setting_spec.rb
@@ -89,25 +89,10 @@ RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
before do
stub_licensed_features(custom_file_templates_for_namespace: false, custom_file_templates: false)
group.update_columns(file_template_project_id: group_template_repo.id)
+ visit edit_project_path(project)
end
- context 'when inherited_issuable_templates enabled' do
- before do
- stub_feature_flags(inherited_issuable_templates: true)
- visit edit_project_path(project)
- end
-
- it_behaves_like 'issue description templates from current project only'
- end
-
- context 'when inherited_issuable_templates disabled' do
- before do
- stub_feature_flags(inherited_issuable_templates: false)
- visit edit_project_path(project)
- end
-
- it_behaves_like 'issue description templates from current project only'
- end
+ it_behaves_like 'issue description templates from current project only'
end
end
end
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index bf90e86c263..862bae45fc6 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -116,7 +116,8 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
click_on('Save changes')
end
- find('.flash-notice')
+ wait_for_all_requests
+
checkbox = find_field('project_printing_merge_request_link_enabled')
expect(checkbox).not_to be_checked
@@ -139,7 +140,8 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
click_on('Save changes')
end
- find('.flash-notice')
+ wait_for_all_requests
+
checkbox = find_field('project_remove_source_branch_after_merge')
expect(checkbox).not_to be_checked
diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb
index 9b09958bae5..a60743f0e47 100644
--- a/spec/features/projects/settings/user_searches_in_settings_spec.rb
+++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe 'User searches project settings', :js do
visit project_settings_operations_path(project)
end
- it_behaves_like 'can search settings', 'Alert integrations', 'Error tracking'
+ it_behaves_like 'can search settings', 'Alerts', 'Error tracking'
end
context 'in Pages page' do
diff --git a/spec/features/projects/show/schema_markup_spec.rb b/spec/features/projects/show/schema_markup_spec.rb
index 1777b72cbf5..28803db924a 100644
--- a/spec/features/projects/show/schema_markup_spec.rb
+++ b/spec/features/projects/show/schema_markup_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Projects > Show > Schema Markup' do
- let_it_be(:project) { create(:project, :repository, :public, :with_avatar, description: 'foobar', tag_list: 'tag1, tag2') }
+ let_it_be(:project) { create(:project, :repository, :public, :with_avatar, description: 'foobar', topic_list: 'topic1, topic2') }
it 'shows SoftwareSourceCode structured markup', :js do
visit project_path(project)
@@ -16,7 +16,7 @@ RSpec.describe 'Projects > Show > Schema Markup' do
expect(page).to have_selector('[itemprop="identifier"]', text: "Project ID: #{project.id}")
expect(page).to have_selector('[itemprop="description"]', text: project.description)
expect(page).to have_selector('[itemprop="license"]', text: project.repository.license.name)
- expect(find_all('[itemprop="keywords"]').map(&:text)).to match_array(project.tag_list.map(&:capitalize))
+ expect(find_all('[itemprop="keywords"]').map(&:text)).to match_array(project.topic_list.map(&:capitalize))
expect(page).to have_selector('[itemprop="about"]')
end
end
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index ffdfbb9fe81..613033373e8 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -12,6 +12,10 @@ RSpec.describe 'Projects > Show > Collaboration links', :js do
sign_in(user)
end
+ def find_new_menu_toggle
+ find('#js-onboarding-new-project-link')
+ end
+
context 'with developer user' do
before do
project.add_developer(user)
@@ -22,7 +26,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js do
# The navigation bar
page.within('.header-new') do
- find('.qa-new-menu-toggle').click
+ find_new_menu_toggle.click
aggregate_failures 'dropdown links in the navigation bar' do
expect(page).to have_link('New issue')
@@ -30,7 +34,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js do
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
end
- find('.qa-new-menu-toggle').click
+ find_new_menu_toggle.click
end
# The dropdown above the tree
@@ -56,7 +60,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js do
visit project_path(project)
page.within('.header-new') do
- find('.qa-new-menu-toggle').click
+ find_new_menu_toggle.click
aggregate_failures 'dropdown links' do
expect(page).not_to have_link('New issue')
@@ -64,7 +68,7 @@ RSpec.describe 'Projects > Show > Collaboration links', :js do
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
end
- find('.qa-new-menu-toggle').click
+ find_new_menu_toggle.click
end
expect(page).not_to have_selector('.qa-add-to-tree')
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 1350ecf6e75..2f7844ff615 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe 'User uses shortcuts', :js do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:user) { project.owner }
before do
- project.add_maintainer(user)
sign_in(user)
visit(project_path(project))
@@ -74,7 +74,7 @@ RSpec.describe 'User uses shortcuts', :js do
find('body').native.send_key('g')
find('body').native.send_key('p')
- expect(page).to have_active_navigation('Project')
+ expect(page).to have_active_navigation(project.name)
end
context 'when feature flag :sidebar_refactor is disabled' do
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
index 3d4d9a7ea96..cce38456df9 100644
--- a/spec/features/projects/user_views_empty_project_spec.rb
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe 'User views an empty project' do
- let(:project) { create(:project, :empty_repo) }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:user) { create(:user) }
shared_examples 'allowing push to default branch' do
it 'shows push-to-master instructions' do
@@ -14,17 +14,25 @@ RSpec.describe 'User views an empty project' do
end
end
- describe 'as a maintainer' do
+ context 'when user is a maintainer' do
before do
project.add_maintainer(user)
sign_in(user)
end
it_behaves_like 'allowing push to default branch'
+
+ it 'shows a link for inviting members and launches invite modal', :js do
+ visit project_path(project)
+
+ click_button 'Invite members'
+
+ expect(page).to have_content("You're inviting members to the")
+ end
end
- describe 'as an admin' do
- let(:user) { create(:user, :admin) }
+ context 'when user is an admin' do
+ let_it_be(:user) { create(:user, :admin) }
context 'when admin mode is enabled' do
before do
@@ -44,16 +52,17 @@ RSpec.describe 'User views an empty project' do
end
end
- describe 'as a developer' do
+ context 'when user is a developer' do
before do
project.add_developer(user)
sign_in(user)
end
- it 'does not show push-to-master instructions' do
+ it 'does not show push-to-master instructions nor invite members link', :aggregate_failures, :js do
visit project_path(project)
expect(page).not_to have_content('git push -u origin master')
+ expect(page).not_to have_button(text: 'Invite members')
end
end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_empty_spec.rb b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
index 1f460f39267..ea045ddb6a1 100644
--- a/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
@@ -75,7 +75,7 @@ RSpec.describe 'Project > User views empty wiki' do
context 'and Confluence is already enabled' do
before do
- create(:confluence_service, project: project)
+ create(:confluence_integration, project: project)
end
it_behaves_like 'empty wiki message', writable: true, issuable: true, confluence: false
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index 30b94495e3d..63ee89bd11f 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -3,15 +3,12 @@
require 'spec_helper'
RSpec.describe 'Projects > Wiki > User views wiki in project page' do
- let(:user) { create(:user) }
-
before do
- project.add_maintainer(user)
- sign_in(user)
+ sign_in(project.owner)
end
context 'when repository is disabled for project' do
- let(:project) do
+ let_it_be(:project) do
create(:project,
:wiki_repo,
:repository_disabled,
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index c18b0f2688b..2ac829d406c 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -128,23 +128,23 @@ RSpec.describe 'Project' do
end
it 'shows project topics' do
- project.update_attribute(:tag_list, 'topic1')
+ project.update_attribute(:topic_list, 'topic1')
visit path
expect(page).to have_css('.home-panel-topic-list')
- expect(page).to have_link('Topic1', href: explore_projects_path(tag: 'topic1'))
+ expect(page).to have_link('Topic1', href: explore_projects_path(topic: 'topic1'))
end
- it 'shows up to 3 project tags' do
- project.update_attribute(:tag_list, 'topic1, topic2, topic3, topic4')
+ it 'shows up to 3 project topics' do
+ project.update_attribute(:topic_list, 'topic1, topic2, topic3, topic4')
visit path
expect(page).to have_css('.home-panel-topic-list')
- expect(page).to have_link('Topic1', href: explore_projects_path(tag: 'topic1'))
- expect(page).to have_link('Topic2', href: explore_projects_path(tag: 'topic2'))
- expect(page).to have_link('Topic3', href: explore_projects_path(tag: 'topic3'))
+ expect(page).to have_link('Topic1', href: explore_projects_path(topic: 'topic1'))
+ expect(page).to have_link('Topic2', href: explore_projects_path(topic: 'topic2'))
+ expect(page).to have_link('Topic3', href: explore_projects_path(topic: 'topic3'))
expect(page).to have_content('+ 1 more')
end
end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 207b74c990a..6fbed21acdb 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -21,13 +21,28 @@ RSpec.describe 'Protected Branches', :js do
expect(ProtectedBranch.count).to eq(1)
end
- it 'does not allow developer to removes protected branch' do
+ it 'does not allow developer to remove protected branch' do
visit project_branches_path(project)
find('input[data-testid="branch-search"]').set('fix')
find('input[data-testid="branch-search"]').native.send_keys(:enter)
- expect(page).to have_css('.btn-danger.disabled')
+ expect(page).to have_button('Only a project maintainer or owner can delete a protected branch', disabled: true)
+ end
+
+ context 'when feature flag :delete_branch_confirmation_modals is disabled' do
+ before do
+ stub_feature_flags(delete_branch_confirmation_modals: false)
+ end
+
+ it 'does not allow developer to remove protected branch' do
+ visit project_branches_path(project)
+
+ find('input[data-testid="branch-search"]').set('fix')
+ find('input[data-testid="branch-search"]').native.send_keys(:enter)
+
+ expect(page).to have_selector('button[data-testid="remove-protected-branch"][disabled]')
+ end
end
end
end
@@ -52,17 +67,44 @@ RSpec.describe 'Protected Branches', :js do
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
- page.find('[data-target="#modal-delete-branch"]').click
- expect(page).to have_css('.js-delete-branch[disabled]')
+ expect(page).to have_button('Delete protected branch', disabled: false)
+
+ page.find('.js-delete-branch-button').click
fill_in 'delete_branch_input', with: 'fix'
- click_link 'Delete protected branch'
+ click_button 'Yes, delete protected branch'
find('input[data-testid="branch-search"]').set('fix')
find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_content('No branches to show')
end
+
+ context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
+ before do
+ stub_feature_flags(delete_branch_confirmation_modals: false)
+ end
+
+ it 'removes branch after modal confirmation' do
+ visit project_branches_path(project)
+
+ find('input[data-testid="branch-search"]').set('fix')
+ find('input[data-testid="branch-search"]').native.send_keys(:enter)
+
+ expect(page).to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 1)
+ page.find('[data-target="#modal-delete-branch"]').click
+
+ expect(page).to have_css('.js-delete-branch[disabled]')
+ fill_in 'delete_branch_input', with: 'fix'
+ click_link 'Delete protected branch'
+
+ find('input[data-testid="branch-search"]').set('fix')
+ find('input[data-testid="branch-search"]').native.send_keys(:enter)
+
+ expect(page).to have_content('No branches to show')
+ end
+ end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index ee3717b3e42..094b31ba784 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'User searches for code' do
wait_for_requests
page.within('[data-testid="project-filter"]') do
- click_on(project.full_name)
+ click_on(project.name)
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 828e478d701..184f8ba0d36 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe 'User searches for issues', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
- click_on(project.full_name)
+ click_on(project.name)
end
search_for_issue(issue1.title)
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 7271716644b..32952a127d3 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -29,6 +29,11 @@ RSpec.describe 'User searches for merge requests', :js do
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
end
end
@@ -55,7 +60,7 @@ RSpec.describe 'User searches for merge requests', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
- click_on(project.full_name)
+ click_on(project.name)
end
search_for_mr(merge_request1.title)
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index f4df91dbc08..e81abb44ba5 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'User searches for milestones', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
- click_on(project.full_name)
+ click_on(project.name)
end
fill_in('dashboard_search', with: milestone1.title)
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 72bd1193fc9..8913f1fe9ee 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'User searches for wiki pages', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
- click_on(project.full_name)
+ click_on(project.name)
end
fill_in('dashboard_search', with: search_term)
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 4c42800cf05..c002d199b01 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'User uses header search field', :js do
expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
end
- context 'when clicking issues' do
+ context 'when clicking issues', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332317' do
let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
it 'shows assigned issues' do
@@ -75,7 +75,7 @@ RSpec.describe 'User uses header search field', :js do
end
end
- context 'when clicking merge requests' do
+ context 'when clicking merge requests', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332317' do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index 86017ca64c5..24f6c70e64c 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -33,10 +33,10 @@ RSpec.describe 'User uses search filters', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
- click_on(group_project.full_name)
+ click_on(group_project.name)
end
- expect(find('[data-testid="project-filter"]')).to have_content(group_project.full_name)
+ expect(find('[data-testid="project-filter"]')).to have_content(group_project.name)
end
context 'when the group filter is set' do
@@ -65,10 +65,10 @@ RSpec.describe 'User uses search filters', :js do
wait_for_requests
page.within('[data-testid="project-filter"]') do
- click_on(project.full_name)
+ click_on(project.name)
end
- expect(find('[data-testid="project-filter"]')).to have_content(project.full_name)
+ expect(find('[data-testid="project-filter"]')).to have_content(project.name)
end
context 'when the project filter is set' do
diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb
index 0f7ae06a6c5..0c06841399d 100644
--- a/spec/features/security/project/snippet/private_access_spec.rb
+++ b/spec/features/security/project/snippet/private_access_spec.rb
@@ -5,23 +5,25 @@ require 'spec_helper'
RSpec.describe "Private Project Snippets Access" do
include AccessMatchers
- let(:project) { create(:project, :private) }
-
- let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) }
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
- it { is_expected.to be_allowed_for(:owner).of(project) }
- it { is_expected.to be_allowed_for(:maintainer).of(project) }
- it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_allowed_for(:reporter).of(project) }
- it { is_expected.to be_allowed_for(:guest).of(project) }
- it { is_expected.to be_denied_for(:user) }
- it { is_expected.to be_denied_for(:external) }
- it { is_expected.to be_denied_for(:visitor) }
+
+ specify :aggregate_failures do
+ is_expected.to be_allowed_for(:owner).of(project)
+ is_expected.to be_allowed_for(:maintainer).of(project)
+ is_expected.to be_allowed_for(:developer).of(project)
+ is_expected.to be_allowed_for(:reporter).of(project)
+ is_expected.to be_allowed_for(:guest).of(project)
+ is_expected.to be_denied_for(:user)
+ is_expected.to be_denied_for(:external)
+ is_expected.to be_denied_for(:visitor)
+ end
end
describe "GET /:project_path/snippets/new" do
@@ -29,14 +31,17 @@ RSpec.describe "Private Project Snippets Access" do
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
- it { is_expected.to be_allowed_for(:owner).of(project) }
- it { is_expected.to be_allowed_for(:maintainer).of(project) }
- it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_allowed_for(:reporter).of(project) }
- it { is_expected.to be_denied_for(:guest).of(project) }
- it { is_expected.to be_denied_for(:user) }
- it { is_expected.to be_denied_for(:external) }
- it { is_expected.to be_denied_for(:visitor) }
+
+ specify :aggregate_failures do
+ is_expected.to be_allowed_for(:maintainer).of(project)
+ is_expected.to be_allowed_for(:owner).of(project)
+ is_expected.to be_allowed_for(:developer).of(project)
+ is_expected.to be_allowed_for(:reporter).of(project)
+ is_expected.to be_denied_for(:guest).of(project)
+ is_expected.to be_denied_for(:user)
+ is_expected.to be_denied_for(:external)
+ is_expected.to be_denied_for(:visitor)
+ end
end
describe "GET /:project_path/snippets/:id for a private snippet" do
@@ -44,14 +49,17 @@ RSpec.describe "Private Project Snippets Access" do
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
- it { is_expected.to be_allowed_for(:owner).of(project) }
- it { is_expected.to be_allowed_for(:maintainer).of(project) }
- it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_allowed_for(:reporter).of(project) }
- it { is_expected.to be_allowed_for(:guest).of(project) }
- it { is_expected.to be_denied_for(:user) }
- it { is_expected.to be_denied_for(:external) }
- it { is_expected.to be_denied_for(:visitor) }
+
+ specify :aggregate_failures do
+ is_expected.to be_allowed_for(:owner).of(project)
+ is_expected.to be_allowed_for(:maintainer).of(project)
+ is_expected.to be_allowed_for(:developer).of(project)
+ is_expected.to be_allowed_for(:reporter).of(project)
+ is_expected.to be_allowed_for(:guest).of(project)
+ is_expected.to be_denied_for(:user)
+ is_expected.to be_denied_for(:external)
+ is_expected.to be_denied_for(:visitor)
+ end
end
describe "GET /:project_path/snippets/:id/raw for a private snippet" do
@@ -59,13 +67,16 @@ RSpec.describe "Private Project Snippets Access" do
it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { is_expected.to be_allowed_for(:admin) }
it('is denied for admin when admin mode is disabled') { is_expected.to be_denied_for(:admin) }
- it { is_expected.to be_allowed_for(:owner).of(project) }
- it { is_expected.to be_allowed_for(:maintainer).of(project) }
- it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_allowed_for(:reporter).of(project) }
- it { is_expected.to be_allowed_for(:guest).of(project) }
- it { is_expected.to be_denied_for(:user) }
- it { is_expected.to be_denied_for(:external) }
- it { is_expected.to be_denied_for(:visitor) }
+
+ specify :aggregate_failures do
+ is_expected.to be_allowed_for(:owner).of(project)
+ is_expected.to be_allowed_for(:maintainer).of(project)
+ is_expected.to be_allowed_for(:developer).of(project)
+ is_expected.to be_allowed_for(:reporter).of(project)
+ is_expected.to be_allowed_for(:guest).of(project)
+ is_expected.to be_denied_for(:user)
+ is_expected.to be_denied_for(:external)
+ is_expected.to be_denied_for(:visitor)
+ end
end
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 56d2aaea203..0309df8f32a 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -220,6 +220,14 @@ RSpec.describe 'User page' do
expect(page).to have_content("Working hard!")
end
+ it 'shows the pronouns of the user if there was one' do
+ user.user_detail.update_column(:pronouns, 'they/them')
+
+ subject
+
+ expect(page).to have_content("(they/them)")
+ end
+
context 'signup disabled' do
it 'shows the sign in link' do
stub_application_setting(signup_enabled: false)
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 17a6abb99e0..a651a6c09c6 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -204,7 +204,7 @@ RSpec.describe 'Signup' do
expect { click_button 'Register' }.to change { User.count }.by(1)
expect(current_path).to eq users_almost_there_path
- expect(page).to have_content('Please check your email to confirm your account')
+ expect(page).to have_content("Please check your email (#{new_user.email}) to confirm your account")
confirm_email
diff --git a/spec/finders/ci/auth_job_finder_spec.rb b/spec/finders/ci/auth_job_finder_spec.rb
index 6cd58f5cd01..78827c9ddee 100644
--- a/spec/finders/ci/auth_job_finder_spec.rb
+++ b/spec/finders/ci/auth_job_finder_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
RSpec.describe Ci::AuthJobFinder do
- let_it_be(:job, reload: true) { create(:ci_build, status: :running) }
+ let_it_be(:user, reload: true) { create(:user) }
+ let_it_be(:job, reload: true) { create(:ci_build, status: :running, user: user) }
let(:token) { job.token }
@@ -55,10 +56,31 @@ RSpec.describe Ci::AuthJobFinder do
describe '#execute' do
subject(:execute) { finder.execute }
- before do
- job.success!
+ context 'when job is not running' do
+ before do
+ job.success!
+ end
+
+ it { is_expected.to be_nil }
end
- it { is_expected.to be_nil }
+ context 'when job is running', :request_store do
+ it 'sets ci_job_token_scope on the job user', :aggregate_failures do
+ expect(subject).to eq(job)
+ expect(subject.user).to be_from_ci_job_token
+ expect(subject.user.ci_job_token_scope.source_project).to eq(job.project)
+ end
+
+ context 'when feature flag ci_scoped_job_token is disabled' do
+ before do
+ stub_feature_flags(ci_scoped_job_token: false)
+ end
+
+ it 'does not set ci_job_token_scope on the job user' do
+ expect(subject).to eq(job)
+ expect(subject.user).not_to be_from_ci_job_token
+ end
+ end
+ end
end
end
diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb
index 4df026f2f5f..250a85dde30 100644
--- a/spec/finders/ci/runners_finder_spec.rb
+++ b/spec/finders/ci/runners_finder_spec.rb
@@ -51,23 +51,55 @@ RSpec.describe Ci::RunnersFinder do
end
context 'sort' do
+ let_it_be(:runner1) { create :ci_runner, created_at: '2018-07-12 07:00', contacted_at: 1.minute.ago }
+ let_it_be(:runner2) { create :ci_runner, created_at: '2018-07-12 08:00', contacted_at: 3.minutes.ago }
+ let_it_be(:runner3) { create :ci_runner, created_at: '2018-07-12 09:00', contacted_at: 2.minutes.ago }
+
+ subject do
+ described_class.new(current_user: admin, params: params).execute
+ end
+
+ shared_examples 'sorts by created_at descending' do
+ it 'sorts by created_at descending' do
+ is_expected.to eq [runner3, runner2, runner1]
+ end
+ end
+
context 'without sort param' do
- it 'sorts by created_at' do
- runner1 = create :ci_runner, created_at: '2018-07-12 07:00'
- runner2 = create :ci_runner, created_at: '2018-07-12 08:00'
- runner3 = create :ci_runner, created_at: '2018-07-12 09:00'
+ let(:params) { {} }
+
+ it_behaves_like 'sorts by created_at descending'
+ end
+
+ %w(created_date created_at_desc).each do |sort|
+ context "with sort param equal to #{sort}" do
+ let(:params) { { sort: sort } }
+
+ it_behaves_like 'sorts by created_at descending'
+ end
+ end
+
+ context 'with sort param equal to created_at_asc' do
+ let(:params) { { sort: 'created_at_asc' } }
+
+ it 'sorts by created_at ascending' do
+ is_expected.to eq [runner1, runner2, runner3]
+ end
+ end
+
+ context 'with sort param equal to contacted_asc' do
+ let(:params) { { sort: 'contacted_asc' } }
- expect(described_class.new(current_user: admin, params: {}).execute).to eq [runner3, runner2, runner1]
+ it 'sorts by contacted_at ascending' do
+ is_expected.to eq [runner2, runner3, runner1]
end
end
- context 'with sort param' do
- it 'sorts by specified attribute' do
- runner1 = create :ci_runner, contacted_at: 1.minute.ago
- runner2 = create :ci_runner, contacted_at: 3.minutes.ago
- runner3 = create :ci_runner, contacted_at: 2.minutes.ago
+ context 'with sort param equal to contacted_desc' do
+ let(:params) { { sort: 'contacted_desc' } }
- expect(described_class.new(current_user: admin, params: { sort: 'contacted_asc' }).execute).to eq [runner2, runner3, runner1]
+ it 'sorts by contacted_at descending' do
+ is_expected.to eq [runner1, runner3, runner2]
end
end
end
@@ -246,8 +278,8 @@ RSpec.describe Ci::RunnersFinder do
subject { described_class.new(current_user: user, group: group, params: params).sort_key }
context 'no params' do
- it 'returns created_date' do
- expect(subject).to eq('created_date')
+ it 'returns created_at_desc' do
+ expect(subject).to eq('created_at_desc')
end
end
diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb
index bad4c482bc6..e8648d131ff 100644
--- a/spec/finders/concerns/packages/finder_helper_spec.rb
+++ b/spec/finders/concerns/packages/finder_helper_spec.rb
@@ -113,41 +113,22 @@ RSpec.describe ::Packages::FinderHelper do
let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
- shared_examples 'handling all conditions' do
- where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do
- 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both packages'
- 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both packages'
- 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
- 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
- end
-
- with_them do
- before do
- project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
- subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
- project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
- group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
- end
-
- it_behaves_like params[:shared_example_name]
- end
- end
-
- context 'with packages_finder_helper_deploy_token enabled' do
- before do
- expect(group).not_to receive(:all_projects)
- end
-
- it_behaves_like 'handling all conditions'
+ where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do
+ 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both packages'
+ 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both packages'
+ 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
+ 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both packages'
end
- context 'with packages_finder_helper_deploy_token disabled' do
+ with_them do
before do
- stub_feature_flags(packages_finder_helper_deploy_token: false)
- expect(group).to receive(:all_projects).and_call_original
+ project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
+ subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
+ project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
+ group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
end
- it_behaves_like 'handling all conditions'
+ it_behaves_like params[:shared_example_name]
end
end
end
@@ -236,41 +217,22 @@ RSpec.describe ::Packages::FinderHelper do
let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
- shared_examples 'handling all conditions' do
- where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do
- 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both projects'
- 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both projects'
- 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both projects'
- 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both projects'
- end
-
- with_them do
- before do
- project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
- subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
- project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
- group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
- end
-
- it_behaves_like params[:shared_example_name]
- end
- end
-
- context 'with packages_finder_helper_deploy_token enabled' do
- before do
- expect(group).not_to receive(:all_projects)
- end
-
- it_behaves_like 'handling all conditions'
+ where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do
+ 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both projects'
+ 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both projects'
+ 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both projects'
+ 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both projects'
end
- context 'with packages_finder_helper_deploy_token disabled' do
+ with_them do
before do
- stub_feature_flags(packages_finder_helper_deploy_token: false)
- expect(group).to receive(:all_projects).and_call_original
+ project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
+ subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
+ project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
+ group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
end
- it_behaves_like 'handling all conditions'
+ it_behaves_like params[:shared_example_name]
end
end
end
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb
index b294f1117f5..bd03b254f40 100644
--- a/spec/finders/deployments_finder_spec.rb
+++ b/spec/finders/deployments_finder_spec.rb
@@ -81,16 +81,6 @@ RSpec.describe DeploymentsFinder do
it 'returns deployments with matched updated_at' do
is_expected.to match_array([deployment_2, deployment_1])
end
-
- context 'when deployments_finder_implicitly_enforce_ordering_for_updated_at_filter feature flag is disabled' do
- before do
- stub_feature_flags(deployments_finder_implicitly_enforce_ordering_for_updated_at_filter: false)
- end
-
- it 'returns deployments with matched updated_at' do
- is_expected.to match_array([deployment_1, deployment_2])
- end
- end
end
context 'when the environment name is specified' do
@@ -244,20 +234,6 @@ RSpec.describe DeploymentsFinder do
expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:updated_at].asc.to_sql)
expect(subject.order_values.second.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql)
end
-
- context 'when deployments_finder_implicitly_enforce_ordering_for_updated_at_filter feature flag is disabled' do
- before do
- stub_feature_flags(deployments_finder_implicitly_enforce_ordering_for_updated_at_filter: false)
- end
-
- it 'sorts by only one column' do
- expect(subject.order_values.size).to eq(1)
- end
-
- it 'sorts by `id`' do
- expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql)
- end
- end
end
context 'when filtering by finished time' do
diff --git a/spec/finders/feature_flags_finder_spec.rb b/spec/finders/feature_flags_finder_spec.rb
index 8744a186212..4faa6a62a1f 100644
--- a/spec/finders/feature_flags_finder_spec.rb
+++ b/spec/finders/feature_flags_finder_spec.rb
@@ -73,11 +73,11 @@ RSpec.describe FeatureFlagsFinder do
end
end
- context 'when new version flags are enabled' do
- let!(:feature_flag_3) { create(:operations_feature_flag, :new_version_flag, name: 'flag-c', project: project) }
+ context 'with a legacy flag' do
+ let!(:feature_flag_3) { create(:operations_feature_flag, :legacy_flag, name: 'flag-c', project: project) }
- it 'returns new and legacy flags' do
- is_expected.to eq([feature_flag_1, feature_flag_2, feature_flag_3])
+ it 'returns new flags' do
+ is_expected.to eq([feature_flag_1, feature_flag_2])
end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 27466ab563f..1c8c2af8e03 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -49,6 +49,11 @@ RSpec.describe IssuesFinder do
let(:expected_issuables) { [issue3, issue4] }
end
+ it_behaves_like 'assignee OR filter' do
+ let(:params) { { or: { assignee_id: [user.id, user2.id] } } }
+ let(:expected_issuables) { [issue1, issue2, issue3, issue5] }
+ end
+
context 'when assignee_id does not exist' do
it_behaves_like 'assignee NOT ID filter' do
let(:params) { { not: { assignee_id: -100 } } }
@@ -79,6 +84,11 @@ RSpec.describe IssuesFinder do
let(:expected_issuables) { [issue3, issue4] }
end
+ it_behaves_like 'assignee OR filter' do
+ let(:params) { { or: { assignee_username: [user2.username, user3.username] } } }
+ let(:expected_issuables) { [issue2, issue3] }
+ end
+
context 'when assignee_username does not exist' do
it_behaves_like 'assignee NOT username filter' do
before do
diff --git a/spec/finders/packages/helm/package_files_finder_spec.rb b/spec/finders/packages/helm/package_files_finder_spec.rb
new file mode 100644
index 00000000000..2b84fd2b2d2
--- /dev/null
+++ b/spec/finders/packages/helm/package_files_finder_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Packages::Helm::PackageFilesFinder do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:helm_package) { create(:helm_package, project: project1) }
+ let_it_be(:helm_package_file) { helm_package.package_files.first }
+ let_it_be(:debian_package) { create(:debian_package, project: project1) }
+
+ describe '#execute' do
+ let(:project) { project1 }
+ let(:channel) { 'stable' }
+ let(:params) { {} }
+
+ subject { described_class.new(project, channel, params).execute }
+
+ context 'with empty params' do
+ it { is_expected.to match_array([helm_package_file]) }
+ end
+
+ context 'with another project' do
+ let(:project) { project2 }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'with another channel' do
+ let(:channel) { 'staging' }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'with file_name' do
+ let(:params) { { file_name: helm_package_file.file_name } }
+
+ it { is_expected.to match_array([helm_package_file]) }
+ end
+
+ context 'with another file_name' do
+ let(:params) { { file_name: 'foobar.tgz' } }
+
+ it { is_expected.to match_array([]) }
+ end
+ end
+end
diff --git a/spec/finders/packages/maven/package_finder_spec.rb b/spec/finders/packages/maven/package_finder_spec.rb
index 13c603f1ec4..38fc3b7cce4 100644
--- a/spec/finders/packages/maven/package_finder_spec.rb
+++ b/spec/finders/packages/maven/package_finder_spec.rb
@@ -17,107 +17,89 @@ RSpec.describe ::Packages::Maven::PackageFinder do
group.add_developer(user)
end
- shared_examples 'Packages::Maven::PackageFinder examples' do
- describe '#execute!' do
- subject { finder.execute! }
+ describe '#execute!' do
+ subject { finder.execute! }
- shared_examples 'handling valid and invalid paths' do
- context 'with a valid path' do
- let(:param_path) { package.maven_metadatum.path }
+ shared_examples 'handling valid and invalid paths' do
+ context 'with a valid path' do
+ let(:param_path) { package.maven_metadatum.path }
- it { is_expected.to eq(package) }
- end
-
- context 'with an invalid path' do
- let(:param_path) { 'com/example/my-app/1.0-SNAPSHOT' }
-
- it 'raises an error' do
- expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
- end
- end
-
- context 'with an uninstallable package' do
- let(:param_path) { package.maven_metadatum.path }
-
- before do
- package.update_column(:status, 1)
- end
-
- it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
- end
+ it { is_expected.to eq(package) }
end
- context 'within the project' do
- let(:project_or_group) { project }
+ context 'with an invalid path' do
+ let(:param_path) { 'com/example/my-app/1.0-SNAPSHOT' }
- it_behaves_like 'handling valid and invalid paths'
- end
-
- context 'within a group' do
- let(:project_or_group) { group }
-
- it_behaves_like 'handling valid and invalid paths'
- end
-
- context 'across all projects' do
it 'raises an error' do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
end
- context 'versionless maven-metadata.xml package' do
- let_it_be(:sub_group1) { create(:group, parent: group) }
- let_it_be(:sub_group2) { create(:group, parent: group) }
- let_it_be(:project1) { create(:project, group: sub_group1) }
- let_it_be(:project2) { create(:project, group: sub_group2) }
- let_it_be(:project3) { create(:project, group: sub_group1) }
- let_it_be(:package_name) { 'foo' }
- let_it_be(:package1) { create(:maven_package, project: project1, name: package_name, version: nil) }
- let_it_be(:package2) { create(:maven_package, project: project2, name: package_name, version: nil) }
- let_it_be(:package3) { create(:maven_package, project: project3, name: package_name, version: nil) }
-
- let(:project_or_group) { group }
- let(:param_path) { package_name }
+ context 'with an uninstallable package' do
+ let(:param_path) { package.maven_metadatum.path }
before do
- sub_group1.add_developer(user)
- sub_group2.add_developer(user)
- # the package with the most recently published file should be returned
- create(:package_file, :xml, package: package2)
- end
-
- context 'without order by package file' do
- it { is_expected.to eq(package3) }
+ package.update_column(:status, 1)
end
- context 'with order by package file' do
- let(:param_order_by_package_file) { true }
-
- it { is_expected.to eq(package2) }
- end
+ it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
- end
- context 'when the maven_metadata_by_path_with_optimization_fence feature flag is off' do
- before do
- stub_feature_flags(maven_metadata_by_path_with_optimization_fence: false)
+ context 'within the project' do
+ let(:project_or_group) { project }
+
+ it_behaves_like 'handling valid and invalid paths'
end
- it_behaves_like 'Packages::Maven::PackageFinder examples'
- end
+ context 'within a group' do
+ let(:project_or_group) { group }
- context 'when the maven_metadata_by_path_with_optimization_fence feature flag is on' do
- before do
- stub_feature_flags(maven_metadata_by_path_with_optimization_fence: true)
+ it_behaves_like 'handling valid and invalid paths'
end
- it_behaves_like 'Packages::Maven::PackageFinder examples'
+ context 'across all projects' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
- it 'uses CTE in the query' do
- sql = described_class.new(user, group, path: package.maven_metadatum.path).send(:packages).to_sql
+ context 'versionless maven-metadata.xml package' do
+ let_it_be(:sub_group1) { create(:group, parent: group) }
+ let_it_be(:sub_group2) { create(:group, parent: group) }
+ let_it_be(:project1) { create(:project, group: sub_group1) }
+ let_it_be(:project2) { create(:project, group: sub_group2) }
+ let_it_be(:project3) { create(:project, group: sub_group1) }
+ let_it_be(:package_name) { 'foo' }
+ let_it_be(:package1) { create(:maven_package, project: project1, name: package_name, version: nil) }
+ let_it_be(:package2) { create(:maven_package, project: project2, name: package_name, version: nil) }
+ let_it_be(:package3) { create(:maven_package, project: project3, name: package_name, version: nil) }
+
+ let(:project_or_group) { group }
+ let(:param_path) { package_name }
+
+ before do
+ sub_group1.add_developer(user)
+ sub_group2.add_developer(user)
+ # the package with the most recently published file should be returned
+ create(:package_file, :xml, package: package2)
+ end
- expect(sql).to include('WITH "maven_metadata_by_path" AS')
+ context 'without order by package file' do
+ it { is_expected.to eq(package3) }
+ end
+
+ context 'with order by package file' do
+ let(:param_order_by_package_file) { true }
+
+ it { is_expected.to eq(package2) }
+ end
end
end
+
+ it 'uses CTE in the query' do
+ sql = described_class.new(user, group, path: package.maven_metadatum.path).send(:packages).to_sql
+
+ expect(sql).to include('WITH "maven_metadata_by_path" AS')
+ end
end
diff --git a/spec/finders/packages/pypi/package_finder_spec.rb b/spec/finders/packages/pypi/package_finder_spec.rb
index 7d9eb8a5cd1..f065bd21f6d 100644
--- a/spec/finders/packages/pypi/package_finder_spec.rb
+++ b/spec/finders/packages/pypi/package_finder_spec.rb
@@ -31,15 +31,7 @@ RSpec.describe Packages::Pypi::PackageFinder do
context 'within a group' do
let(:scope) { group }
- it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
-
- context 'user with access' do
- before do
- project.add_developer(user)
- end
-
- it { is_expected.to eq(package2) }
- end
+ it { is_expected.to eq(package2) }
end
end
end
diff --git a/spec/finders/pending_todos_finder_spec.rb b/spec/finders/pending_todos_finder_spec.rb
index b17915f0d59..f317d8b1633 100644
--- a/spec/finders/pending_todos_finder_spec.rb
+++ b/spec/finders/pending_todos_finder_spec.rb
@@ -3,8 +3,11 @@
require 'spec_helper'
RSpec.describe PendingTodosFinder do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:note) { create(:note) }
+
let(:users) { [user, user2] }
describe '#execute' do
@@ -30,20 +33,16 @@ RSpec.describe PendingTodosFinder do
end
it 'supports retrieving of todos for a specific todo target' do
- issue = create(:issue)
- note = create(:note)
todo = create(:todo, :pending, user: user, target: issue)
create(:todo, :pending, user: user, target: note)
- todos = described_class.new(users, target_id: issue.id).execute
+ todos = described_class.new(users, target_id: issue.id, target_type: 'Issue').execute
expect(todos).to eq([todo])
end
it 'supports retrieving of todos for a specific target type' do
- issue = create(:issue)
- note = create(:note)
todo = create(:todo, :pending, user: user, target: issue)
create(:todo, :pending, user: user, target: note)
@@ -61,5 +60,20 @@ RSpec.describe PendingTodosFinder do
expect(todos).to eq([todo])
end
+
+ it 'supports retrieving of todos for specific discussion' do
+ first_discussion_note = create(:discussion_note_on_issue, noteable: issue, project: issue.project)
+ note_2 = create(:note, discussion_id: first_discussion_note.discussion_id)
+ note_3 = create(:note, discussion_id: first_discussion_note.discussion_id)
+ todo1 = create(:todo, :pending, target: issue, note: note_2, user: note_2.author)
+ todo2 = create(:todo, :pending, target: issue, note: note_3, user: note_3.author)
+ create(:todo, :pending, note: note, user: user)
+ discussion = Discussion.lazy_find(first_discussion_note.discussion_id)
+ users = [note_2.author, note_3.author, user]
+
+ todos = described_class.new(users, discussion: discussion).execute
+
+ expect(todos).to contain_exactly(todo1, todo2)
+ end
end
end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 3d3e4183d4e..9b58da2e398 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -165,7 +165,7 @@ RSpec.describe Projects::Serverless::FunctionsFinder do
context 'has prometheus' do
let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) }
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
- let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_integrations_prometheus, cluster: cluster) }
let(:finder) { described_class.new(project) }
before do
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 364e5de4ece..b8b5e2c3bb7 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -137,9 +137,9 @@ RSpec.describe ProjectsFinder do
end
end
- describe 'filter by tags' do
+ describe 'filter by tags (deprecated)' do
before do
- public_project.tag_list = 'foo'
+ public_project.topic_list = 'foo'
public_project.save!
end
@@ -148,6 +148,37 @@ RSpec.describe ProjectsFinder do
it { is_expected.to eq([public_project]) }
end
+ describe 'filter by topics' do
+ before do
+ public_project.topic_list = 'foo, bar'
+ public_project.save!
+ end
+
+ context 'single topic' do
+ let(:params) { { topic: 'foo' } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ context 'multiple topics' do
+ let(:params) { { topic: 'bar, foo' } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ context 'one topic matches, other one does not' do
+ let(:params) { { topic: 'foo, xyz' } }
+
+ it { is_expected.to eq([]) }
+ end
+
+ context 'no matching topic' do
+ let(:params) { { topic: 'xyz' } }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+
describe 'filter by personal' do
let!(:personal_project) { create(:project, namespace: user.namespace) }
let(:params) { { personal: true } }
diff --git a/spec/finders/security/security_jobs_finder_spec.rb b/spec/finders/security/security_jobs_finder_spec.rb
index fa8253b96b5..3023c42341b 100644
--- a/spec/finders/security/security_jobs_finder_spec.rb
+++ b/spec/finders/security/security_jobs_finder_spec.rb
@@ -28,19 +28,19 @@ RSpec.describe Security::SecurityJobsFinder do
end
end
- context 'with combination of security jobs and license management jobs' do
+ context 'with combination of security jobs and license scanning jobs' do
let!(:sast_build) { create(:ci_build, :sast, pipeline: pipeline) }
let!(:container_scanning_build) { create(:ci_build, :container_scanning, pipeline: pipeline) }
let!(:dast_build) { create(:ci_build, :dast, pipeline: pipeline) }
let!(:secret_detection_build) { create(:ci_build, :secret_detection, pipeline: pipeline) }
- let!(:license_management_build) { create(:ci_build, :license_management, pipeline: pipeline) }
+ let!(:license_scanning_build) { create(:ci_build, :license_scanning, pipeline: pipeline) }
it 'returns only the security jobs' do
is_expected.to include(sast_build)
is_expected.to include(container_scanning_build)
is_expected.to include(dast_build)
is_expected.to include(secret_detection_build)
- is_expected.not_to include(license_management_build)
+ is_expected.not_to include(license_scanning_build)
end
end
end
diff --git a/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json b/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
new file mode 100644
index 00000000000..73904438ede
--- /dev/null
+++ b/spec/fixtures/api/schemas/analytics/cycle_analytics/summary.json
@@ -0,0 +1,21 @@
+{
+ "type": "array",
+ "items": {
+ "minItems": 2,
+ "maxItems": 3,
+ "type": "object",
+ "required": ["value", "title"],
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "unit": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+}
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index f6db336fe65..ce62655648b 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -42,8 +42,6 @@
"host": {"type": ["string", "null"]},
"port": {"type": ["integer", "514"]},
"protocol": {"type": ["integer", "0"]},
- "waf_log_enabled": {"type": ["boolean", "true"]},
- "cilium_log_enabled": {"type": ["boolean", "true"]},
"update_available": { "type": ["boolean", "null"] },
"can_uninstall": { "type": "boolean" },
"available_domains": {
diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json
index 56298cb124d..58d3832440c 100644
--- a/spec/fixtures/api/schemas/entities/issue_board.json
+++ b/spec/fixtures/api/schemas/entities/issue_board.json
@@ -18,7 +18,8 @@
"type": "object",
"properties": {
"id": { "type": "integer" },
- "path": { "type": "string" }
+ "path": { "type": "string" },
+ "path_with_namespace": { "type": "string" }
}
},
"milestone": {
diff --git a/spec/fixtures/api/schemas/external_validation.json b/spec/fixtures/api/schemas/external_validation.json
index 3ff71626cc0..280b77b221a 100644
--- a/spec/fixtures/api/schemas/external_validation.json
+++ b/spec/fixtures/api/schemas/external_validation.json
@@ -32,7 +32,9 @@
"id": { "type": "integer" },
"username": { "type": "string" },
"email": { "type": "string" },
- "created_at": { "type": ["string", "null"], "format": "date-time" }
+ "created_at": { "type": ["string", "null"], "format": "date-time" },
+ "current_sign_in_ip": { "type": ["string", "null"] },
+ "last_sign_in_ip": { "type": ["string", "null"] }
}
},
"pipeline": {
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index ca08e005e9d..3dfe6712b75 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -83,6 +83,7 @@
{ "$ref": "./package_conan_metadata.json" },
{ "$ref": "./package_maven_metadata.json" },
{ "$ref": "./package_nuget_metadata.json" },
+ { "$ref": "./package_pypi_metadata.json" },
{ "type": "null" }
]
},
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json b/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
new file mode 100644
index 00000000000..cecebe3a0e9
--- /dev/null
+++ b/spec/fixtures/api/schemas/graphql/packages/package_pypi_metadata.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id"],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "requiredPython": {
+ "type": "string"
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json
index c3a140c1bd7..11dfa131e88 100644
--- a/spec/fixtures/api/schemas/public_api/v4/board.json
+++ b/spec/fixtures/api/schemas/public_api/v4/board.json
@@ -15,6 +15,7 @@
"description",
"default_branch",
"tag_list",
+ "topics",
"ssh_url_to_repo",
"http_url_to_repo",
"web_url",
@@ -34,6 +35,7 @@
"description": { "type": ["string", "null"] },
"default_branch": { "type": ["string", "null"] },
"tag_list": { "type": "array" },
+ "topics": { "type": "array" },
"ssh_url_to_repo": { "type": "string" },
"http_url_to_repo": { "type": "string" },
"web_url": { "type": "string" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/label_basic.json b/spec/fixtures/api/schemas/public_api/v4/label_basic.json
index a4653c67ed2..a501bc2ec56 100644
--- a/spec/fixtures/api/schemas/public_api/v4/label_basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/label_basic.json
@@ -6,8 +6,7 @@
"color",
"description",
"description_html",
- "text_color",
- "remove_on_close"
+ "text_color"
],
"properties": {
"id": { "type": "integer" },
@@ -21,8 +20,7 @@
"text_color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
- },
- "remove_on_close": { "type": "boolean" }
+ }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/project.json b/spec/fixtures/api/schemas/public_api/v4/project.json
index 4a3149f2bdc..2f708538d96 100644
--- a/spec/fixtures/api/schemas/public_api/v4/project.json
+++ b/spec/fixtures/api/schemas/public_api/v4/project.json
@@ -15,6 +15,12 @@
"type": "string"
}
},
+ "topics": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"ssh_url_to_repo": { "type": "string" },
"http_url_to_repo": { "type": "string" },
"web_url": { "type": "string" },
@@ -37,7 +43,7 @@
},
"required": [
"id", "name", "name_with_namespace", "description", "path",
- "path_with_namespace", "created_at", "default_branch", "tag_list",
+ "path_with_namespace", "created_at", "default_branch", "tag_list", "topics",
"ssh_url_to_repo", "http_url_to_repo", "web_url", "readme_url", "avatar_url",
"star_count", "forks_count", "last_activity_at", "namespace"
],
diff --git a/spec/fixtures/bulk_imports/boards.ndjson b/spec/fixtures/bulk_imports/boards.ndjson
new file mode 100644
index 00000000000..a3e28584ff5
--- /dev/null
+++ b/spec/fixtures/bulk_imports/boards.ndjson
@@ -0,0 +1 @@
+{"id":173,"project_id":null,"created_at":"2020-02-11T14:35:51.561Z","updated_at":"2020-02-11T14:35:51.561Z","name":"first board","milestone_id":null,"group_id":4351,"weight":null,"lists":[{"id":189,"board_id":173,"label_id":271,"list_type":"label","position":0,"created_at":"2020-02-11T14:35:57.131Z","updated_at":"2020-02-11T14:35:57.131Z","user_id":null,"milestone_id":null,"max_issue_count":0,"max_issue_weight":0,"label":{"id":271,"title":"TSL","color":"#58796f","project_id":null,"created_at":"2019-11-20T17:02:20.541Z","updated_at":"2020-02-06T15:44:52.048Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[]},"board":{"id":173,"project_id":null,"created_at":"2020-02-11T14:35:51.561Z","updated_at":"2020-02-11T14:35:51.561Z","name":"hi","milestone_id":null,"group_id":4351,"weight":null}},{"id":190,"board_id":173,"label_id":272,"list_type":"label","position":1,"created_at":"2020-02-11T14:35:57.868Z","updated_at":"2020-02-11T14:35:57.868Z","user_id":null,"milestone_id":null,"max_issue_count":0,"max_issue_weight":0,"label":{"id":272,"title":"Sosync","color":"#110320","project_id":null,"created_at":"2019-11-20T17:02:20.532Z","updated_at":"2020-02-06T15:44:52.057Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[]},"board":{"id":173,"project_id":null,"created_at":"2020-02-11T14:35:51.561Z","updated_at":"2020-02-11T14:35:51.561Z","name":"hi","milestone_id":null,"group_id":4351,"weight":null}},{"id":188,"board_id":173,"label_id":null,"list_type":"closed","position":null,"created_at":"2020-02-11T14:35:51.593Z","updated_at":"2020-02-11T14:35:51.593Z","user_id":null,"milestone_id":null,"max_issue_count":0,"max_issue_weight":0}],"labels":[]}
diff --git a/spec/fixtures/bulk_imports/gz/boards.ndjson.gz b/spec/fixtures/bulk_imports/gz/boards.ndjson.gz
new file mode 100644
index 00000000000..294ef04db88
--- /dev/null
+++ b/spec/fixtures/bulk_imports/gz/boards.ndjson.gz
Binary files differ
diff --git a/spec/fixtures/bulk_imports/labels.ndjson.gz b/spec/fixtures/bulk_imports/gz/labels.ndjson.gz
index 6bb10a53346..6bb10a53346 100644
--- a/spec/fixtures/bulk_imports/labels.ndjson.gz
+++ b/spec/fixtures/bulk_imports/gz/labels.ndjson.gz
Binary files differ
diff --git a/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz b/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz
new file mode 100644
index 00000000000..f959cd7a0bd
--- /dev/null
+++ b/spec/fixtures/bulk_imports/gz/milestones.ndjson.gz
Binary files differ
diff --git a/spec/fixtures/bulk_imports/labels.ndjson b/spec/fixtures/bulk_imports/labels.ndjson
new file mode 100644
index 00000000000..c7d31d7cbbe
--- /dev/null
+++ b/spec/fixtures/bulk_imports/labels.ndjson
@@ -0,0 +1 @@
+{"id":111,"title":"Label 1","color":"#6699cc","project_id":null,"created_at":"2021-04-15T07:15:08.063Z","updated_at":"2021-04-15T07:15:08.063Z","template":false,"description":"Label 1","group_id":107,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
diff --git a/spec/fixtures/bulk_imports/milestones.ndjson b/spec/fixtures/bulk_imports/milestones.ndjson
new file mode 100644
index 00000000000..40523f276e7
--- /dev/null
+++ b/spec/fixtures/bulk_imports/milestones.ndjson
@@ -0,0 +1,5 @@
+{"id":7642,"title":"v4.0","project_id":null,"description":"Et laudantium enim omnis ea reprehenderit iure.","due_date":null,"created_at":"2019-11-20T17:02:14.336Z","updated_at":"2019-11-20T17:02:14.336Z","state":"closed","iid":5,"start_date":null,"group_id":4351}
+{"id":7641,"title":"v3.0","project_id":null,"description":"Et repellat culpa nemo consequatur ut reprehenderit.","due_date":null,"created_at":"2019-11-20T17:02:14.323Z","updated_at":"2019-11-20T17:02:14.323Z","state":"active","iid":4,"start_date":null,"group_id":4351}
+{"id":7640,"title":"v2.0","project_id":null,"description":"Velit cupiditate est neque voluptates iste rem sunt.","due_date":null,"created_at":"2019-11-20T17:02:14.309Z","updated_at":"2019-11-20T17:02:14.309Z","state":"active","iid":3,"start_date":null,"group_id":4351}
+{"id":7639,"title":"v1.0","project_id":null,"description":"Amet velit repellat ut rerum aut cum.","due_date":null,"created_at":"2019-11-20T17:02:14.296Z","updated_at":"2019-11-20T17:02:14.296Z","state":"active","iid":2,"start_date":null,"group_id":4351}
+{"id":7638,"title":"v0.0","project_id":null,"description":"Ea quia asperiores ut modi dolorem sunt non numquam.","due_date":null,"created_at":"2019-11-20T17:02:14.282Z","updated_at":"2019-11-20T17:02:14.282Z","state":"active","iid":1,"start_date":null,"group_id":4351}
diff --git a/spec/fixtures/config/redis_cache_config_with_env.yml b/spec/fixtures/config/redis_cache_config_with_env.yml
deleted file mode 100644
index 52fd5a06460..00000000000
--- a/spec/fixtures/config/redis_cache_config_with_env.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-test:
- url: <%= ENV['TEST_GITLAB_REDIS_CACHE_URL'] %>
diff --git a/spec/fixtures/config/redis_cache_new_format_host.yml b/spec/fixtures/config/redis_cache_new_format_host.yml
deleted file mode 100644
index 02b9e7384ac..00000000000
--- a/spec/fixtures/config/redis_cache_new_format_host.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-# redis://[:password@]host[:port][/db-number][?option=value]
-# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
-development:
- url: redis://:mynewpassword@localhost:6380/10
- sentinels:
- -
- host: localhost
- port: 26380 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26380 # point to sentinel, not to redis port
-test:
- url: redis://:mynewpassword@localhost:6380/10
- sentinels:
- -
- host: localhost
- port: 26380 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26380 # point to sentinel, not to redis port
-production:
- url: redis://:mynewpassword@localhost:6380/10
- sentinels:
- -
- host: replica1
- port: 26380 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26380 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_cache_new_format_socket.yml b/spec/fixtures/config/redis_cache_new_format_socket.yml
deleted file mode 100644
index 3634c550163..00000000000
--- a/spec/fixtures/config/redis_cache_new_format_socket.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-development:
- url: unix:/path/to/redis.cache.sock
-test:
- url: unix:/path/to/redis.cache.sock
-production:
- url: unix:/path/to/redis.cache.sock
diff --git a/spec/fixtures/config/redis_cache_old_format_host.yml b/spec/fixtures/config/redis_cache_old_format_host.yml
deleted file mode 100644
index 3609dcd022e..00000000000
--- a/spec/fixtures/config/redis_cache_old_format_host.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-# redis://[:password@]host[:port][/db-number][?option=value]
-# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
-development: redis://:mypassword@localhost:6380/10
-test: redis://:mypassword@localhost:6380/10
-production: redis://:mypassword@localhost:6380/10
diff --git a/spec/fixtures/config/redis_cache_old_format_socket.yml b/spec/fixtures/config/redis_cache_old_format_socket.yml
deleted file mode 100644
index 26fa0eda245..00000000000
--- a/spec/fixtures/config/redis_cache_old_format_socket.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-development: unix:/path/to/old/redis.cache.sock
-test: unix:/path/to/old/redis.cache.sock
-production: unix:/path/to/old/redis.cache.sock
diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml
index dc8d74c63fa..3bd91bcee6b 100644
--- a/spec/fixtures/config/redis_new_format_host.yml
+++ b/spec/fixtures/config/redis_new_format_host.yml
@@ -1,29 +1,29 @@
# redis://[:password@]host[:port][/db-number][?option=value]
# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
development:
- url: redis://:mynewpassword@localhost:6379/99
+ url: redis://:mynewpassword@development-host:6379/99
sentinels:
-
- host: localhost
+ host: development-replica1
port: 26379 # point to sentinel, not to redis port
-
- host: replica2
+ host: development-replica2
port: 26379 # point to sentinel, not to redis port
test:
- url: redis://:mynewpassword@localhost:6379/99
+ url: redis://:mynewpassword@test-host:6379/99
sentinels:
-
- host: localhost
+ host: test-replica1
port: 26379 # point to sentinel, not to redis port
-
- host: replica2
+ host: test-replica2
port: 26379 # point to sentinel, not to redis port
production:
- url: redis://:mynewpassword@localhost:6379/99
+ url: redis://:mynewpassword@production-host:6379/99
sentinels:
-
- host: replica1
+ host: production-replica1
port: 26379 # point to sentinel, not to redis port
-
- host: replica2
+ host: production-replica2
port: 26379 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_queues_config_with_env.yml b/spec/fixtures/config/redis_queues_config_with_env.yml
deleted file mode 100644
index d16a9d8a7f8..00000000000
--- a/spec/fixtures/config/redis_queues_config_with_env.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-test:
- url: <%= ENV['TEST_GITLAB_REDIS_QUEUES_URL'] %>
diff --git a/spec/fixtures/config/redis_queues_new_format_host.yml b/spec/fixtures/config/redis_queues_new_format_host.yml
deleted file mode 100644
index bd0d82a5066..00000000000
--- a/spec/fixtures/config/redis_queues_new_format_host.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-# redis://[:password@]host[:port][/db-number][?option=value]
-# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
-development:
- url: redis://:mynewpassword@localhost:6381/11
- sentinels:
- -
- host: localhost
- port: 26381 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26381 # point to sentinel, not to redis port
-test:
- url: redis://:mynewpassword@localhost:6381/11
- sentinels:
- -
- host: localhost
- port: 26381 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26381 # point to sentinel, not to redis port
-production:
- url: redis://:mynewpassword@localhost:6381/11
- sentinels:
- -
- host: replica1
- port: 26381 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26381 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_queues_new_format_socket.yml b/spec/fixtures/config/redis_queues_new_format_socket.yml
deleted file mode 100644
index b488d84d022..00000000000
--- a/spec/fixtures/config/redis_queues_new_format_socket.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-development:
- url: unix:/path/to/redis.queues.sock
-test:
- url: unix:/path/to/redis.queues.sock
-production:
- url: unix:/path/to/redis.queues.sock
diff --git a/spec/fixtures/config/redis_queues_old_format_host.yml b/spec/fixtures/config/redis_queues_old_format_host.yml
deleted file mode 100644
index 6531748a8d7..00000000000
--- a/spec/fixtures/config/redis_queues_old_format_host.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-# redis://[:password@]host[:port][/db-number][?option=value]
-# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
-development: redis://:mypassword@localhost:6381/11
-test: redis://:mypassword@localhost:6381/11
-production: redis://:mypassword@localhost:6381/11
diff --git a/spec/fixtures/config/redis_queues_old_format_socket.yml b/spec/fixtures/config/redis_queues_old_format_socket.yml
deleted file mode 100644
index 53f5db72758..00000000000
--- a/spec/fixtures/config/redis_queues_old_format_socket.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-development: unix:/path/to/old/redis.queues.sock
-test: unix:/path/to/old/redis.queues.sock
-production: unix:/path/to/old/redis.queues.sock
diff --git a/spec/fixtures/config/redis_shared_state_config_with_env.yml b/spec/fixtures/config/redis_shared_state_config_with_env.yml
deleted file mode 100644
index eab7203d0de..00000000000
--- a/spec/fixtures/config/redis_shared_state_config_with_env.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-test:
- url: <%= ENV['TEST_GITLAB_REDIS_SHARED_STATE_URL'] %>
diff --git a/spec/fixtures/config/redis_shared_state_new_format_host.yml b/spec/fixtures/config/redis_shared_state_new_format_host.yml
deleted file mode 100644
index 1c690567ae9..00000000000
--- a/spec/fixtures/config/redis_shared_state_new_format_host.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-# redis://[:password@]host[:port][/db-number][?option=value]
-# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
-development:
- url: redis://:mynewpassword@localhost:6382/12
- sentinels:
- -
- host: localhost
- port: 26382 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26382 # point to sentinel, not to redis port
-test:
- url: redis://:mynewpassword@localhost:6382/12
- sentinels:
- -
- host: localhost
- port: 26382 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26382 # point to sentinel, not to redis port
-production:
- url: redis://:mynewpassword@localhost:6382/12
- sentinels:
- -
- host: replica1
- port: 26382 # point to sentinel, not to redis port
- -
- host: replica2
- port: 26382 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_shared_state_new_format_socket.yml b/spec/fixtures/config/redis_shared_state_new_format_socket.yml
deleted file mode 100644
index 1b0e699729e..00000000000
--- a/spec/fixtures/config/redis_shared_state_new_format_socket.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-development:
- url: unix:/path/to/redis.shared_state.sock
-test:
- url: unix:/path/to/redis.shared_state.sock
-production:
- url: unix:/path/to/redis.shared_state.sock
diff --git a/spec/fixtures/config/redis_shared_state_old_format_host.yml b/spec/fixtures/config/redis_shared_state_old_format_host.yml
deleted file mode 100644
index fef5e768c5d..00000000000
--- a/spec/fixtures/config/redis_shared_state_old_format_host.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-# redis://[:password@]host[:port][/db-number][?option=value]
-# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
-development: redis://:mypassword@localhost:6382/12
-test: redis://:mypassword@localhost:6382/12
-production: redis://:mypassword@localhost:6382/12
diff --git a/spec/fixtures/config/redis_shared_state_old_format_socket.yml b/spec/fixtures/config/redis_shared_state_old_format_socket.yml
deleted file mode 100644
index 4746afbb5ef..00000000000
--- a/spec/fixtures/config/redis_shared_state_old_format_socket.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-development: unix:/path/to/old/redis.shared_state.sock
-test: unix:/path/to/old/redis.shared_state.sock
-production: unix:/path/to/old/redis.shared_state.sock
diff --git a/spec/fixtures/dns/a_rr.json b/spec/fixtures/dns/a_rr.json
new file mode 100644
index 00000000000..44ea739cec4
--- /dev/null
+++ b/spec/fixtures/dns/a_rr.json
@@ -0,0 +1,4 @@
+{
+ "desc": "A response of a `dig patroni-02-db-gstg.node.east-us-2.consul. A` query",
+ "payload": "JJSFAAABAAEAAAABEnBhdHJvbmktMDItZGItZ3N0ZwRub2RlCWVhc3QtdXMt\nMgZjb25zdWwAAAEAAcAMAAEAAQAAAAAABArgHWbADAAQAAEAAAAAABgXY29u\nc3VsLW5ldHdvcmstc2VnbWVudD0=\n"
+}
diff --git a/spec/fixtures/dns/a_with_aaaa_rr_in_additional_section.json b/spec/fixtures/dns/a_with_aaaa_rr_in_additional_section.json
new file mode 100644
index 00000000000..9d83d4caa2d
--- /dev/null
+++ b/spec/fixtures/dns/a_with_aaaa_rr_in_additional_section.json
@@ -0,0 +1,4 @@
+{
+ "desc": "A response of `dig google.com` query that contains AAAA records in additional section",
+ "payload": "YQiBgAABAAEADQAbBmdvb2dsZQNjb20AAAEAAcAMAAEAAQAAAOEABKzZFE7AEwACAAEAAAjmABQB\nagxndGxkLXNlcnZlcnMDbmV0AMATAAIAAQAACOYABAFrwDrAEwACAAEAAAjmAAQBYcA6wBMAAgAB\nAAAI5gAEAWXAOsATAAIAAQAACOYABAFmwDrAEwACAAEAAAjmAAQBbMA6wBMAAgABAAAI5gAEAWPA\nOsATAAIAAQAACOYABAFowDrAEwACAAEAAAjmAAQBbcA6wBMAAgABAAAI5gAEAWfAOsATAAIAAQAA\nCOYABAFkwDrAEwACAAEAAAjmAAQBacA6wBMAAgABAAAI5gAEAWLAOsBoAAEAAQAACOYABMAFBh7A\naAAcAAEAAAjmABAgAQUDqD4AAAAAAAAAAgAwwQgAAQABAAAI5gAEwCEOHsEIABwAAQAACOYAECAB\nBQMjHQAAAAAAAAACADDAqAABAAEAAAjmAATAGlwewKgAHAABAAAI5gAQIAEFA4PrAAAAAAAAAAAA\nMMDoAAEAAQAACOYABMAfUB7A6AAcAAEAAAjmABAgAQUAhW4AAAAAAAAAAAAwwHgAAQABAAAI5gAE\nwAxeHsB4ABwAAQAACOYAECABBQIcoQAAAAAAAAAAADDAiAABAAEAAAjmAATAIzMewIgAHAABAAAI\n5gAQIAEFA9QUAAAAAAAAAAAAMMDYAAEAAQAACOYABMAqXR7A2AAcAAEAAAjmABAgAQUD7qMAAAAA\nAAAAAAAwwLgAAQABAAAI5gAEwDZwHsC4ABwAAQAACOYAECABBQIIzAAAAAAAAAAAADDA+AABAAEA\nAAjmAATAK6wewPgAHAABAAAI5gAQIAEFAznBAAAAAAAAAAAAMMA4AAEAAQAACOYABMAwTx7AOAAc\nAAEAAAjmABAgAQUCcJQAAAAAAAAAAAAwwFgAAQABAAAI5gAEwDSyHsBYABwAAQAACOYAECABBQMN\nLQAAAAAAAAAAADDAmAABAAEAAAjmAATAKaIewJgAHAABAAAI5gAQIAEFANk3AAAAAAAAAAAAMMDI\nAAEAAQAACOYABMA3Ux7AyAAcAAEAAAjmABAgAQUBsfkAAAAAAAAAAAAwAAApEAAAAAAAAAA=\n"
+}
diff --git a/spec/fixtures/dns/aaaa_rr.json b/spec/fixtures/dns/aaaa_rr.json
new file mode 100644
index 00000000000..ae06e5255de
--- /dev/null
+++ b/spec/fixtures/dns/aaaa_rr.json
@@ -0,0 +1,4 @@
+{
+ "desc": "A response of `dig google.com AAAA` query",
+ "payload": "PA+BgAABAAEADQAbBmdvb2dsZQNjb20AABwAAcAMABwAAQAAASwAECoAFFBA\nDggKAAAAAAAAIA7AEwACAAEAAAFMABQBYgxndGxkLXNlcnZlcnMDbmV0AMAT\nAAIAAQAAAUwABAFtwEbAEwACAAEAAAFMAAQBY8BGwBMAAgABAAABTAAEAWbA\nRsATAAIAAQAAAUwABAFnwEbAEwACAAEAAAFMAAQBa8BGwBMAAgABAAABTAAE\nAWXARsATAAIAAQAAAUwABAFqwEbAEwACAAEAAAFMAAQBZMBGwBMAAgABAAAB\nTAAEAWnARsATAAIAAQAAAUwABAFhwEbAEwACAAEAAAFMAAQBbMBGwBMAAgAB\nAAABTAAEAWjARsD0AAEAAQAAAUwABMAFBh7A9AAcAAEAAAFMABAgAQUDqD4A\nAAAAAAAAAgAwwEQAAQABAAABTAAEwCEOHsBEABwAAQAAAUwAECABBQMjHQAA\nAAAAAAACADDAdAABAAEAAAFMAATAGlwewHQAHAABAAABTAAQIAEFA4PrAAAA\nAAAAAAAAMMDUAAEAAQAAAUwABMAfUB7A1AAcAAEAAAFMABAgAQUAhW4AAAAA\nAAAAAAAwwLQAAQABAAABTAAEwAxeHsC0ABwAAQAAAUwAECABBQIcoQAAAAAA\nAAAAADDAhAABAAEAAAFMAATAIzMewIQAHAABAAABTAAQIAEFA9QUAAAAAAAA\nAAAAMMCUAAEAAQAAAUwABMAqXR7AlAAcAAEAAAFMABAgAQUD7qMAAAAAAAAA\nAAAwwRQAAQABAAABTAAEwDZwHsEUABwAAQAAAUwAECABBQIIzAAAAAAAAAAA\nADDA5AABAAEAAAFMAATAK6wewOQAHAABAAABTAAQIAEFAznBAAAAAAAAAAAA\nMMDEAAEAAQAAAUwABMAwTx7AxAAcAAEAAAFMABAgAQUCcJQAAAAAAAAAAAAw\nwKQAAQABAAABTAAEwDSyHsCkABwAAQAAAUwAECABBQMNLQAAAAAAAAAAADDB\nBAABAAEAAAFMAATAKaIewQQAHAABAAABTAAQIAEFANk3AAAAAAAAAAAAMMBk\nAAEAAQAAAUwABMA3Ux7AZAAcAAEAAAFMABAgAQUBsfkAAAAAAAAAAAAwAAAp\nEAAAAAAAAAA=\n"
+}
diff --git a/spec/fixtures/dns/srv_with_a_rr_in_additional_section.json b/spec/fixtures/dns/srv_with_a_rr_in_additional_section.json
new file mode 100644
index 00000000000..97db149ad8e
--- /dev/null
+++ b/spec/fixtures/dns/srv_with_a_rr_in_additional_section.json
@@ -0,0 +1,4 @@
+{
+ "desc": "A response of `dig replica.patroni.service.consul. SRV` query that contains A records in additional section",
+ "payload": "y8uFAAABAAQAAAAIB3JlcGxpY2EHcGF0cm9uaQdzZXJ2aWNlBmNvbnN1bAAA\nIQABwAwAIQABAAAAAAAwAAEAAQAAEnBhdHJvbmktMDQtZGItZ3N0ZwRub2Rl\nCWVhc3QtdXMtMgZjb25zdWwAwAwAIQABAAAAAAAwAAEAAQAAEnBhdHJvbmkt\nMDUtZGItZ3N0ZwRub2RlCWVhc3QtdXMtMgZjb25zdWwAwAwAIQABAAAAAAAw\nAAEAAQAAEnBhdHJvbmktMDItZGItZ3N0ZwRub2RlCWVhc3QtdXMtMgZjb25z\ndWwAwAwAIQABAAAAAAAwAAEAAQAAEnBhdHJvbmktMDMtZGItZ3N0ZwRub2Rl\nCWVhc3QtdXMtMgZjb25zdWwAwEIAAQABAAAAAAAECuAdaMBCABAAAQAAAAAA\nGBdjb25zdWwtbmV0d29yay1zZWdtZW50PcB+AAEAAQAAAAAABArgHWnAfgAQ\nAAEAAAAAABgXY29uc3VsLW5ldHdvcmstc2VnbWVudD3AugABAAEAAAAAAAQK\n4B1mwLoAEAABAAAAAAAYF2NvbnN1bC1uZXR3b3JrLXNlZ21lbnQ9wPYAAQAB\nAAAAAAAECuAdZ8D2ABAAAQAAAAAAGBdjb25zdWwtbmV0d29yay1zZWdtZW50\nPQ==\n"
+}
diff --git a/spec/fixtures/helm/helm_list_v2_cilium_deployed.json.gz b/spec/fixtures/helm/helm_list_v2_cilium_deployed.json.gz
deleted file mode 100644
index a343356c95c..00000000000
--- a/spec/fixtures/helm/helm_list_v2_cilium_deployed.json.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_cilium_failed.json.gz b/spec/fixtures/helm/helm_list_v2_cilium_failed.json.gz
deleted file mode 100644
index f7faff2ca19..00000000000
--- a/spec/fixtures/helm/helm_list_v2_cilium_failed.json.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_cilium_missing.json.gz b/spec/fixtures/helm/helm_list_v2_cilium_missing.json.gz
deleted file mode 100644
index 20cac36287b..00000000000
--- a/spec/fixtures/helm/helm_list_v2_cilium_missing.json.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_empty_blob.json.gz b/spec/fixtures/helm/helm_list_v2_empty_blob.json.gz
deleted file mode 100644
index 5647f052c3b..00000000000
--- a/spec/fixtures/helm/helm_list_v2_empty_blob.json.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz
deleted file mode 100644
index bcbbba8dc00..00000000000
--- a/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz
deleted file mode 100644
index 0b39b42bdfa..00000000000
--- a/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz
deleted file mode 100644
index 20cac36287b..00000000000
--- a/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric.yml b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric.yml
index 90d395e1eda..f694e617320 100644
--- a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric.yml
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric.yml
@@ -12,6 +12,7 @@ milestone: "13.9"
introduced_by_url:
time_frame: 7d
data_source:
+data_category: Operational
distribution:
- ce
# Add here corresponding tiers
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml
index c51b5bf6e01..5cebfbcbad9 100644
--- a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml
@@ -12,6 +12,7 @@ milestone: "13.9"
introduced_by_url:
time_frame: 7d
data_source:
+data_category: Operational
distribution:
- ee
tier:
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml
index c1ed9783308..d448e7bf3f6 100644
--- a/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml
@@ -13,6 +13,7 @@ milestone: "13.9"
introduced_by_url:
time_frame: 7d
data_source:
+data_category: Operational
distribution:
- ce
- ee
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric.rb b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric.rb
new file mode 100644
index 00000000000..9816ff7c9eb
--- /dev/null
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Usage
+ module Metrics
+ module Instrumentations
+ class CountFooMetric < RedisHLLMetric
+ def value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
new file mode 100644
index 00000000000..bc7df779a58
--- /dev/null
+++ b/spec/fixtures/lib/generators/gitlab/usage_metric_generator/sample_metric_test.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountFooMetric do
+ it_behaves_like 'a correct instrumented metric value', {}, 1
+end
diff --git a/spec/fixtures/packages/debian/sample-dev_1.2.3~binary_amd64.deb b/spec/fixtures/packages/debian/sample-dev_1.2.3~binary_amd64.deb
index 70567d75265..68c7f2813fb 100644
--- a/spec/fixtures/packages/debian/sample-dev_1.2.3~binary_amd64.deb
+++ b/spec/fixtures/packages/debian/sample-dev_1.2.3~binary_amd64.deb
Binary files differ
diff --git a/spec/fixtures/packages/debian/sample/debian/control b/spec/fixtures/packages/debian/sample/debian/control
index 168d8e20d62..26d84e1c35d 100644
--- a/spec/fixtures/packages/debian/sample/debian/control
+++ b/spec/fixtures/packages/debian/sample/debian/control
@@ -14,7 +14,7 @@ Section: libdevel
Architecture: any
Multi-Arch: same
Depends: libsample0 (= ${binary:Version}), ${misc:Depends}
-Description: Some mostly empty developpement files
+Description: Some mostly empty development files
Used in GitLab tests.
.
Testing another paragraph.
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc
index 164d4755ed4..4a5755cd612 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.dsc
@@ -12,8 +12,8 @@ Package-List:
sample-dev deb libdevel optional arch=any
sample-udeb udeb libs optional arch=any
Checksums-Sha1:
- 5f8bba5574eb01ac3b1f5e2988e8c29307788236 864 sample_1.2.3~alpha2.tar.xz
+ c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz
Checksums-Sha256:
- b5a599e88e7cbdda3bde808160a21ba1dd1ec76b2ec8d4912aae769648d68362 864 sample_1.2.3~alpha2.tar.xz
+ 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz
Files:
- d79b34f58f61ff4ad696d9bd0b8daa68 864 sample_1.2.3~alpha2.tar.xz
+ d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz
index da70fd2094f..2bad3f065b8 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2.tar.xz
Binary files differ
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo
index dd63727ba31..36e2390b8c7 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.buildinfo
@@ -4,175 +4,182 @@ Binary: libsample0 sample-dev sample-udeb
Architecture: amd64 source
Version: 1.2.3~alpha2
Checksums-Md5:
- 3b0817804f669e16cdefac583ad88f0e 671 sample_1.2.3~alpha2.dsc
+ ceccb6bb3e45ce6550b24234d4023e0f 671 sample_1.2.3~alpha2.dsc
fb0842b21adc44207996296fe14439dd 1124 libsample0_1.2.3~alpha2_amd64.deb
- d2afbd28e4d74430d22f9504e18bfdf5 1164 sample-dev_1.2.3~binary_amd64.deb
+ 5fafc04dcae1525e1367b15413e5a5c7 1164 sample-dev_1.2.3~binary_amd64.deb
72b1dd7d98229e2fb0355feda1d3a165 736 sample-udeb_1.2.3~alpha2_amd64.udeb
Checksums-Sha1:
- 32ecbd674f0bfd310df68484d87752490685a8d6 671 sample_1.2.3~alpha2.dsc
+ 375ba20ea1789e1e90d469c3454ce49a431d0442 671 sample_1.2.3~alpha2.dsc
5248b95600e85bfe7f63c0dfce330a75f5777366 1124 libsample0_1.2.3~alpha2_amd64.deb
- f81e4f66c8c6bb899653a3340c157965ee69634f 1164 sample-dev_1.2.3~binary_amd64.deb
+ fcd5220b1501ec150ccf37f06e4da919a8612be4 1164 sample-dev_1.2.3~binary_amd64.deb
e42e8f2fe04ed1bb73b44a187674480d0e49dcba 736 sample-udeb_1.2.3~alpha2_amd64.udeb
Checksums-Sha256:
- 844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba 671 sample_1.2.3~alpha2.dsc
+ 81fc156ba937cdb6215362cc4bf6b8dc47be9b4253ba0f1a4ab10c7ea0c4c4e5 671 sample_1.2.3~alpha2.dsc
1c383a525bfcba619c7305ccd106d61db501a6bbaf0003bf8d0c429fbdb7fcc1 1124 libsample0_1.2.3~alpha2_amd64.deb
- 9fbeee2191ce4dab5288fad5ecac1bd369f58fef9a992a880eadf0caf25f086d 1164 sample-dev_1.2.3~binary_amd64.deb
+ b8aa8b73a14bc1e0012d4c5309770f5160a8ea7f9dfe6f45222ea6b8a3c35325 1164 sample-dev_1.2.3~binary_amd64.deb
2b0c152b3ab4cc07663350424de972c2b7621d69fe6df2e0b94308a191e4632f 736 sample-udeb_1.2.3~alpha2_amd64.udeb
Build-Origin: Debian
Build-Architecture: amd64
-Build-Date: Thu, 08 Oct 2020 15:15:24 +0200
+Build-Date: Fri, 14 May 2021 16:51:32 +0200
Build-Tainted-By:
- merged-usr-via-symlinks
+ merged-usr-via-aliased-dirs
usr-local-has-includes
usr-local-has-libraries
usr-local-has-programs
Installed-Build-Depends:
- autoconf (= 2.69-11.1),
- automake (= 1:1.16.2-4),
- autopoint (= 0.19.8.1-10),
- autotools-dev (= 20180224.1),
+ autoconf (= 2.69-14),
+ automake (= 1:1.16.3-2),
+ autopoint (= 0.21-4),
+ autotools-dev (= 20180224.1+nmu1),
base-files (= 11),
- base-passwd (= 3.5.47),
- bash (= 5.0-7),
- binutils (= 2.35.1-1),
- binutils-common (= 2.35.1-1),
- binutils-x86-64-linux-gnu (= 2.35.1-1),
- bsdextrautils (= 2.36-3+b1),
- bsdmainutils (= 12.1.7),
- bsdutils (= 1:2.36-3+b1),
- build-essential (= 12.8),
+ base-passwd (= 3.5.49),
+ bash (= 5.1-2+b1),
+ binutils (= 2.35.2-2),
+ binutils-common (= 2.35.2-2),
+ binutils-x86-64-linux-gnu (= 2.35.2-2),
+ bsdextrautils (= 2.36.1-7),
+ bsdmainutils (= 12.1.7+nmu3),
+ bsdutils (= 1:2.36.1-7),
+ build-essential (= 12.9),
bzip2 (= 1.0.8-4),
- calendar (= 12.1.7),
coreutils (= 8.32-4+b1),
- cpp (= 4:10.2.0-1),
- cpp-10 (= 10.2.0-9),
- cpp-9 (= 9.3.0-18),
- dash (= 0.5.10.2-7),
- debconf (= 1.5.74),
- debhelper (= 13.2.1),
+ cpp (= 4:10.2.1-1),
+ cpp-10 (= 10.2.1-6),
+ cpp-9 (= 9.3.0-22),
+ dash (= 0.5.11+git20200708+dd9ef66-5),
+ debconf (= 1.5.75),
+ debhelper (= 13.3.4),
debianutils (= 4.11.2),
- dh-autoreconf (= 19),
- dh-strip-nondeterminism (= 1.9.0-1),
- diffutils (= 1:3.7-3),
- dpkg (= 1.20.5),
- dpkg-dev (= 1.20.5),
- dwz (= 0.13-5),
- file (= 1:5.38-5),
- findutils (= 4.7.0-1),
- g++ (= 4:10.2.0-1),
- g++-10 (= 10.2.0-9),
- gcc (= 4:10.2.0-1),
- gcc-10 (= 10.2.0-9),
- gcc-10-base (= 10.2.0-9),
- gcc-9 (= 9.3.0-18),
- gcc-9-base (= 9.3.0-18),
- gettext (= 0.19.8.1-10),
- gettext-base (= 0.19.8.1-10),
- grep (= 3.4-1),
- groff-base (= 1.22.4-5),
- gzip (= 1.10-2),
+ dh-autoreconf (= 20),
+ dh-strip-nondeterminism (= 1.11.0-1),
+ diffutils (= 1:3.7-5),
+ dpkg (= 1.20.9),
+ dpkg-dev (= 1.20.9),
+ dwz (= 0.13+20210201-1),
+ file (= 1:5.39-3),
+ findutils (= 4.8.0-1),
+ g++ (= 4:10.2.1-1),
+ g++-10 (= 10.2.1-6),
+ gcc (= 4:10.2.1-1),
+ gcc-10 (= 10.2.1-6),
+ gcc-10-base (= 10.2.1-6),
+ gcc-9 (= 9.3.0-22),
+ gcc-9-base (= 9.3.0-22),
+ gettext (= 0.21-4),
+ gettext-base (= 0.21-4),
+ grep (= 3.6-1),
+ groff-base (= 1.22.4-6),
+ gzip (= 1.10-4),
hostname (= 3.23),
- init-system-helpers (= 1.58),
+ init-system-helpers (= 1.60),
intltool-debian (= 0.35.0+20060710.5),
- libacl1 (= 2.2.53-8),
+ libacl1 (= 2.2.53-10),
libarchive-zip-perl (= 1.68-1),
- libasan5 (= 9.3.0-18),
- libasan6 (= 10.2.0-9),
- libatomic1 (= 10.2.0-9),
- libattr1 (= 1:2.4.48-5),
- libaudit-common (= 1:2.8.5-3),
- libaudit1 (= 1:2.8.5-3+b1),
- libbinutils (= 2.35.1-1),
- libblkid1 (= 2.36-3+b1),
- libbsd0 (= 0.10.0-1),
+ libasan5 (= 9.3.0-22),
+ libasan6 (= 10.2.1-6),
+ libatomic1 (= 10.2.1-6),
+ libattr1 (= 1:2.4.48-6),
+ libaudit-common (= 1:3.0-2),
+ libaudit1 (= 1:3.0-2),
+ libbinutils (= 2.35.2-2),
+ libblkid1 (= 2.36.1-7),
libbz2-1.0 (= 1.0.8-4),
- libc-bin (= 2.31-3),
- libc-dev-bin (= 2.31-3),
- libc6 (= 2.31-3),
- libc6-dev (= 2.31-3),
- libcap-ng0 (= 0.7.9-2.2),
- libcc1-0 (= 10.2.0-9),
- libcroco3 (= 0.6.13-1),
- libcrypt-dev (= 1:4.4.17-1),
- libcrypt1 (= 1:4.4.17-1),
- libctf-nobfd0 (= 2.35.1-1),
- libctf0 (= 2.35.1-1),
- libdb5.3 (= 5.3.28+dfsg1-0.6),
- libdebconfclient0 (= 0.254),
- libdebhelper-perl (= 13.2.1),
- libdpkg-perl (= 1.20.5),
- libelf1 (= 0.181-1),
- libffi7 (= 3.3-4),
- libfile-stripnondeterminism-perl (= 1.9.0-1),
- libgcc-10-dev (= 10.2.0-9),
- libgcc-9-dev (= 9.3.0-18),
- libgcc-s1 (= 10.2.0-9),
- libgcrypt20 (= 1.8.6-2),
- libgdbm-compat4 (= 1.18.1-5.1),
- libgdbm6 (= 1.18.1-5.1),
- libglib2.0-0 (= 2.66.0-2),
- libgmp10 (= 2:6.2.0+dfsg-6),
- libgomp1 (= 10.2.0-9),
+ libc-bin (= 2.31-11),
+ libc-dev-bin (= 2.31-11),
+ libc6 (= 2.31-11),
+ libc6-dev (= 2.31-11),
+ libcap-ng0 (= 0.7.9-2.2+b1),
+ libcc1-0 (= 10.2.1-6),
+ libcom-err2 (= 1.46.2-1),
+ libcrypt-dev (= 1:4.4.18-2),
+ libcrypt1 (= 1:4.4.18-2),
+ libctf-nobfd0 (= 2.35.2-2),
+ libctf0 (= 2.35.2-2),
+ libdb5.3 (= 5.3.28+dfsg1-0.8),
+ libdebconfclient0 (= 0.257),
+ libdebhelper-perl (= 13.3.4),
+ libdpkg-perl (= 1.20.9),
+ libelf1 (= 0.183-1),
+ libfile-stripnondeterminism-perl (= 1.11.0-1),
+ libgcc-10-dev (= 10.2.1-6),
+ libgcc-9-dev (= 9.3.0-22),
+ libgcc-s1 (= 10.2.1-6),
+ libgcrypt20 (= 1.8.7-3),
+ libgdbm-compat4 (= 1.19-2),
+ libgdbm6 (= 1.19-2),
+ libgmp10 (= 2:6.2.1+dfsg-1),
+ libgomp1 (= 10.2.1-6),
libgpg-error0 (= 1.38-2),
- libicu67 (= 67.1-4),
- libisl22 (= 0.22.1-1),
- libitm1 (= 10.2.0-9),
- liblsan0 (= 10.2.0-9),
- liblz4-1 (= 1.9.2-2),
- liblzma5 (= 5.2.4-1+b1),
- libmagic-mgc (= 1:5.38-5),
- libmagic1 (= 1:5.38-5),
- libmount1 (= 2.36-3+b1),
+ libgssapi-krb5-2 (= 1.18.3-5),
+ libicu67 (= 67.1-6),
+ libisl23 (= 0.23-1),
+ libitm1 (= 10.2.1-6),
+ libk5crypto3 (= 1.18.3-5),
+ libkeyutils1 (= 1.6.1-2),
+ libkrb5-3 (= 1.18.3-5),
+ libkrb5support0 (= 1.18.3-5),
+ liblsan0 (= 10.2.1-6),
+ liblz4-1 (= 1.9.3-1),
+ liblzma5 (= 5.2.5-2),
+ libmagic-mgc (= 1:5.39-3),
+ libmagic1 (= 1:5.39-3),
+ libmount1 (= 2.36.1-7),
libmpc3 (= 1.2.0-1),
libmpfr6 (= 4.1.0-3),
- libpam-modules (= 1.3.1-5),
- libpam-modules-bin (= 1.3.1-5),
- libpam-runtime (= 1.3.1-5),
- libpam0g (= 1.3.1-5),
- libpcre2-8-0 (= 10.34-7),
+ libnsl-dev (= 1.3.0-2),
+ libnsl2 (= 1.3.0-2),
+ libpam-modules (= 1.4.0-7),
+ libpam-modules-bin (= 1.4.0-7),
+ libpam-runtime (= 1.4.0-7),
+ libpam0g (= 1.4.0-7),
+ libpcre2-8-0 (= 10.36-2),
libpcre3 (= 2:8.39-13),
- libperl5.30 (= 5.30.3-4),
+ libperl5.32 (= 5.32.1-4),
libpipeline1 (= 1.5.3-1),
- libquadmath0 (= 10.2.0-9),
- libseccomp2 (= 2.4.4-1),
- libselinux1 (= 3.1-2),
- libsigsegv2 (= 2.12-2),
- libsmartcols1 (= 2.36-3+b1),
- libstdc++-10-dev (= 10.2.0-9),
- libstdc++6 (= 10.2.0-9),
+ libquadmath0 (= 10.2.1-6),
+ libseccomp2 (= 2.5.1-1),
+ libselinux1 (= 3.1-3),
+ libsigsegv2 (= 2.13-1),
+ libsmartcols1 (= 2.36.1-7),
+ libssl1.1 (= 1.1.1k-1),
+ libstdc++-10-dev (= 10.2.1-6),
+ libstdc++6 (= 10.2.1-6),
libsub-override-perl (= 0.09-2),
- libsystemd0 (= 246.6-1),
- libtinfo6 (= 6.2+20200918-1),
- libtool (= 2.4.6-14),
- libtsan0 (= 10.2.0-9),
- libubsan1 (= 10.2.0-9),
+ libsystemd0 (= 247.3-5),
+ libtinfo6 (= 6.2+20201114-2),
+ libtirpc-common (= 1.3.1-1),
+ libtirpc-dev (= 1.3.1-1),
+ libtirpc3 (= 1.3.1-1),
+ libtool (= 2.4.6-15),
+ libtsan0 (= 10.2.1-6),
+ libubsan1 (= 10.2.1-6),
libuchardet0 (= 0.0.7-1),
- libudev1 (= 246.6-1),
+ libudev1 (= 247.3-5),
libunistring2 (= 0.9.10-4),
- libuuid1 (= 2.36-3+b1),
- libxml2 (= 2.9.10+dfsg-6),
- libzstd1 (= 1.4.5+dfsg-4),
- linux-libc-dev (= 5.8.10-1),
+ libuuid1 (= 2.36.1-7),
+ libxml2 (= 2.9.10+dfsg-6.3+b1),
+ libzstd1 (= 1.4.8+dfsg-2.1),
+ linux-libc-dev (= 5.10.28-1),
login (= 1:4.8.1-1),
lsb-base (= 11.1.0),
- m4 (= 1.4.18-4),
- make (= 4.3-4),
- man-db (= 2.9.3-2),
+ m4 (= 1.4.18-5),
+ make (= 4.3-4.1),
+ man-db (= 2.9.4-2),
mawk (= 1.3.4.20200120-2),
- ncal (= 12.1.7),
- ncurses-base (= 6.2+20200918-1),
- ncurses-bin (= 6.2+20200918-1),
- patch (= 2.7.6-6),
- perl (= 5.30.3-4),
- perl-base (= 5.30.3-4),
- perl-modules-5.30 (= 5.30.3-4),
- po-debconf (= 1.0.21),
+ ncal (= 12.1.7+nmu3),
+ ncurses-base (= 6.2+20201114-2),
+ ncurses-bin (= 6.2+20201114-2),
+ patch (= 2.7.6-7),
+ perl (= 5.32.1-4),
+ perl-base (= 5.32.1-4),
+ perl-modules-5.32 (= 5.32.1-4),
+ po-debconf (= 1.0.21+nmu1),
sed (= 4.7-1),
- sensible-utils (= 0.0.12+nmu1),
- sysvinit-utils (= 2.96-5),
- tar (= 1.30+dfsg-7),
- util-linux (= 2.36-3+b1),
- xz-utils (= 5.2.4-1+b1),
+ sensible-utils (= 0.0.14),
+ sysvinit-utils (= 2.96-7),
+ tar (= 1.34+dfsg-1),
+ util-linux (= 2.36.1-7),
+ xz-utils (= 5.2.5-2),
zlib1g (= 1:1.2.11.dfsg-2)
Environment:
DEB_BUILD_OPTIONS="parallel=8"
diff --git a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes
index 7a6bb78eb78..7aa4761c49c 100644
--- a/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes
+++ b/spec/fixtures/packages/debian/sample_1.2.3~alpha2_amd64.changes
@@ -10,30 +10,30 @@ Maintainer: John Doe <john.doe@example.com>
Changed-By: John Doe <john.doe@example.com>
Description:
libsample0 - Some mostly empty lib
- sample-dev - Some mostly empty developpement files
+ sample-dev - Some mostly empty development files
sample-udeb - Some mostly empty udeb (udeb)
Changes:
sample (1.2.3~alpha2) unstable; urgency=medium
.
* Initial release
Checksums-Sha1:
- 32ecbd674f0bfd310df68484d87752490685a8d6 671 sample_1.2.3~alpha2.dsc
- 5f8bba5574eb01ac3b1f5e2988e8c29307788236 864 sample_1.2.3~alpha2.tar.xz
+ 375ba20ea1789e1e90d469c3454ce49a431d0442 671 sample_1.2.3~alpha2.dsc
+ c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz
5248b95600e85bfe7f63c0dfce330a75f5777366 1124 libsample0_1.2.3~alpha2_amd64.deb
- f81e4f66c8c6bb899653a3340c157965ee69634f 1164 sample-dev_1.2.3~binary_amd64.deb
+ fcd5220b1501ec150ccf37f06e4da919a8612be4 1164 sample-dev_1.2.3~binary_amd64.deb
e42e8f2fe04ed1bb73b44a187674480d0e49dcba 736 sample-udeb_1.2.3~alpha2_amd64.udeb
- 0d47e899f3cc67a2253a4629456ff927e0db5c60 5280 sample_1.2.3~alpha2_amd64.buildinfo
+ 661f7507efa6fdd3763c95581d0baadb978b7ef5 5507 sample_1.2.3~alpha2_amd64.buildinfo
Checksums-Sha256:
- 844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba 671 sample_1.2.3~alpha2.dsc
- b5a599e88e7cbdda3bde808160a21ba1dd1ec76b2ec8d4912aae769648d68362 864 sample_1.2.3~alpha2.tar.xz
+ 81fc156ba937cdb6215362cc4bf6b8dc47be9b4253ba0f1a4ab10c7ea0c4c4e5 671 sample_1.2.3~alpha2.dsc
+ 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz
1c383a525bfcba619c7305ccd106d61db501a6bbaf0003bf8d0c429fbdb7fcc1 1124 libsample0_1.2.3~alpha2_amd64.deb
- 9fbeee2191ce4dab5288fad5ecac1bd369f58fef9a992a880eadf0caf25f086d 1164 sample-dev_1.2.3~binary_amd64.deb
+ b8aa8b73a14bc1e0012d4c5309770f5160a8ea7f9dfe6f45222ea6b8a3c35325 1164 sample-dev_1.2.3~binary_amd64.deb
2b0c152b3ab4cc07663350424de972c2b7621d69fe6df2e0b94308a191e4632f 736 sample-udeb_1.2.3~alpha2_amd64.udeb
- f9900d3c94e94b329232668dcbef3dba2d96c07147b15b6dc0533452e4dd8a43 5280 sample_1.2.3~alpha2_amd64.buildinfo
+ d0c169e9caa5b303a914b27b5adf69768fe6687d4925905b7d0cd9c0f9d4e56c 5507 sample_1.2.3~alpha2_amd64.buildinfo
Files:
- 3b0817804f669e16cdefac583ad88f0e 671 libs optional sample_1.2.3~alpha2.dsc
- d79b34f58f61ff4ad696d9bd0b8daa68 864 libs optional sample_1.2.3~alpha2.tar.xz
+ ceccb6bb3e45ce6550b24234d4023e0f 671 libs optional sample_1.2.3~alpha2.dsc
+ d5ca476e4229d135a88f9c729c7606c9 864 libs optional sample_1.2.3~alpha2.tar.xz
fb0842b21adc44207996296fe14439dd 1124 libs optional libsample0_1.2.3~alpha2_amd64.deb
- d2afbd28e4d74430d22f9504e18bfdf5 1164 libdevel optional sample-dev_1.2.3~binary_amd64.deb
+ 5fafc04dcae1525e1367b15413e5a5c7 1164 libdevel optional sample-dev_1.2.3~binary_amd64.deb
72b1dd7d98229e2fb0355feda1d3a165 736 libs optional sample-udeb_1.2.3~alpha2_amd64.udeb
- 4e085dd67c120ca967ec314f65770a42 5280 libs optional sample_1.2.3~alpha2_amd64.buildinfo
+ 12a5ac4f16ad75f8741327ac23b4c0d7 5507 libs optional sample_1.2.3~alpha2_amd64.buildinfo
diff --git a/spec/fixtures/product_intelligence/survey_response_schema.json b/spec/fixtures/product_intelligence/survey_response_schema.json
index 11454116d83..03d2d170137 100644
--- a/spec/fixtures/product_intelligence/survey_response_schema.json
+++ b/spec/fixtures/product_intelligence/survey_response_schema.json
@@ -3,7 +3,7 @@
"self": {
"vendor": "com.gitlab",
"name": "survey_response",
- "version": "1-0-0",
+ "version": "1-0-1",
"format": "jsonschema"
},
"type": "object",
@@ -47,6 +47,12 @@
"description": "Username",
"type": ["string", "null"],
"maxLength": 255
+ },
+ "onboarding_progress": {
+ "description": "Onboarding progress",
+ "type": ["integer", "null"],
+ "minimum": 0,
+ "maximum": 2147483647
}
}
}
diff --git a/spec/fixtures/whats_new/20201225_01_05.yml b/spec/fixtures/whats_new/20201225_01_05.yml
index 27c8f989b08..d707502af54 100644
--- a/spec/fixtures/whats_new/20201225_01_05.yml
+++ b/spec/fixtures/whats_new/20201225_01_05.yml
@@ -1,7 +1,7 @@
---
- title: bright and sunshinin' day
body: |
- ## bright and sunshinin' day
+ bright and sunshinin' [day](https://en.wikipedia.org/wiki/Day)
self-managed: true
gitlab-com: false
packages: ["Premium", "Ultimate"]
diff --git a/spec/frontend/__helpers__/mock_user_callout_dismisser.js b/spec/frontend/__helpers__/mock_user_callout_dismisser.js
new file mode 100644
index 00000000000..652f36028dc
--- /dev/null
+++ b/spec/frontend/__helpers__/mock_user_callout_dismisser.js
@@ -0,0 +1,16 @@
+/**
+ * Mock factory for the UserCalloutDismisser component.
+ * @param {slotProps} The slot props to pass to the default slot content.
+ * @returns {VueComponent}
+ */
+export const makeMockUserCalloutDismisser = ({
+ dismiss = () => {},
+ shouldShowCallout = true,
+} = {}) => ({
+ render() {
+ return this.$scopedSlots.default({
+ dismiss,
+ shouldShowCallout,
+ });
+ },
+});
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js
index a94cee84f74..2aae91f8a39 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper.js
@@ -1,5 +1,5 @@
import * as testingLibrary from '@testing-library/dom';
-import { createWrapper, WrapperArray, mount, shallowMount } from '@vue/test-utils';
+import { createWrapper, WrapperArray, ErrorWrapper, mount, shallowMount } from '@vue/test-utils';
import { isArray, upperFirst } from 'lodash';
const vNodeContainsText = (vnode, text) =>
@@ -81,14 +81,9 @@ export const extendedWrapper = (wrapper) => {
options,
);
- // Return VTU `ErrorWrapper` if element is not found
- // https://github.com/vuejs/vue-test-utils/blob/dev/packages/test-utils/src/error-wrapper.js
- // VTU does not expose `ErrorWrapper` so, as of now, this is the best way to
- // create an `ErrorWrapper`
+ // Element not found, return an `ErrorWrapper`
if (!elements.length) {
- const emptyElement = document.createElement('div');
-
- return createWrapper(emptyElement).find('testing-library-element-not-found');
+ return new ErrorWrapper(query);
}
return createWrapper(elements[0], this.options || {});
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
index dfe5a483223..3bb228f94b8 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
@@ -4,6 +4,7 @@ import {
shallowMount,
Wrapper as VTUWrapper,
WrapperArray as VTUWrapperArray,
+ ErrorWrapper as VTUErrorWrapper,
} from '@vue/test-utils';
import {
extendedWrapper,
@@ -195,7 +196,7 @@ describe('Vue test utils helpers', () => {
});
it('returns a VTU error wrapper', () => {
- expect(wrapper[findMethod](text, options).exists()).toBe(false);
+ expect(wrapper[findMethod](text, options)).toBeInstanceOf(VTUErrorWrapper);
});
});
});
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index 826fb820d9b..20e8bc059ec 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -7,7 +7,6 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import mockAlerts from 'jest/vue_shared/alert_details/mocks/alerts.json';
import AlertManagementTable from '~/alert_management/components/alert_management_table.vue';
import { visitUrl } from '~/lib/utils/url_utility';
-import AlertDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import defaultProvideValues from '../mocks/alerts_provide_config.json';
@@ -41,8 +40,7 @@ describe('AlertManagementTable', () => {
resolved: 11,
all: 26,
};
- const findDeprecationNotice = () =>
- wrapper.findComponent(AlertDeprecationWarning).findComponent(GlAlert);
+ const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
function mountComponent({ provide = {}, data = {}, loading = false, stubs = {} } = {}) {
wrapper = extendedWrapper(
@@ -239,19 +237,21 @@ describe('AlertManagementTable', () => {
expect(visitUrl).toHaveBeenCalledWith('/1527542/details', true);
});
- describe('deprecation notice', () => {
- it('shows the deprecation notice when available', () => {
- mountComponent({ provide: { hasManagedPrometheus: true } });
-
- expect(findDeprecationNotice().exists()).toBe(true);
- });
-
- it('hides the deprecation notice when not available', () => {
- mountComponent();
-
- expect(findDeprecationNotice().exists()).toBe(false);
- });
- });
+ it.each`
+ managedAlertsDeprecation | hasManagedPrometheus | isVisible
+ ${false} | ${false} | ${false}
+ ${false} | ${true} | ${true}
+ ${true} | ${false} | ${false}
+ ${true} | ${true} | ${false}
+ `(
+ 'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
+ ({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
+ mountComponent({
+ provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } },
+ });
+ expect(findDeprecationNotice().exists()).toBe(isVisible);
+ },
+ );
describe('alert issue links', () => {
beforeEach(() => {
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
index 85d21f231b1..3a374084dbc 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -11,6 +11,7 @@ exports[`Alert integration settings form default state should match the default
<form>
<gl-form-group-stub
class="gl-pl-0"
+ labeldescription=""
>
<gl-form-checkbox-stub
checked="true"
@@ -26,22 +27,24 @@ exports[`Alert integration settings form default state should match the default
class="col-8 col-md-9 gl-px-6"
label-for="alert-integration-settings-issue-template"
label-size="sm"
+ labeldescription=""
>
<label
class="gl-display-inline-flex"
for="alert-integration-settings-issue-template"
>
- Incident template (optional)
+ Incident template (optional).
<gl-link-stub
href="/help/user/project/description_templates#create-an-issue-template"
target="_blank"
>
- <gl-icon-stub
- name="question"
- size="12"
- />
+ <span
+ class="gl-font-weight-normal gl-pl-2"
+ >
+ Learn more.
+ </span>
</gl-link-stub>
</label>
@@ -75,6 +78,7 @@ exports[`Alert integration settings form default state should match the default
<gl-form-group-stub
class="gl-pl-0 gl-mb-5"
+ labeldescription=""
>
<gl-form-checkbox-stub>
<span>
@@ -85,6 +89,7 @@ exports[`Alert integration settings form default state should match the default
<gl-form-group-stub
class="gl-pl-0 gl-mb-5"
+ labeldescription=""
>
<gl-form-checkbox-stub
checked="true"
@@ -103,7 +108,7 @@ exports[`Alert integration settings form default state should match the default
icon=""
size="medium"
type="submit"
- variant="success"
+ variant="confirm"
>
Save changes
diff --git a/spec/frontend/incidents_settings/components/alerts_form_spec.js b/spec/frontend/alerts_settings/components/alerts_form_spec.js
index 2516e8afdfa..a045954dfb8 100644
--- a/spec/frontend/incidents_settings/components/alerts_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_form_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import AlertsSettingsForm from '~/incidents_settings/components/alerts_form.vue';
+import AlertsSettingsForm from '~/alerts_settings/components/alerts_form.vue';
describe('Alert integration settings form', () => {
let wrapper;
@@ -25,7 +25,6 @@ describe('Alert integration settings form', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
diff --git a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
index c43d78a1cf3..3ffbb7ab60a 100644
--- a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
@@ -80,7 +80,7 @@ describe('AlertIntegrationsList', () => {
const cell = finsStatusCell().at(0);
const activatedIcon = cell.find(GlIcon);
expect(cell.text()).toBe(i18n.status.enabled.name);
- expect(activatedIcon.attributes('name')).toBe('check-circle-filled');
+ expect(activatedIcon.attributes('name')).toBe('check');
expect(activatedIcon.attributes('title')).toBe(i18n.status.enabled.tooltip);
});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 595c3f1a289..1c4dde39585 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -95,6 +95,10 @@ describe('AlertsSettingsWrapper', () => {
},
provide: {
...provide,
+ alertSettings: {
+ templates: [],
+ },
+ service: {},
},
mocks: {
$apollo: {
@@ -129,12 +133,17 @@ describe('AlertsSettingsWrapper', () => {
wrapper = mount(AlertsSettingsWrapper, {
localVue,
apolloProvider: fakeApollo,
+ provide: {
+ alertSettings: {
+ templates: [],
+ },
+ service: {},
+ },
});
}
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
describe('template', () => {
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 139128e6d4a..f708d8c7728 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -116,6 +116,24 @@ describe('Api', () => {
});
});
});
+
+ describe('deleteProjectPackageFile', () => {
+ const packageFileId = 'package_file_id';
+
+ it('delete a package', () => {
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages/${packageId}/package_files/${packageFileId}`;
+
+ jest.spyOn(axios, 'delete');
+ mock.onDelete(expectedUrl).replyOnce(httpStatus.OK, true);
+
+ return Api.deleteProjectPackageFile(projectId, packageId, packageFileId).then(
+ ({ data }) => {
+ expect(data).toEqual(true);
+ expect(axios.delete).toHaveBeenCalledWith(expectedUrl);
+ },
+ );
+ });
+ });
});
describe('container registry', () => {
@@ -1503,33 +1521,55 @@ describe('Api', () => {
'Content-Type': 'application/json',
};
- describe('when usage data increment unique users is called with feature flag disabled', () => {
+ describe('when user is set', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: false };
+ window.gon.current_user_id = 1;
});
- it('returns null', () => {
- jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).replyOnce(httpStatus.OK, true);
+ describe('when usage data increment unique users is called with feature flag disabled', () => {
+ beforeEach(() => {
+ gon.features = { ...gon.features, usageDataApi: false };
+ });
- expect(axios.post).toHaveBeenCalledTimes(0);
- expect(Api.trackRedisHllUserEvent(event)).toEqual(null);
+ it('returns null and does not call the endpoint', () => {
+ jest.spyOn(axios, 'post');
+
+ const result = Api.trackRedisHllUserEvent(event);
+
+ expect(result).toEqual(null);
+ expect(axios.post).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('when usage data increment unique users is called', () => {
+ beforeEach(() => {
+ gon.features = { ...gon.features, usageDataApi: true };
+ });
+
+ it('resolves the Promise', () => {
+ jest.spyOn(axios, 'post');
+ mock.onPost(expectedUrl, { event }).replyOnce(httpStatus.OK, true);
+
+ return Api.trackRedisHllUserEvent(event).then(({ data }) => {
+ expect(data).toEqual(true);
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl, postData, { headers });
+ });
+ });
});
});
- describe('when usage data increment unique users is called', () => {
+ describe('when user is not set and feature flag enabled', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: true };
});
- it('resolves the Promise', () => {
+ it('returns null and does not call the endpoint', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl, { event }).replyOnce(httpStatus.OK, true);
- return Api.trackRedisHllUserEvent(event).then(({ data }) => {
- expect(data).toEqual(true);
- expect(axios.post).toHaveBeenCalledWith(expectedUrl, postData, { headers });
- });
+ const result = Api.trackRedisHllUserEvent(event);
+
+ expect(result).toEqual(null);
+ expect(axios.post).toHaveBeenCalledTimes(0);
});
});
});
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 03a28ce8001..cb71edd1238 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -104,7 +104,7 @@ describe('Batch comments draft preview item component', () => {
notes: [
{
author: {
- name: 'Author Name',
+ name: "Author 'Nick' Name",
},
},
],
@@ -114,7 +114,7 @@ describe('Batch comments draft preview item component', () => {
it('renders title', () => {
expect(vm.$el.querySelector('.review-preview-item-header-text').textContent).toContain(
- "Author Name's thread",
+ "Author 'Nick' Name's thread",
);
});
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 da19265ce82..b0e9e5dd00b 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
@@ -139,9 +139,14 @@ describe('Batch comments store actions', () => {
it('commits SET_BATCH_COMMENTS_DRAFTS with returned data', (done) => {
const commit = jest.fn();
+ const dispatch = jest.fn();
const context = {
getters,
commit,
+ dispatch,
+ state: {
+ drafts: [{ line_code: '123' }, { line_code: null, discussion_id: '1' }],
+ },
};
res = { id: 1 };
mock.onAny().reply(200, res);
@@ -150,6 +155,7 @@ describe('Batch comments store actions', () => {
.fetchDrafts(context)
.then(() => {
expect(commit).toHaveBeenCalledWith('SET_BATCH_COMMENTS_DRAFTS', { id: 1 });
+ expect(dispatch).toHaveBeenCalledWith('convertToDiscussion', '1', { root: true });
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/behaviors/shortcuts/keybindings_spec.js b/spec/frontend/behaviors/shortcuts/keybindings_spec.js
index 53ce06e78c6..3ad44a16ae1 100644
--- a/spec/frontend/behaviors/shortcuts/keybindings_spec.js
+++ b/spec/frontend/behaviors/shortcuts/keybindings_spec.js
@@ -5,6 +5,7 @@ import {
getCustomizations,
keybindingGroups,
TOGGLE_PERFORMANCE_BAR,
+ HIDE_APPEARING_CONTENT,
LOCAL_STORAGE_KEY,
WEB_IDE_COMMIT,
} from '~/behaviors/shortcuts/keybindings';
@@ -95,4 +96,14 @@ describe('~/behaviors/shortcuts/keybindings', () => {
expect(keysFor(TOGGLE_PERFORMANCE_BAR)).toEqual(['p b']);
});
});
+
+ describe('when tooltips or popovers are visible', () => {
+ beforeEach(() => {
+ setupCustomizations();
+ });
+
+ it('returns the default keybinding for the command', () => {
+ expect(keysFor(HIDE_APPEARING_CONTENT)).toEqual(['esc']);
+ });
+ });
});
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
new file mode 100644
index 00000000000..09633dc5d5d
--- /dev/null
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -0,0 +1,67 @@
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import TableContents from '~/blob/components/table_contents.vue';
+
+let wrapper;
+
+function createComponent() {
+ wrapper = shallowMount(TableContents);
+}
+
+async function setLoaded(loaded) {
+ document.querySelector('.blob-viewer').setAttribute('data-loaded', loaded);
+
+ await nextTick();
+}
+
+describe('Markdown table of contents component', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <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>
+ </div>
+ `);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('not loaded', () => {
+ it('does not populate dropdown', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
+ });
+ });
+
+ describe('loaded', () => {
+ it('populates dropdown', async () => {
+ createComponent();
+
+ await setLoaded(true);
+
+ const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
+
+ expect(dropdownItems.exists()).toBe(true);
+ expect(dropdownItems.length).toBe(4);
+ });
+
+ it('sets padding for dropdown items', async () => {
+ createComponent();
+
+ await setLoaded(true);
+
+ const dropdownLinks = wrapper.findAll('[data-testid="tableContentsLink"]');
+
+ 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');
+ });
+ });
+});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 36043b09636..15ea5d4eec4 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel } from '@gitlab/ui';
+import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { range } from 'lodash';
import Vuex from 'vuex';
@@ -63,6 +63,7 @@ describe('Board card component', () => {
},
stubs: {
GlLabel: true,
+ GlLoadingIcon: true,
},
mocks: {
$apollo: {
@@ -121,6 +122,10 @@ describe('Board card component', () => {
expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false);
});
+ it('does not render loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ });
+
describe('blocked', () => {
it('renders blocked icon if issue is blocked', async () => {
createWrapper({
@@ -399,4 +404,17 @@ describe('Board card component', () => {
});
});
});
+
+ describe('loading', () => {
+ it('renders loading icon', async () => {
+ createWrapper({
+ item: {
+ ...issue,
+ isLoading: true,
+ },
+ });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index bf39c3f3e42..76629c96f22 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -80,6 +80,7 @@ const createComponent = ({
rootPath: '/',
weightFeatureAvailable: false,
boardWeight: null,
+ canAdminList: true,
},
stubs: {
BoardCard,
@@ -181,12 +182,6 @@ describe('Board list component', () => {
});
});
- it('loads more issues after scrolling', () => {
- wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
-
- expect(actions.fetchItemsForList).toHaveBeenCalled();
- });
-
it('does not load issues if already loading', () => {
wrapper = createComponent({
state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
diff --git a/spec/frontend/boards/boards_util_spec.js b/spec/frontend/boards/boards_util_spec.js
index 0feb1411003..289905a1948 100644
--- a/spec/frontend/boards/boards_util_spec.js
+++ b/spec/frontend/boards/boards_util_spec.js
@@ -1,17 +1,103 @@
-import { transformNotFilters } from '~/boards/boards_util';
+import { filterVariables } from '~/boards/boards_util';
-describe('transformNotFilters', () => {
- const filters = {
- 'not[labelName]': ['label'],
- 'not[assigneeUsername]': 'assignee',
- };
-
- it('formats not filters, transforms epicId to fullEpicId', () => {
- const result = transformNotFilters(filters);
-
- expect(result).toEqual({
- labelName: ['label'],
- assigneeUsername: 'assignee',
+describe('filterVariables', () => {
+ it.each([
+ [
+ 'correctly processes array filter values',
+ {
+ filters: {
+ 'not[filterA]': ['val1', 'val2'],
+ },
+ expected: {
+ not: {
+ filterA: ['val1', 'val2'],
+ },
+ },
+ },
+ ],
+ [
+ "renames a filter if 'remap' method is available",
+ {
+ filters: {
+ filterD: 'some value',
+ },
+ expected: {
+ filterA: 'some value',
+ not: {},
+ },
+ },
+ ],
+ [
+ 'correctly processes a negated filter that supports negation',
+ {
+ filters: {
+ 'not[filterA]': 'some value 1',
+ 'not[filterB]': 'some value 2',
+ },
+ expected: {
+ not: {
+ filterA: 'some value 1',
+ },
+ },
+ },
+ ],
+ [
+ 'correctly removes an unsupported filter depending on issuableType',
+ {
+ issuableType: 'epic',
+ filters: {
+ filterA: 'some value 1',
+ filterE: 'some value 2',
+ },
+ expected: {
+ filterE: 'some value 2',
+ not: {},
+ },
+ },
+ ],
+ [
+ 'applies a transform when the filter value needs to be modified',
+ {
+ filters: {
+ filterC: 'abc',
+ 'not[filterC]': 'def',
+ },
+ expected: {
+ filterC: 'ABC',
+ not: {
+ filterC: 'DEF',
+ },
+ },
+ },
+ ],
+ ])('%s', (_, { filters, issuableType = 'issue', expected }) => {
+ const result = filterVariables({
+ filters,
+ issuableType,
+ filterInfo: {
+ filterA: {
+ negatedSupport: true,
+ },
+ filterB: {
+ negatedSupport: false,
+ },
+ filterC: {
+ negatedSupport: true,
+ transform: (val) => val.toUpperCase(),
+ },
+ filterD: {
+ remap: () => 'filterA',
+ },
+ filterE: {
+ negatedSupport: true,
+ },
+ },
+ filterFields: {
+ issue: ['filterA', 'filterB', 'filterC', 'filterD'],
+ epic: ['filterE'],
+ },
});
+
+ expect(result).toEqual(expected);
});
});
diff --git a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
index c000f300e4d..3fb0706fd10 100644
--- a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
+++ b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
-"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
+"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issue-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
<use href=\\"#issue-block\\"></use>
</svg>
<div class=\\"gl-popover\\">
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index ceafa6ead94..9a9ce7b8dc1 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -1,5 +1,6 @@
import { GlLabel } from '@gitlab/ui';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import BoardCard from '~/boards/components/board_card.vue';
@@ -12,8 +13,7 @@ describe('Board card', () => {
let store;
let mockActions;
- const localVue = createLocalVue();
- localVue.use(Vuex);
+ Vue.use(Vuex);
const createStore = ({ initialState = {} } = {}) => {
mockActions = {
@@ -41,14 +41,14 @@ describe('Board card', () => {
provide = {},
mountFn = shallowMount,
stubs = { BoardCardInner },
+ item = mockIssue,
} = {}) => {
wrapper = mountFn(BoardCard, {
- localVue,
stubs,
store,
propsData: {
list: mockLabelList,
- item: mockIssue,
+ item,
disabled: false,
index: 0,
...propsData,
@@ -72,6 +72,10 @@ describe('Board card', () => {
await wrapper.vm.$nextTick();
};
+ beforeEach(() => {
+ window.gon = { features: {} };
+ });
+
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -140,6 +144,10 @@ describe('Board card', () => {
});
describe('when using multi-select', () => {
+ beforeEach(() => {
+ window.gon = { features: { boardMultiSelect: true } };
+ });
+
it('should call vuex action "multiSelectBoardItem" with correct parameters', async () => {
await multiSelectCard();
@@ -151,4 +159,24 @@ describe('Board card', () => {
});
});
});
+
+ describe('when card is loading', () => {
+ it('card is disabled and user cannot drag', () => {
+ createStore();
+ mountComponent({ item: { ...mockIssue, isLoading: true } });
+
+ expect(wrapper.classes()).toContain('is-disabled');
+ expect(wrapper.classes()).not.toContain('user-can-drag');
+ });
+ });
+
+ describe('when card is not loading', () => {
+ it('user can drag', () => {
+ createStore();
+ mountComponent();
+
+ expect(wrapper.classes()).not.toContain('is-disabled');
+ expect(wrapper.classes()).toContain('user-can-drag');
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 01c99a02db2..10d739c65f5 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -1,13 +1,13 @@
import { GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
-import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
-import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
+import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
@@ -68,6 +68,9 @@ describe('BoardContentSidebar', () => {
iterations: {
loading: false,
},
+ attributesList: {
+ loading: false,
+ },
},
},
},
@@ -84,38 +87,41 @@ describe('BoardContentSidebar', () => {
});
it('confirms we render GlDrawer', () => {
- expect(wrapper.find(GlDrawer).exists()).toBe(true);
+ expect(wrapper.findComponent(GlDrawer).exists()).toBe(true);
});
it('does not render GlDrawer when isSidebarOpen is false', () => {
createStore({ mockGetters: { isSidebarOpen: () => false } });
createComponent();
- expect(wrapper.find(GlDrawer).exists()).toBe(false);
+ expect(wrapper.findComponent(GlDrawer).exists()).toBe(false);
});
it('applies an open attribute', () => {
- expect(wrapper.find(GlDrawer).props('open')).toBe(true);
+ expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true);
});
it('renders BoardSidebarLabelsSelect', () => {
- expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true);
+ expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true);
});
it('renders BoardSidebarTitle', () => {
- expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true);
+ expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true);
});
- it('renders BoardSidebarDueDate', () => {
- expect(wrapper.find(BoardSidebarDueDate).exists()).toBe(true);
+ it('renders SidebarDateWidget', () => {
+ expect(wrapper.findComponent(SidebarDateWidget).exists()).toBe(true);
});
it('renders BoardSidebarSubscription', () => {
- expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true);
+ expect(wrapper.findComponent(SidebarSubscriptionsWidget).exists()).toBe(true);
});
- it('renders BoardSidebarMilestoneSelect', () => {
- expect(wrapper.find(BoardSidebarMilestoneSelect).exists()).toBe(true);
+ it('renders SidebarDropdownWidget for milestones', () => {
+ expect(wrapper.findComponent(SidebarDropdownWidget).exists()).toBe(true);
+ expect(wrapper.findComponent(SidebarDropdownWidget).props('issuableAttribute')).toEqual(
+ 'milestone',
+ );
});
describe('when we emit close', () => {
@@ -128,7 +134,7 @@ describe('BoardContentSidebar', () => {
});
it('calls toggleBoardItem with correct parameters', async () => {
- wrapper.find(GlDrawer).vm.$emit('close');
+ wrapper.findComponent(GlDrawer).vm.$emit('close');
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), {
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index e27badca9de..6ac5d16e5a3 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -105,9 +105,9 @@ describe('BoardFilteredSearch', () => {
beforeEach(() => {
store = createStore();
- jest.spyOn(store, 'dispatch');
-
createComponent();
+
+ jest.spyOn(wrapper.vm, 'performSearch').mockImplementation();
});
it('sets the url params to the correct results', async () => {
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 24fcdd528d5..80d740458dc 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -9,14 +9,12 @@ import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql'
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
import { createStore } from '~/boards/stores';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
stripFinalUrlSegment: jest.requireActual('~/lib/utils/url_utility').stripFinalUrlSegment,
}));
-jest.mock('~/flash');
const currentBoard = {
id: 1,
@@ -194,9 +192,11 @@ describe('BoardForm', () => {
expect(visitUrl).toHaveBeenCalledWith('test-path');
});
- it('shows an error flash if GraphQL mutation fails', async () => {
+ it('shows a GlAlert if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.new });
+ jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
+
fillForm();
await waitForPromises();
@@ -205,7 +205,7 @@ describe('BoardForm', () => {
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalled();
+ expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
});
@@ -290,9 +290,11 @@ describe('BoardForm', () => {
expect(visitUrl).toHaveBeenCalledWith('test-path?group_by=epic');
});
- it('shows an error flash if GraphQL mutation fails', async () => {
+ it('shows a GlAlert if GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.edit });
+ jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
+
findInput().trigger('keyup.enter', { metaKey: true });
await waitForPromises();
@@ -301,7 +303,7 @@ describe('BoardForm', () => {
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalled();
+ expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
@@ -335,9 +337,11 @@ describe('BoardForm', () => {
expect(visitUrl).toHaveBeenCalledWith('root');
});
- it('shows an error flash if GraphQL mutation fails', async () => {
+ it('dispatches `setError` action when GraphQL mutation fails', async () => {
mutate = jest.fn().mockRejectedValue('Houston, we have a problem');
createComponent({ canAdminBoard: true, currentPage: formType.delete });
+ jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
+
findModal().vm.$emit('primary');
await waitForPromises();
@@ -346,7 +350,7 @@ describe('BoardForm', () => {
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalled();
+ expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_deprecated_spec.js b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
index fdc7cd2b1d4..db79e67fe78 100644
--- a/spec/frontend/boards/components/board_list_header_deprecated_spec.js
+++ b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
@@ -31,6 +31,7 @@ describe('Board List Header Component', () => {
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
+ currentUserId = 1,
} = {}) => {
const boardId = '1';
@@ -62,6 +63,7 @@ describe('Board List Header Component', () => {
},
provide: {
boardId,
+ currentUserId,
},
});
};
@@ -100,10 +102,12 @@ describe('Board List Header Component', () => {
});
});
- it('does render when logged out', () => {
- createComponent();
+ it('does not render when logged out', () => {
+ createComponent({
+ currentUserId: null,
+ });
- expect(findAddIssueButton().exists()).toBe(true);
+ expect(findAddIssueButton().exists()).toBe(false);
});
});
@@ -143,7 +147,6 @@ describe('Board List Header Component', () => {
it("when logged in it calls list update and doesn't set localStorage", () => {
jest.spyOn(List.prototype, 'update');
- window.gon.current_user_id = 1;
createComponent({ withLocalStorage: false });
@@ -158,7 +161,7 @@ describe('Board List Header Component', () => {
it("when logged out it doesn't call list update and sets localStorage", () => {
jest.spyOn(List.prototype, 'update');
- createComponent();
+ createComponent({ currentUserId: null });
findCaret().vm.$emit('click');
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index d2dfb4148b3..0abb00e0fa5 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -28,7 +28,7 @@ describe('Board List Header Component', () => {
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
- currentUserId = null,
+ currentUserId = 1,
} = {}) => {
const boardId = '1';
@@ -109,10 +109,12 @@ describe('Board List Header Component', () => {
});
});
- it('does render when logged out', () => {
- createComponent();
+ it('does not render when logged out', () => {
+ createComponent({
+ currentUserId: null,
+ });
- expect(findAddIssueButton().exists()).toBe(true);
+ expect(findAddIssueButton().exists()).toBe(false);
});
});
@@ -153,7 +155,9 @@ describe('Board List Header Component', () => {
});
it("when logged out it doesn't call list update and sets localStorage", async () => {
- createComponent();
+ createComponent({
+ currentUserId: null,
+ });
findCaret().vm.$emit('click');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
deleted file mode 100644
index 8fd178a0856..00000000000
--- a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import { GlDatepicker } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
-import { createStore } from '~/boards/stores';
-import createFlash from '~/flash';
-
-const TEST_DUE_DATE = '2020-02-20';
-const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020';
-const TEST_PARSED_DATE = new Date(2020, 1, 20);
-const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' };
-
-jest.mock('~/flash');
-
-describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => {
- let wrapper;
- let store;
-
- afterEach(() => {
- wrapper.destroy();
- store = null;
- wrapper = null;
- });
-
- const createWrapper = ({ dueDate = null } = {}) => {
- store = createStore();
- store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } };
- store.state.activeId = TEST_ISSUE.id;
-
- wrapper = shallowMount(BoardSidebarDueDate, {
- store,
- provide: {
- canUpdate: true,
- },
- stubs: {
- 'board-editable-item': BoardEditableItem,
- },
- });
- };
-
- const findDatePicker = () => wrapper.find(GlDatepicker);
- const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
- const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
-
- it('renders "None" when no due date is set', () => {
- createWrapper();
-
- expect(findCollapsed().text()).toBe('None');
- expect(findResetButton().exists()).toBe(false);
- });
-
- it('renders formatted due date with reset button when set', () => {
- createWrapper({ dueDate: TEST_DUE_DATE });
-
- expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
- expect(findResetButton().exists()).toBe(true);
- });
-
- describe('when due date is submitted', () => {
- beforeEach(async () => {
- createWrapper();
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].dueDate = TEST_DUE_DATE;
- });
- findDatePicker().vm.$emit('input', TEST_PARSED_DATE);
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders formatted due date with reset button', () => {
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
- expect(findResetButton().exists()).toBe(true);
- });
-
- it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({
- dueDate: TEST_DUE_DATE,
- projectPath: 'h/b',
- });
- });
- });
-
- describe('when due date is cleared', () => {
- beforeEach(async () => {
- createWrapper();
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].dueDate = null;
- });
- findDatePicker().vm.$emit('clear');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders "None"', () => {
- expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toBe('None');
- });
- });
-
- describe('when due date is resetted', () => {
- beforeEach(async () => {
- createWrapper({ dueDate: TEST_DUE_DATE });
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].dueDate = null;
- });
- findResetButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders "None"', () => {
- expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toBe('None');
- });
- });
-
- describe('when the mutation fails', () => {
- beforeEach(async () => {
- createWrapper({ dueDate: TEST_DUE_DATE });
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- throw new Error(['failed mutation']);
- });
- findDatePicker().vm.$emit('input', 'Invalid date');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders former issue due date', () => {
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
- expect(createFlash).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
index ad682774ee6..8992a5780f3 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_labels_select_spec.js
@@ -9,11 +9,8 @@ import {
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import { createStore } from '~/boards/stores';
-import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-jest.mock('~/flash');
-
const TEST_LABELS_PAYLOAD = TEST_LABELS.map((label) => ({ ...label, set: true }));
const TEST_LABELS_TITLES = TEST_LABELS.map((label) => label.title);
@@ -154,6 +151,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
jest.spyOn(wrapper.vm, 'setActiveBoardItemLabels').mockImplementation(() => {
throw new Error(['failed mutation']);
});
+ jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findLabelsSelect().vm.$emit('updateSelectedLabels', [{ id: '?' }]);
await wrapper.vm.$nextTick();
});
@@ -161,7 +159,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
it('collapses sidebar and renders former issue weight', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findLabelsTitles()).toEqual(TEST_LABELS_TITLES);
- expect(createFlash).toHaveBeenCalled();
+ expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
deleted file mode 100644
index 8706424a296..00000000000
--- a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
+++ /dev/null
@@ -1,178 +0,0 @@
-import { GlLoadingIcon, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
-import { createStore } from '~/boards/stores';
-import createFlash from '~/flash';
-
-const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, referencePath: 'h/b#2' };
-
-jest.mock('~/flash');
-
-describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => {
- let wrapper;
- let store;
-
- afterEach(() => {
- wrapper.destroy();
- store = null;
- wrapper = null;
- });
-
- const createWrapper = ({ milestone = null, loading = false } = {}) => {
- store = createStore();
- store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } };
- store.state.activeId = TEST_ISSUE.id;
-
- wrapper = shallowMount(BoardSidebarMilestoneSelect, {
- store,
- provide: {
- canUpdate: true,
- },
- data: () => ({
- milestones: [TEST_MILESTONE],
- }),
- stubs: {
- 'board-editable-item': BoardEditableItem,
- },
- mocks: {
- $apollo: {
- loading,
- },
- },
- });
- };
-
- const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
- const findLoader = () => wrapper.find(GlLoadingIcon);
- const findDropdown = () => wrapper.find(GlDropdown);
- const findBoardEditableItem = () => wrapper.find(BoardEditableItem);
- const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]');
- const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]');
- const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]');
-
- describe('when not editing', () => {
- it('opens the milestone dropdown on clicking edit', async () => {
- createWrapper();
- wrapper.vm.$refs.dropdown.show = jest.fn();
-
- await findBoardEditableItem().vm.$emit('open');
-
- expect(wrapper.vm.$refs.dropdown.show).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('when editing', () => {
- beforeEach(() => {
- createWrapper();
- jest.spyOn(wrapper.vm.$refs.sidebarItem, 'collapse');
- });
-
- it('collapses BoardEditableItem on clicking edit', async () => {
- await findBoardEditableItem().vm.$emit('close');
-
- expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
- });
-
- it('collapses BoardEditableItem on hiding dropdown', async () => {
- await findDropdown().vm.$emit('hide');
-
- expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
- });
- });
-
- it('renders "None" when no milestone is selected', () => {
- createWrapper();
-
- expect(findCollapsed().text()).toBe('None');
- });
-
- it('renders milestone title when set', () => {
- createWrapper({ milestone: TEST_MILESTONE });
-
- expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
- });
-
- it('shows loader while Apollo is loading', async () => {
- createWrapper({ milestone: TEST_MILESTONE, loading: true });
-
- expect(findLoader().exists()).toBe(true);
- });
-
- it('shows message when error or no milestones found', async () => {
- createWrapper();
-
- await wrapper.setData({ milestones: [] });
-
- expect(findNoMilestonesFoundItem().text()).toBe('No milestones found');
- });
-
- describe('when milestone is selected', () => {
- beforeEach(async () => {
- createWrapper();
-
- jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].milestone = TEST_MILESTONE;
- });
- findDropdownItem().vm.$emit('click');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders selected milestone', () => {
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
- });
-
- it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
- milestoneId: TEST_MILESTONE.id,
- projectPath: 'h/b',
- });
- });
- });
-
- describe('when milestone is set to "None"', () => {
- beforeEach(async () => {
- createWrapper({ milestone: TEST_MILESTONE });
-
- jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].milestone = null;
- });
- findUnsetMilestoneItem().vm.$emit('click');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders "None"', () => {
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toBe('None');
- });
-
- it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
- milestoneId: null,
- projectPath: 'h/b',
- });
- });
- });
-
- describe('when the mutation fails', () => {
- const testMilestone = { id: '1', title: 'Former milestone' };
-
- beforeEach(async () => {
- createWrapper({ milestone: testMilestone });
-
- jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
- throw new Error(['failed mutation']);
- });
- findDropdownItem().vm.$emit('click');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders former milestone', () => {
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toContain(testMilestone.title);
- expect(createFlash).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
index 7976e73ff2f..8847f626c1f 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -5,11 +5,8 @@ import Vuex from 'vuex';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import { createStore } from '~/boards/stores';
import * as types from '~/boards/stores/mutation_types';
-import createFlash from '~/flash';
import { mockActiveIssue } from '../../mock_data';
-jest.mock('~/flash.js');
-
Vue.use(Vuex);
describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
@@ -153,13 +150,15 @@ describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () =
jest.spyOn(wrapper.vm, 'setActiveItemSubscribed').mockImplementation(async () => {
throw new Error();
});
+ jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findToggle().trigger('click');
await wrapper.vm.$nextTick();
- expect(createFlash).toHaveBeenNthCalledWith(1, {
- message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage,
- });
+ expect(wrapper.vm.setError).toHaveBeenCalled();
+ expect(wrapper.vm.setError.mock.calls[0][0].message).toBe(
+ wrapper.vm.$options.i18n.updateSubscribedErrorMessage,
+ );
});
});
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
index 03924bfa8d3..74441e147cf 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
@@ -26,6 +26,7 @@ describe('BoardSidebarTimeTracker', () => {
store = createStore();
store.state.boardItems = {
1: {
+ iid: 1,
timeEstimate: 3600,
totalTimeSpent: 1800,
humanTimeEstimate: '1h',
@@ -46,12 +47,16 @@ describe('BoardSidebarTimeTracker', () => {
createComponent({ provide: { timeTrackingLimitToHours } });
expect(wrapper.find(IssuableTimeTracker).props()).toEqual({
- timeEstimate: 3600,
- timeSpent: 1800,
- humanTimeEstimate: '1h',
- humanTimeSpent: '30min',
limitToHours: timeTrackingLimitToHours,
showCollapsed: false,
+ issuableIid: '1',
+ fullPath: '',
+ initialTimeTracking: {
+ timeEstimate: 3600,
+ totalTimeSpent: 1800,
+ humanTimeEstimate: '1h',
+ humanTotalTimeSpent: '30min',
+ },
});
},
);
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 c8ccd4c88a5..4a8eda298f2 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -3,7 +3,6 @@ import { shallowMount } from '@vue/test-utils';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
-import createFlash from '~/flash';
const TEST_TITLE = 'New item title';
const TEST_ISSUE_A = {
@@ -19,8 +18,6 @@ const TEST_ISSUE_B = {
referencePath: 'h/b#2',
};
-jest.mock('~/flash');
-
describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
let wrapper;
let store;
@@ -168,6 +165,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
throw new Error(['failed mutation']);
});
+ jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findFormInput().vm.$emit('input', 'Invalid title');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await wrapper.vm.$nextTick();
@@ -176,7 +174,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
it('collapses sidebar and renders former item title', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
- expect(createFlash).toHaveBeenCalled();
+ expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/boards/project_select_deprecated_spec.js b/spec/frontend/boards/project_select_deprecated_spec.js
index 37f519ef5b9..4494de43083 100644
--- a/spec/frontend/boards/project_select_deprecated_spec.js
+++ b/spec/frontend/boards/project_select_deprecated_spec.js
@@ -5,7 +5,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import ProjectSelect from '~/boards/components/project_select_deprecated.vue';
import { ListType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
@@ -237,8 +237,10 @@ describe('ProjectSelect component', () => {
await searchForProject('foobar');
- expect(flash).toHaveBeenCalledTimes(1);
- expect(flash).toHaveBeenCalledWith('Something went wrong while fetching projects');
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Something went wrong while fetching projects',
+ });
});
describe('with non-empty search result', () => {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 09343b5704f..b28412f2127 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -15,6 +15,7 @@ import {
formatIssueInput,
formatIssue,
getMoveData,
+ updateListPosition,
} from '~/boards/boards_util';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
@@ -29,13 +30,13 @@ import {
mockIssue2,
rawIssue,
mockIssues,
- mockMilestone,
labels,
mockActiveIssue,
mockGroupProjects,
mockMoveIssueParams,
mockMoveState,
mockMoveData,
+ mockList,
} from '../mock_data';
jest.mock('~/flash');
@@ -70,27 +71,28 @@ describe('setFilters', () => {
[
'with correct filters as payload',
{
- filters: { labelName: 'label' },
- updatedFilters: { labelName: 'label', not: {} },
+ filters: { labelName: 'label', foobar: 'not-a-filter', search: 'quick brown fox' },
+ filterVariables: { labelName: 'label', search: 'quick brown fox', not: {} },
},
],
[
- 'and updates assigneeWildcardId',
+ "and use 'assigneeWildcardId' as filter variable for 'assigneId' param",
{
filters: { assigneeId: 'None' },
- updatedFilters: { assigneeWildcardId: 'NONE', not: {} },
+ filterVariables: { assigneeWildcardId: 'NONE', not: {} },
},
],
- ])('should commit mutation SET_FILTERS %s', (_, { filters, updatedFilters }) => {
+ ])('should commit mutation SET_FILTERS %s', (_, { filters, filterVariables }) => {
const state = {
filters: {},
+ issuableType: issuableTypes.issue,
};
testAction(
actions.setFilters,
filters,
state,
- [{ type: types.SET_FILTERS, payload: updatedFilters }],
+ [{ type: types.SET_FILTERS, payload: filterVariables }],
[],
);
});
@@ -373,6 +375,24 @@ describe('createIssueList', () => {
});
});
+describe('addList', () => {
+ const getters = {
+ getListByTitle: jest.fn().mockReturnValue(mockList),
+ };
+
+ it('should commit RECEIVE_ADD_LIST_SUCCESS mutation and dispatch fetchItemsForList action', () => {
+ testAction({
+ action: actions.addList,
+ payload: mockLists[1],
+ state: { ...getters },
+ expectedMutations: [
+ { type: types.RECEIVE_ADD_LIST_SUCCESS, payload: updateListPosition(mockLists[1]) },
+ ],
+ expectedActions: [{ type: 'fetchItemsForList', payload: { listId: mockList.id } }],
+ });
+ });
+});
+
describe('fetchLabels', () => {
it('should commit mutation RECEIVE_LABELS_SUCCESS on success', async () => {
const queryResponse = {
@@ -520,7 +540,8 @@ describe('toggleListCollapsed', () => {
describe('removeList', () => {
let state;
- const list = mockLists[0];
+ let getters;
+ const list = mockLists[1];
const listId = list.id;
const mutationVariables = {
mutation: destroyBoardListMutation,
@@ -534,6 +555,9 @@ describe('removeList', () => {
boardLists: mockListsById,
issuableType: issuableTypes.issue,
};
+ getters = {
+ getListByTitle: jest.fn().mockReturnValue(mockList),
+ };
});
afterEach(() => {
@@ -543,13 +567,15 @@ describe('removeList', () => {
it('optimistically deletes the list', () => {
const commit = jest.fn();
- actions.removeList({ commit, state }, listId);
+ actions.removeList({ commit, state, getters, dispatch: () => {} }, listId);
expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
});
it('keeps the updated list if remove succeeds', async () => {
const commit = jest.fn();
+ const dispatch = jest.fn();
+
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
destroyBoardList: {
@@ -558,17 +584,18 @@ describe('removeList', () => {
},
});
- await actions.removeList({ commit, state }, listId);
+ await actions.removeList({ commit, state, getters, dispatch }, listId);
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
expect(commit.mock.calls).toEqual([[types.REMOVE_LIST, listId]]);
+ expect(dispatch.mock.calls).toEqual([['fetchItemsForList', { listId: mockList.id }]]);
});
it('restores the list if update fails', async () => {
const commit = jest.fn();
jest.spyOn(gqlClient, 'mutate').mockResolvedValue(Promise.reject());
- await actions.removeList({ commit, state }, listId);
+ await actions.removeList({ commit, state, getters, dispatch: () => {} }, listId);
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
expect(commit.mock.calls).toEqual([
@@ -587,7 +614,7 @@ describe('removeList', () => {
},
});
- await actions.removeList({ commit, state }, listId);
+ await actions.removeList({ commit, state, getters, dispatch: () => {} }, listId);
expect(gqlClient.mutate).toHaveBeenCalledWith(mutationVariables);
expect(commit.mock.calls).toEqual([
@@ -649,6 +676,10 @@ describe('fetchItemsForList', () => {
state,
[
{
+ type: types.RESET_ITEMS_FOR_LIST,
+ payload: listId,
+ },
+ {
type: types.REQUEST_ITEMS_FOR_LIST,
payload: { listId, fetchNext: false },
},
@@ -671,6 +702,10 @@ describe('fetchItemsForList', () => {
state,
[
{
+ type: types.RESET_ITEMS_FOR_LIST,
+ payload: listId,
+ },
+ {
type: types.REQUEST_ITEMS_FOR_LIST,
payload: { listId, fetchNext: false },
},
@@ -1114,6 +1149,7 @@ describe('addListItem', () => {
listId: mockLists[0].id,
itemId: mockIssue.id,
atIndex: 0,
+ inProgress: false,
},
},
{ type: types.UPDATE_BOARD_ITEM, payload: mockIssue },
@@ -1244,8 +1280,9 @@ describe('addListNewIssue', () => {
type: 'addListItem',
payload: {
list: fakeList,
- item: formatIssue({ ...mockIssue, id: 'tmp' }),
+ item: formatIssue({ ...mockIssue, id: 'tmp', isLoading: true }),
position: 0,
+ inProgress: true,
},
},
{ type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } },
@@ -1286,8 +1323,9 @@ describe('addListNewIssue', () => {
type: 'addListItem',
payload: {
list: fakeList,
- item: formatIssue({ ...mockIssue, id: 'tmp' }),
+ item: formatIssue({ ...mockIssue, id: 'tmp', isLoading: true }),
position: 0,
+ inProgress: true,
},
},
{ type: 'removeListItem', payload: { listId: fakeList.id, itemId: 'tmp' } },
@@ -1348,57 +1386,6 @@ describe('setActiveIssueLabels', () => {
});
});
-describe('setActiveIssueDueDate', () => {
- const state = { boardItems: { [mockIssue.id]: mockIssue } };
- const getters = { activeBoardItem: mockIssue };
- const testDueDate = '2020-02-20';
- const input = {
- dueDate: testDueDate,
- projectPath: 'h/b',
- };
-
- it('should commit due date after setting the issue', (done) => {
- jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
- data: {
- updateIssue: {
- issue: {
- dueDate: testDueDate,
- },
- errors: [],
- },
- },
- });
-
- const payload = {
- itemId: getters.activeBoardItem.id,
- prop: 'dueDate',
- value: testDueDate,
- };
-
- testAction(
- actions.setActiveIssueDueDate,
- input,
- { ...state, ...getters },
- [
- {
- type: types.UPDATE_BOARD_ITEM_BY_ID,
- payload,
- },
- ],
- [],
- done,
- );
- });
-
- it('throws error if fails', async () => {
- jest
- .spyOn(gqlClient, 'mutate')
- .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
-
- await expect(actions.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error);
- });
-});
-
describe('setActiveItemSubscribed', () => {
const state = {
boardItems: {
@@ -1456,60 +1443,6 @@ describe('setActiveItemSubscribed', () => {
});
});
-describe('setActiveIssueMilestone', () => {
- const state = { boardItems: { [mockIssue.id]: mockIssue } };
- const getters = { activeBoardItem: mockIssue };
- const testMilestone = {
- ...mockMilestone,
- id: 'gid://gitlab/Milestone/1',
- };
- const input = {
- milestoneId: testMilestone.id,
- projectPath: 'h/b',
- };
-
- it('should commit milestone after setting the issue', (done) => {
- jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
- data: {
- updateIssue: {
- issue: {
- milestone: testMilestone,
- },
- errors: [],
- },
- },
- });
-
- const payload = {
- itemId: getters.activeBoardItem.id,
- prop: 'milestone',
- value: testMilestone,
- };
-
- testAction(
- actions.setActiveIssueMilestone,
- input,
- { ...state, ...getters },
- [
- {
- type: types.UPDATE_BOARD_ITEM_BY_ID,
- payload,
- },
- ],
- [],
- done,
- );
- });
-
- it('throws error if fails', async () => {
- jest
- .spyOn(gqlClient, 'mutate')
- .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
-
- await expect(actions.setActiveIssueMilestone({ getters }, input)).rejects.toThrow(Error);
- });
-});
-
describe('setActiveItemTitle', () => {
const state = {
boardItems: { [mockIssue.id]: mockIssue },
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index d89abcc79ae..5b38f04e77b 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -273,6 +273,53 @@ describe('Board Store Mutations', () => {
});
});
+ describe('RESET_ITEMS_FOR_LIST', () => {
+ it('should remove issues from boardItemsByListId state', () => {
+ const listId = 'gid://gitlab/List/1';
+ const boardItemsByListId = {
+ [listId]: [mockIssue.id],
+ };
+
+ state = {
+ ...state,
+ boardItemsByListId,
+ };
+
+ mutations[types.RESET_ITEMS_FOR_LIST](state, listId);
+
+ expect(state.boardItemsByListId[listId]).toEqual([]);
+ });
+ });
+
+ describe('REQUEST_ITEMS_FOR_LIST', () => {
+ const listId = 'gid://gitlab/List/1';
+ const boardItemsByListId = {
+ [listId]: [mockIssue.id],
+ };
+
+ it.each`
+ fetchNext | isLoading | isLoadingMore
+ ${true} | ${undefined} | ${true}
+ ${false} | ${true} | ${undefined}
+ `(
+ 'sets isLoading to $isLoading and isLoadingMore to $isLoadingMore when fetchNext is $fetchNext',
+ ({ fetchNext, isLoading, isLoadingMore }) => {
+ state = {
+ ...state,
+ boardItemsByListId,
+ listsFlags: {
+ [listId]: {},
+ },
+ };
+
+ mutations[types.REQUEST_ITEMS_FOR_LIST](state, { listId, fetchNext });
+
+ expect(state.listsFlags[listId].isLoading).toBe(isLoading);
+ expect(state.listsFlags[listId].isLoadingMore).toBe(isLoadingMore);
+ },
+ );
+ });
+
describe('RECEIVE_ITEMS_FOR_LIST_SUCCESS', () => {
it('updates boardItemsByListId and issues on state', () => {
const listIssues = {
diff --git a/spec/frontend/branches/components/delete_branch_button_spec.js b/spec/frontend/branches/components/delete_branch_button_spec.js
new file mode 100644
index 00000000000..acbc83a9bdc
--- /dev/null
+++ b/spec/frontend/branches/components/delete_branch_button_spec.js
@@ -0,0 +1,96 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import DeleteBranchButton from '~/branches/components/delete_branch_button.vue';
+import eventHub from '~/branches/event_hub';
+
+let wrapper;
+let findDeleteButton;
+
+const createComponent = (props = {}) => {
+ wrapper = shallowMount(DeleteBranchButton, {
+ propsData: {
+ branchName: 'test',
+ deletePath: '/path/to/branch',
+ defaultBranchName: 'main',
+ ...props,
+ },
+ });
+};
+
+describe('Delete branch button', () => {
+ let eventHubSpy;
+
+ beforeEach(() => {
+ findDeleteButton = () => wrapper.findComponent(GlButton);
+ eventHubSpy = jest.spyOn(eventHub, '$emit');
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the button with default tooltip, style, and icon', () => {
+ createComponent();
+
+ expect(findDeleteButton().attributes()).toMatchObject({
+ title: 'Delete branch',
+ variant: 'danger',
+ icon: 'remove',
+ });
+ });
+
+ it('renders a different tooltip for a protected branch', () => {
+ createComponent({ isProtectedBranch: true });
+
+ expect(findDeleteButton().attributes()).toMatchObject({
+ title: 'Delete protected branch',
+ variant: 'danger',
+ icon: 'remove',
+ });
+ });
+
+ it('renders a different protected tooltip when it is both protected and disabled', () => {
+ createComponent({ isProtectedBranch: true, disabled: true });
+
+ expect(findDeleteButton().attributes()).toMatchObject({
+ title: 'Only a project maintainer or owner can delete a protected branch',
+ variant: 'default',
+ });
+ });
+
+ it('emits the data to eventHub when button is clicked', () => {
+ createComponent({ merged: true });
+
+ findDeleteButton().vm.$emit('click');
+
+ expect(eventHubSpy).toHaveBeenCalledWith('openModal', {
+ branchName: 'test',
+ defaultBranchName: 'main',
+ deletePath: '/path/to/branch',
+ isProtectedBranch: false,
+ merged: true,
+ });
+ });
+
+ describe('#disabled', () => {
+ it('does not disable the button by default when mounted', () => {
+ createComponent();
+
+ expect(findDeleteButton().attributes()).toMatchObject({
+ title: 'Delete branch',
+ variant: 'danger',
+ });
+ });
+
+ // Used for unallowed users and for the default branch.
+ it('disables the button when mounted for a disabled modal', () => {
+ createComponent({ disabled: true, tooltip: 'The default branch cannot be deleted' });
+
+ expect(findDeleteButton().attributes()).toMatchObject({
+ title: 'The default branch cannot be deleted',
+ disabled: 'true',
+ variant: 'default',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/branches/components/delete_branch_modal_spec.js b/spec/frontend/branches/components/delete_branch_modal_spec.js
new file mode 100644
index 00000000000..0c6111bda9e
--- /dev/null
+++ b/spec/frontend/branches/components/delete_branch_modal_spec.js
@@ -0,0 +1,157 @@
+import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeleteBranchModal from '~/branches/components/delete_branch_modal.vue';
+import eventHub from '~/branches/event_hub';
+
+let wrapper;
+
+const branchName = 'test_modal';
+const defaultBranchName = 'default';
+const deletePath = '/path/to/branch';
+const merged = false;
+const isProtectedBranch = false;
+
+const createComponent = (data = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(DeleteBranchModal, {
+ data() {
+ return {
+ branchName,
+ deletePath,
+ defaultBranchName,
+ merged,
+ isProtectedBranch,
+ ...data,
+ };
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+ },
+ }),
+ );
+};
+
+const findModal = () => wrapper.findComponent(GlModal);
+const findModalMessage = () => wrapper.findByTestId('modal-message');
+const findDeleteButton = () => wrapper.findByTestId('delete-branch-confirmation-button');
+const findCancelButton = () => wrapper.findByTestId('delete-branch-cancel-button');
+const findFormInput = () => wrapper.findComponent(GlFormInput);
+const findForm = () => wrapper.find('form');
+
+describe('Delete branch modal', () => {
+ const expectedUnmergedWarning =
+ 'This branch hasn’t been merged into default. To avoid data loss, consider merging this branch before deleting it.';
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Deleting a regular branch', () => {
+ const expectedTitle = 'Delete branch. Are you ABSOLUTELY SURE?';
+ const expectedWarning = "You're about to permanently delete the branch test_modal.";
+ const expectedMessage = `${expectedWarning} ${expectedUnmergedWarning}`;
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the modal correctly', () => {
+ expect(findModal().props('title')).toBe(expectedTitle);
+ expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessage);
+ expect(findCancelButton().text()).toBe('Cancel, keep branch');
+ expect(findDeleteButton().text()).toBe('Yes, delete branch');
+ expect(findForm().attributes('action')).toBe(deletePath);
+ });
+
+ it('submits the form when the delete button is clicked', () => {
+ const submitFormSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
+
+ findDeleteButton().trigger('click');
+
+ expect(findForm().attributes('action')).toBe(deletePath);
+ expect(submitFormSpy).toHaveBeenCalled();
+ });
+
+ it('calls show on the modal when a `openModal` event is received through the event hub', async () => {
+ const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
+
+ eventHub.$emit('openModal', {
+ isProtectedBranch,
+ branchName,
+ defaultBranchName,
+ deletePath,
+ merged,
+ });
+
+ expect(showSpy).toHaveBeenCalled();
+ });
+
+ it('calls hide on the modal when cancel button is clicked', () => {
+ const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
+
+ findCancelButton().trigger('click');
+
+ expect(closeModalSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('Deleting a protected branch (for owner or maintainer)', () => {
+ const expectedTitleProtected = 'Delete protected branch. Are you ABSOLUTELY SURE?';
+ const expectedWarningProtected =
+ "You're about to permanently delete the protected branch test_modal.";
+ const expectedMessageProtected = `${expectedWarningProtected} ${expectedUnmergedWarning}`;
+ const expectedConfirmationText =
+ 'Once you confirm and press Yes, delete protected branch, it cannot be undone or recovered. Please type the following to confirm: test_modal';
+
+ beforeEach(() => {
+ createComponent({ isProtectedBranch: true });
+ });
+
+ describe('rendering the modal correctly for a protected branch', () => {
+ it('sets the modal title for a protected branch', () => {
+ expect(findModal().props('title')).toBe(expectedTitleProtected);
+ });
+
+ it('renders the correct text in the modal message', () => {
+ expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessageProtected);
+ });
+
+ it('renders the protected branch name confirmation form with expected text and action', () => {
+ expect(findForm().text()).toMatchInterpolatedText(expectedConfirmationText);
+ expect(findForm().attributes('action')).toBe(deletePath);
+ });
+
+ it('renders the buttons with the correct button text', () => {
+ expect(findCancelButton().text()).toBe('Cancel, keep branch');
+ expect(findDeleteButton().text()).toBe('Yes, delete protected branch');
+ });
+ });
+
+ it('opens with the delete button disabled and enables it when branch name is confirmed', async () => {
+ expect(findDeleteButton().props('disabled')).toBe(true);
+
+ findFormInput().vm.$emit('input', branchName);
+
+ await waitForPromises();
+
+ expect(findDeleteButton().props('disabled')).not.toBe(true);
+ });
+ });
+
+ describe('Deleting a merged branch', () => {
+ it('does not include the unmerged branch warning when merged is true', () => {
+ createComponent({ merged: true });
+
+ expect(findModalMessage().html()).not.toContain(expectedUnmergedWarning);
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
index be3640936dc..426e6cae8fb 100644
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ b/spec/frontend/ci_variable_list/store/actions_spec.js
@@ -5,7 +5,7 @@ import * as actions from '~/ci_variable_list/store/actions';
import * as types from '~/ci_variable_list/store/mutation_types';
import getInitialState from '~/ci_variable_list/store/state';
import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import mockData from '../services/mock_data';
@@ -240,7 +240,9 @@ describe('CI variable list store actions', () => {
mock.onGet(state.endpoint).reply(500);
testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }], () => {
- expect(createFlash).toHaveBeenCalledWith('There was an error fetching the variables.');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error fetching the variables.',
+ });
done();
});
});
@@ -278,9 +280,9 @@ describe('CI variable list store actions', () => {
[],
[{ type: 'requestEnvironments' }],
() => {
- expect(createFlash).toHaveBeenCalledWith(
- 'There was an error fetching the environments information.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error fetching the environments information.',
+ });
done();
},
);
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index 6047b404197..e5e336eb3d5 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -62,6 +62,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon gl-mt-3 gl-align-self-start"
data-testid="dropdown-item-checkbox"
+ role="img"
>
<use
href="#mobile-issue-close"
@@ -117,6 +118,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden gl-mt-3 gl-align-self-start"
data-testid="dropdown-item-checkbox"
+ role="img"
>
<use
href="#mobile-issue-close"
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index db5915cb1eb..511f5fc1d89 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -2,8 +2,6 @@ import { shallowMount, mount } from '@vue/test-utils';
import ApplicationRow from '~/clusters/components/application_row.vue';
import Applications from '~/clusters/components/applications.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
-import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
-import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
@@ -72,9 +70,6 @@ describe('Applications', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
- it('renders a row for Fluentd', () => {
- expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
- });
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
@@ -117,10 +112,6 @@ describe('Applications', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
- it('renders a row for Fluentd', () => {
- expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
- });
-
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
@@ -163,10 +154,6 @@ describe('Applications', () => {
expect(wrapper.find('.js-cluster-application-row-elastic_stack').exists()).toBe(true);
});
- it('renders a row for Fluentd', () => {
- expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
- });
-
it('renders a row for Cilium', () => {
expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
});
@@ -185,24 +172,6 @@ describe('Applications', () => {
expect(findByTestId('ingressCostWarning').element).toMatchSnapshot();
});
- describe('with nested component', () => {
- const propsData = {
- applications: {
- ingress: {
- title: 'Ingress',
- status: 'installed',
- },
- },
- };
-
- beforeEach(() => createShallowComponent(propsData));
-
- it('renders IngressModsecuritySettings', () => {
- const modsecuritySettings = wrapper.find(IngressModsecuritySettings);
- expect(modsecuritySettings.exists()).toBe(true);
- });
- });
-
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
@@ -231,7 +200,6 @@ describe('Applications', () => {
title: 'Ingress',
status: 'installed',
externalHostname: 'localhost.localdomain',
- modsecurity_enabled: false,
},
cert_manager: { title: 'Cert-Manager' },
crossplane: { title: 'Crossplane', stack: '' },
@@ -240,7 +208,6 @@ describe('Applications', () => {
jupyter: { title: 'JupyterHub', hostname: '' },
knative: { title: 'Knative', hostname: '' },
elastic_stack: { title: 'Elastic Stack' },
- fluentd: { title: 'Fluentd' },
cilium: { title: 'GitLab Container Network Policies' },
},
});
@@ -534,14 +501,6 @@ describe('Applications', () => {
});
});
- describe('Fluentd application', () => {
- beforeEach(() => createShallowComponent());
-
- it('renders the correct Component', () => {
- expect(wrapper.find(FluentdOutputSettings).exists()).toBe(true);
- });
- });
-
describe('Cilium application', () => {
it('shows the correct description', () => {
createComponent({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
diff --git a/spec/frontend/clusters/components/fluentd_output_settings_spec.js b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
deleted file mode 100644
index 2c6e5bbd46a..00000000000
--- a/spec/frontend/clusters/components/fluentd_output_settings_spec.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import { GlAlert, GlDropdown, GlFormCheckbox } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
-import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
-import eventHub from '~/clusters/event_hub';
-
-const { UPDATING } = APPLICATION_STATUS;
-
-describe('FluentdOutputSettings', () => {
- let wrapper;
-
- const defaultSettings = {
- protocol: 'tcp',
- host: '127.0.0.1',
- port: 514,
- wafLogEnabled: true,
- ciliumLogEnabled: false,
- };
- const defaultProps = {
- status: 'installable',
- updateFailed: false,
- ...defaultSettings,
- };
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(FluentdOutputSettings, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
- const updateComponentPropsFromEvent = () => {
- const { isEditingSettings, ...props } = eventHub.$emit.mock.calls[0][1];
- wrapper.setProps(props);
- };
- const findSaveButton = () => wrapper.find({ ref: 'saveBtn' });
- const findCancelButton = () => wrapper.find({ ref: 'cancelBtn' });
- const findProtocolDropdown = () => wrapper.find(GlDropdown);
- const findCheckbox = (name) =>
- wrapper.findAll(GlFormCheckbox).wrappers.find((x) => x.text() === name);
- const findHost = () => wrapper.find('#fluentd-host');
- const findPort = () => wrapper.find('#fluentd-port');
- const changeCheckbox = (checkbox) => {
- const currentValue = checkbox.attributes('checked')?.toString() === 'true';
- checkbox.vm.$emit('input', !currentValue);
- };
- const changeInput = ({ element }, val) => {
- element.value = val;
- element.dispatchEvent(new Event('input'));
- };
- const changePort = (val) => changeInput(findPort(), val);
- const changeHost = (val) => changeInput(findHost(), val);
- const changeProtocol = (idx) => findProtocolDropdown().vm.$children[idx].$emit('click');
- const toApplicationSettings = ({ wafLogEnabled, ciliumLogEnabled, ...settings }) => ({
- ...settings,
- waf_log_enabled: wafLogEnabled,
- cilium_log_enabled: ciliumLogEnabled,
- });
-
- describe('when fluentd is installed', () => {
- beforeEach(() => {
- createComponent({ status: 'installed' });
- jest.spyOn(eventHub, '$emit');
- });
-
- it('does not render save and cancel buttons', () => {
- expect(findSaveButton().exists()).toBe(false);
- expect(findCancelButton().exists()).toBe(false);
- });
-
- describe.each`
- desc | changeFn | key | value
- ${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'}
- ${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'}
- ${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123}
- ${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Web Application Firewall Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled}
- ${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Container Network Policies Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled}
- `('$desc', ({ changeFn, key, value }) => {
- beforeEach(() => {
- changeFn();
- });
-
- it('triggers set event to be propagated with the current value', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', {
- [key]: value,
- isEditingSettings: true,
- });
- });
-
- describe('when value is updated from store', () => {
- beforeEach(() => {
- updateComponentPropsFromEvent();
- });
-
- it('enables save and cancel buttons', () => {
- expect(findSaveButton().exists()).toBe(true);
- expect(findSaveButton().attributes().disabled).toBeUndefined();
- expect(findCancelButton().exists()).toBe(true);
- expect(findCancelButton().attributes().disabled).toBeUndefined();
- });
-
- describe('and the save changes button is clicked', () => {
- beforeEach(() => {
- eventHub.$emit.mockClear();
- findSaveButton().vm.$emit('click');
- });
-
- it('triggers save event and pass current values', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
- id: FLUENTD,
- params: toApplicationSettings({
- ...defaultSettings,
- [key]: value,
- }),
- });
- });
- });
-
- describe('and the cancel button is clicked', () => {
- beforeEach(() => {
- eventHub.$emit.mockClear();
- findCancelButton().vm.$emit('click');
- });
-
- it('triggers reset event', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', {
- ...defaultSettings,
- isEditingSettings: false,
- });
- });
-
- describe('when value is updated from store', () => {
- beforeEach(() => {
- updateComponentPropsFromEvent();
- });
-
- it('does not render save and cancel buttons', () => {
- expect(findSaveButton().exists()).toBe(false);
- expect(findCancelButton().exists()).toBe(false);
- });
- });
- });
- });
- });
-
- describe(`when fluentd status is ${UPDATING}`, () => {
- beforeEach(() => {
- createComponent({ installed: true, status: UPDATING });
- });
-
- it('renders loading spinner in save button', () => {
- expect(findSaveButton().props('loading')).toBe(true);
- });
-
- it('renders disabled save button', () => {
- expect(findSaveButton().props('disabled')).toBe(true);
- });
-
- it('renders save button with "Saving" label', () => {
- expect(findSaveButton().text()).toBe('Saving');
- });
- });
-
- describe('when fluentd fails to update', () => {
- beforeEach(() => {
- createComponent({ updateFailed: true });
- });
-
- it('displays a error message', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
- });
- });
- });
-
- describe('when fluentd is not installed', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('does not render the save button', () => {
- expect(findSaveButton().exists()).toBe(false);
- expect(findCancelButton().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
deleted file mode 100644
index f83a350a27c..00000000000
--- a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import { GlAlert, GlToggle, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
-import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants';
-import eventHub from '~/clusters/event_hub';
-
-const { UPDATING } = APPLICATION_STATUS;
-
-describe('IngressModsecuritySettings', () => {
- let wrapper;
-
- const defaultProps = {
- modsecurity_enabled: false,
- status: 'installable',
- installed: false,
- modsecurity_mode: 'logging',
- updateAvailable: false,
- };
-
- const createComponent = (props = defaultProps) => {
- wrapper = shallowMount(IngressModsecuritySettings, {
- propsData: {
- ingress: {
- ...defaultProps,
- ...props,
- },
- },
- });
- };
-
- const findSaveButton = () =>
- wrapper.find('[data-qa-selector="save_ingress_modsecurity_settings"]');
- const findCancelButton = () =>
- wrapper.find('[data-qa-selector="cancel_ingress_modsecurity_settings"]');
- const findModSecurityToggle = () => wrapper.find(GlToggle);
- const findModSecurityDropdown = () => wrapper.find(GlDropdown);
-
- describe('when ingress is installed', () => {
- beforeEach(() => {
- createComponent({ installed: true, status: 'installed' });
- jest.spyOn(eventHub, '$emit');
- });
-
- it('does not render save and cancel buttons', () => {
- expect(findSaveButton().exists()).toBe(false);
- expect(findCancelButton().exists()).toBe(false);
- });
-
- describe('with toggle changed by the user', () => {
- beforeEach(() => {
- findModSecurityToggle().vm.$emit('change');
- wrapper.setProps({
- ingress: {
- ...defaultProps,
- installed: true,
- status: 'installed',
- modsecurity_enabled: true,
- },
- });
- });
-
- it('renders toggle with label', () => {
- expect(findModSecurityToggle().props('label')).toBe(
- IngressModsecuritySettings.i18n.modSecurityEnabled,
- );
- });
-
- it('renders save and cancel buttons', () => {
- expect(findSaveButton().exists()).toBe(true);
- expect(findCancelButton().exists()).toBe(true);
- });
-
- it('enables related toggle and buttons', () => {
- expect(findSaveButton().attributes().disabled).toBeUndefined();
- expect(findCancelButton().attributes().disabled).toBeUndefined();
- });
-
- describe('with dropdown changed by the user', () => {
- beforeEach(() => {
- findModSecurityDropdown().vm.$children[1].$emit('click');
- wrapper.setProps({
- ingress: {
- ...defaultProps,
- installed: true,
- status: 'installed',
- modsecurity_enabled: true,
- modsecurity_mode: 'blocking',
- },
- });
- });
-
- it('renders both save and cancel buttons', () => {
- expect(findSaveButton().exists()).toBe(true);
- expect(findCancelButton().exists()).toBe(true);
- });
-
- describe('and the save changes button is clicked', () => {
- beforeEach(() => {
- findSaveButton().vm.$emit('click');
- });
-
- it('triggers save event and pass current modsecurity value', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
- id: INGRESS,
- params: { modsecurity_enabled: true, modsecurity_mode: 'blocking' },
- });
- });
- });
- });
-
- describe('and the cancel button is clicked', () => {
- beforeEach(() => {
- findCancelButton().vm.$emit('click');
- });
-
- it('triggers reset event and hides both cancel and save changes button', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith('resetIngressModSecurityChanges', INGRESS);
- expect(findSaveButton().exists()).toBe(false);
- expect(findCancelButton().exists()).toBe(false);
- });
- });
-
- describe('with a new version available', () => {
- beforeEach(() => {
- wrapper.setProps({
- ingress: {
- ...defaultProps,
- installed: true,
- status: 'installed',
- modsecurity_enabled: true,
- updateAvailable: true,
- },
- });
- });
-
- it('disables related toggle and buttons', () => {
- expect(findSaveButton().attributes().disabled).toBe('true');
- expect(findCancelButton().attributes().disabled).toBe('true');
- });
- });
- });
-
- it('triggers set event to be propagated with the current modsecurity value', () => {
- wrapper.setData({ modSecurityEnabled: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('setIngressModSecurityEnabled', {
- id: INGRESS,
- modSecurityEnabled: true,
- });
- });
- });
-
- describe(`when ingress status is ${UPDATING}`, () => {
- beforeEach(() => {
- createComponent({ installed: true, status: UPDATING });
- });
-
- it('renders loading spinner in save button', () => {
- expect(findSaveButton().props('loading')).toBe(true);
- });
-
- it('renders disabled save button', () => {
- expect(findSaveButton().props('disabled')).toBe(true);
- });
-
- it('renders save button with "Saving" label', () => {
- expect(findSaveButton().text()).toBe('Saving');
- });
- });
-
- describe('when ingress fails to update', () => {
- beforeEach(() => {
- createComponent({ updateFailed: true });
- });
-
- it('displays a error message', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(true);
- });
- });
- });
-
- describe('when ingress is not installed', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('does not render the save button', () => {
- expect(findSaveButton().exists()).toBe(false);
- expect(findModSecurityToggle().props('value')).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index c5cec4c4fdb..b129baa2d83 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -15,7 +15,6 @@ describe('ClusterIntegrationForm', () => {
editable: true,
environmentScope: '*',
baseDomain: 'testDomain',
- applicationIngressExternalIp: null,
};
const createWrapper = (storeValues = defaultStoreValues) => {
@@ -72,18 +71,6 @@ describe('ClusterIntegrationForm', () => {
expect(findSubmitButton().exists()).toBe(false);
});
});
-
- it('does not render external IP block if applicationIngressExternalIp was not passed', () => {
- createWrapper({ ...defaultStoreValues });
-
- expect(wrapper.find('.js-ingress-domain-help-text').exists()).toBe(false);
- });
-
- it('renders external IP block if applicationIngressExternalIp was passed', () => {
- createWrapper({ ...defaultStoreValues, applicationIngressExternalIp: '127.0.0.1' });
-
- expect(wrapper.find('.js-ingress-domain-help-text').exists()).toBe(true);
- });
});
describe('reactivity', () => {
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index 4f8b27d623c..a75fcb0cb06 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -20,7 +20,6 @@ const CLUSTERS_MOCK_DATA = {
external_ip: null,
external_hostname: null,
can_uninstall: false,
- modsecurity_enabled: false,
},
{
name: 'runner',
@@ -154,7 +153,6 @@ const APPLICATIONS_MOCK_STATE = {
ingress: {
title: 'Ingress',
status: 'installable',
- modsecurity_enabled: false,
},
crossplane: { title: 'Crossplane', status: 'installable', stack: '' },
cert_manager: { title: 'Cert-Manager', status: 'installable' },
@@ -163,7 +161,6 @@ const APPLICATIONS_MOCK_STATE = {
jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' },
knative: { title: 'Knative ', status: 'installable', hostname: '' },
elastic_stack: { title: 'Elastic Stack', status: 'installable' },
- fluentd: { title: 'Fluentd', status: 'installable' },
cilium: {
title: 'GitLab Container Network Policies',
status: 'not_installable',
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index c80949531c8..cdba6fc6ab8 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -84,16 +84,12 @@ describe('Clusters Store', () => {
externalHostname: null,
installable: true,
installed: false,
- isEditingModSecurityEnabled: false,
- isEditingModSecurityMode: false,
installFailed: true,
uninstallable: false,
updateFailed: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
- modsecurity_enabled: false,
- modsecurity_mode: undefined,
},
runner: {
title: 'GitLab Runner',
@@ -126,25 +122,6 @@ describe('Clusters Store', () => {
uninstallFailed: false,
validationError: null,
},
- fluentd: {
- title: 'Fluentd',
- status: null,
- statusReason: null,
- requestReason: null,
- port: null,
- ciliumLogEnabled: null,
- host: null,
- protocol: null,
- installable: true,
- installed: false,
- isEditingSettings: false,
- installFailed: false,
- uninstallable: false,
- uninstallSuccessful: false,
- uninstallFailed: false,
- validationError: null,
- wafLogEnabled: null,
- },
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
diff --git a/spec/frontend/commit/pipelines/pipelines_spec.js b/spec/frontend/commit/pipelines/pipelines_spec.js
deleted file mode 100644
index fe928a01acf..00000000000
--- a/spec/frontend/commit/pipelines/pipelines_spec.js
+++ /dev/null
@@ -1,280 +0,0 @@
-import '~/commons';
-import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import Api from '~/api';
-import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
-import axios from '~/lib/utils/axios_utils';
-
-describe('Pipelines table in Commits and Merge requests', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
- let pipeline;
- let PipelinesTable;
- let mock;
- let vm;
- const props = {
- endpoint: 'endpoint.json',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- };
-
- const findRunPipelineBtn = () => vm.$el.querySelector('[data-testid="run_pipeline_button"]');
- const findRunPipelineBtnMobile = () =>
- vm.$el.querySelector('[data-testid="run_pipeline_button_mobile"]');
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- const { pipelines } = getJSONFixture(jsonFixtureName);
-
- PipelinesTable = Vue.extend(pipelinesTable);
- pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- describe('successful request', () => {
- describe('without pipelines', () => {
- beforeEach(() => {
- mock.onGet('endpoint.json').reply(200, []);
-
- vm = mountComponent(PipelinesTable, props);
- });
-
- it('should render the empty state', (done) => {
- setImmediate(() => {
- expect(vm.$el.querySelector('.empty-state')).toBeDefined();
- expect(vm.$el.querySelector('.realtime-loading')).toBe(null);
- expect(vm.$el.querySelector('.js-pipelines-error-state')).toBe(null);
- done();
- });
- });
- });
-
- describe('with pipelines', () => {
- beforeEach(() => {
- mock.onGet('endpoint.json').reply(200, [pipeline]);
- vm = mountComponent(PipelinesTable, props);
- });
-
- it('should render a table with the received pipelines', (done) => {
- setImmediate(() => {
- expect(vm.$el.querySelectorAll('.ci-table .commit').length).toEqual(1);
- expect(vm.$el.querySelector('.realtime-loading')).toBe(null);
- expect(vm.$el.querySelector('.empty-state')).toBe(null);
- expect(vm.$el.querySelector('.js-pipelines-error-state')).toBe(null);
- done();
- });
- });
-
- describe('with pagination', () => {
- it('should make an API request when using pagination', (done) => {
- setImmediate(() => {
- jest.spyOn(vm, 'updateContent').mockImplementation(() => {});
-
- vm.store.state.pageInfo = {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- };
-
- vm.$nextTick(() => {
- vm.$el.querySelector('.next-page-item').click();
-
- expect(vm.updateContent).toHaveBeenCalledWith({ page: '2' });
- done();
- });
- });
- });
- });
- });
-
- describe('pipeline badge counts', () => {
- beforeEach(() => {
- mock.onGet('endpoint.json').reply(200, [pipeline]);
- });
-
- it('should receive update-pipelines-count event', (done) => {
- const element = document.createElement('div');
- document.body.appendChild(element);
-
- element.addEventListener('update-pipelines-count', (event) => {
- expect(event.detail.pipelines).toEqual([pipeline]);
- done();
- });
-
- vm = mountComponent(PipelinesTable, props);
-
- element.appendChild(vm.$el);
- });
- });
- });
-
- describe('run pipeline button', () => {
- let pipelineCopy;
-
- beforeEach(() => {
- pipelineCopy = { ...pipeline };
- });
-
- describe('when latest pipeline has detached flag', () => {
- it('renders the run pipeline button', (done) => {
- pipelineCopy.flags.detached_merge_request_pipeline = true;
- pipelineCopy.flags.merge_request_pipeline = true;
-
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
-
- vm = mountComponent(PipelinesTable, { ...props });
-
- setImmediate(() => {
- expect(findRunPipelineBtn()).not.toBeNull();
- expect(findRunPipelineBtnMobile()).not.toBeNull();
- done();
- });
- });
- });
-
- describe('when latest pipeline does not have detached flag', () => {
- it('does not render the run pipeline button', (done) => {
- pipelineCopy.flags.detached_merge_request_pipeline = false;
- pipelineCopy.flags.merge_request_pipeline = false;
-
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
-
- vm = mountComponent(PipelinesTable, { ...props });
-
- setImmediate(() => {
- expect(findRunPipelineBtn()).toBeNull();
- expect(findRunPipelineBtnMobile()).toBeNull();
- done();
- });
- });
- });
-
- describe('on click', () => {
- const findModal = () =>
- document.querySelector('#create-pipeline-for-fork-merge-request-modal');
-
- beforeEach((done) => {
- pipelineCopy.flags.detached_merge_request_pipeline = true;
-
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
-
- vm = mountComponent(PipelinesTable, {
- ...props,
- canRunPipeline: true,
- projectId: '5',
- mergeRequestId: 3,
- });
-
- jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
-
- setImmediate(() => {
- done();
- });
- });
-
- it('on desktop, shows a loading button', (done) => {
- findRunPipelineBtn().click();
-
- vm.$nextTick(() => {
- expect(findModal()).toBeNull();
-
- expect(findRunPipelineBtn().disabled).toBe(true);
- expect(findRunPipelineBtn().querySelector('.gl-spinner')).not.toBeNull();
-
- setImmediate(() => {
- expect(findRunPipelineBtn().disabled).toBe(false);
- expect(findRunPipelineBtn().querySelector('.gl-spinner')).toBeNull();
-
- done();
- });
- });
- });
-
- it('on mobile, shows a loading button', (done) => {
- findRunPipelineBtnMobile().click();
-
- vm.$nextTick(() => {
- expect(findModal()).toBeNull();
-
- expect(findModal()).toBeNull();
- expect(findRunPipelineBtn().querySelector('.gl-spinner')).not.toBeNull();
-
- setImmediate(() => {
- expect(findRunPipelineBtn().disabled).toBe(false);
- expect(findRunPipelineBtn().querySelector('.gl-spinner')).toBeNull();
-
- done();
- });
- });
- });
- });
-
- describe('on click for fork merge request', () => {
- const findModal = () =>
- document.querySelector('#create-pipeline-for-fork-merge-request-modal');
-
- beforeEach((done) => {
- pipelineCopy.flags.detached_merge_request_pipeline = true;
-
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
-
- vm = mountComponent(PipelinesTable, {
- ...props,
- projectId: '5',
- mergeRequestId: 3,
- canCreatePipelineInTargetProject: true,
- sourceProjectFullPath: 'test/parent-project',
- targetProjectFullPath: 'test/fork-project',
- });
-
- jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
-
- setImmediate(() => {
- done();
- });
- });
-
- it('on desktop, shows a security warning modal', (done) => {
- findRunPipelineBtn().click();
-
- vm.$nextTick(() => {
- expect(findModal()).not.toBeNull();
- done();
- });
- });
-
- it('on mobile, shows a security warning modal', (done) => {
- findRunPipelineBtnMobile().click();
-
- vm.$nextTick(() => {
- expect(findModal()).not.toBeNull();
- done();
- });
- });
- });
- });
-
- describe('unsuccessfull request', () => {
- beforeEach(() => {
- mock.onGet('endpoint.json').reply(500, []);
-
- vm = mountComponent(PipelinesTable, props);
- });
-
- it('should render error state', (done) => {
- setImmediate(() => {
- expect(vm.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
- expect(vm.$el.querySelector('.realtime-loading')).toBe(null);
- expect(vm.$el.querySelector('.ci-table')).toBe(null);
- done();
- });
- });
- });
-});
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
new file mode 100644
index 00000000000..4bf6727af3b
--- /dev/null
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -0,0 +1,253 @@
+import { GlEmptyState, GlLoadingIcon, GlModal, GlTable } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Api from '~/api';
+import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
+import axios from '~/lib/utils/axios_utils';
+
+describe('Pipelines table in Commits and Merge requests', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+ let wrapper;
+ let pipeline;
+ let mock;
+
+ const findRunPipelineBtn = () => wrapper.findByTestId('run_pipeline_button');
+ const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile');
+ const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ mount(PipelinesTable, {
+ propsData: {
+ endpoint: 'endpoint.json',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
+ ...props,
+ },
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ const { pipelines } = getJSONFixture(jsonFixtureName);
+
+ pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('successful request', () => {
+ describe('without pipelines', () => {
+ beforeEach(async () => {
+ mock.onGet('endpoint.json').reply(200, []);
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should render the empty state', () => {
+ expect(findTableRows()).toHaveLength(0);
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ });
+ });
+
+ describe('with pipelines', () => {
+ beforeEach(async () => {
+ mock.onGet('endpoint.json').reply(200, [pipeline]);
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should render a table with the received pipelines', () => {
+ expect(findTable().exists()).toBe(true);
+ expect(findTableRows()).toHaveLength(1);
+ expect(findLoadingState().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ });
+
+ describe('with pagination', () => {
+ it('should make an API request when using pagination', async () => {
+ jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
+
+ await wrapper.setData({
+ store: {
+ state: {
+ pageInfo: {
+ page: 1,
+ total: 10,
+ perPage: 2,
+ nextPage: 2,
+ totalPages: 5,
+ },
+ },
+ },
+ });
+
+ wrapper.find('.next-page-item').trigger('click');
+
+ expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ page: '2' });
+ });
+ });
+
+ describe('pipeline badge counts', () => {
+ it('should receive update-pipelines-count event', (done) => {
+ const element = document.createElement('div');
+ document.body.appendChild(element);
+
+ element.addEventListener('update-pipelines-count', (event) => {
+ expect(event.detail.pipelines).toEqual([pipeline]);
+ done();
+ });
+
+ createComponent();
+
+ element.appendChild(wrapper.vm.$el);
+ });
+ });
+ });
+ });
+
+ describe('run pipeline button', () => {
+ let pipelineCopy;
+
+ beforeEach(() => {
+ pipelineCopy = { ...pipeline };
+ });
+
+ describe('when latest pipeline has detached flag', () => {
+ it('renders the run pipeline button', async () => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+ pipelineCopy.flags.merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findRunPipelineBtn().exists()).toBe(true);
+ expect(findRunPipelineBtnMobile().exists()).toBe(true);
+ });
+ });
+
+ describe('when latest pipeline does not have detached flag', () => {
+ it('does not render the run pipeline button', async () => {
+ pipelineCopy.flags.detached_merge_request_pipeline = false;
+ pipelineCopy.flags.merge_request_pipeline = false;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findRunPipelineBtn().exists()).toBe(false);
+ expect(findRunPipelineBtnMobile().exists()).toBe(false);
+ });
+ });
+
+ describe('on click', () => {
+ beforeEach(async () => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ createComponent({
+ canRunPipeline: true,
+ projectId: '5',
+ mergeRequestId: 3,
+ });
+
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
+
+ await waitForPromises();
+ });
+
+ it('on desktop, shows a loading button', async () => {
+ await findRunPipelineBtn().trigger('click');
+
+ expect(findRunPipelineBtn().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findRunPipelineBtn().props('loading')).toBe(false);
+ });
+
+ it('on mobile, shows a loading button', async () => {
+ await findRunPipelineBtnMobile().trigger('click');
+
+ expect(findRunPipelineBtn().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findRunPipelineBtn().props('disabled')).toBe(false);
+ expect(findRunPipelineBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('on click for fork merge request', () => {
+ beforeEach(async () => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
+
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+
+ createComponent({
+ projectId: '5',
+ mergeRequestId: 3,
+ canCreatePipelineInTargetProject: true,
+ sourceProjectFullPath: 'test/parent-project',
+ targetProjectFullPath: 'test/fork-project',
+ });
+
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
+
+ await waitForPromises();
+ });
+
+ it('on desktop, shows a security warning modal', async () => {
+ await findRunPipelineBtn().trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findModal()).not.toBeNull();
+ });
+
+ it('on mobile, shows a security warning modal', async () => {
+ await findRunPipelineBtnMobile().trigger('click');
+
+ expect(findModal()).not.toBeNull();
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ beforeEach(async () => {
+ mock.onGet('endpoint.json').reply(500, []);
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should render error state', () => {
+ expect(findEmptyState().text()).toBe(
+ 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
new file mode 100644
index 00000000000..e56c37b0dc9
--- /dev/null
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`content_editor/components/toolbar_link_button renders dropdown component 1`] = `
+"<div class=\\"dropdown b-dropdown gl-new-dropdown btn-group\\" aria-label=\\"Insert link\\" title=\\"Insert link\\">
+ <!----><button aria-haspopup=\\"true\\" aria-expanded=\\"false\\" type=\\"button\\" class=\\"btn dropdown-toggle btn-default btn-sm gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only\\">
+ <!----> <svg data-testid=\\"link-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"dropdown-icon gl-icon s16\\">
+ <use href=\\"#link\\"></use>
+ </svg> <span class=\\"gl-new-dropdown-button-text\\"></span> <svg data-testid=\\"chevron-down-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"gl-button-icon dropdown-chevron gl-icon s16\\">
+ <use href=\\"#chevron-down\\"></use>
+ </svg></button>
+ <ul role=\\"menu\\" tabindex=\\"-1\\" class=\\"dropdown-menu\\">
+ <div class=\\"gl-new-dropdown-inner\\">
+ <!---->
+ <div class=\\"gl-new-dropdown-contents\\">
+ <li role=\\"presentation\\" class=\\"gl-px-3!\\">
+ <form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
+ <div placeholder=\\"Link URL\\">
+ <div role=\\"group\\" class=\\"input-group\\">
+ <!---->
+ <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"gl-form-input form-control\\">
+ <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
+ <!---->
+ <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
+ <!---->
+ </div>
+ </div>
+ </form>
+ </li>
+ <!---->
+ <!---->
+ </div>
+ <!---->
+ </div>
+ </ul>
+</div>"
+`;
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index e3741032bf4..59c4190ad3a 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -27,7 +27,10 @@ describe('ContentEditor', () => {
it('renders editor content component and attaches editor instance', () => {
createWrapper(editor);
- expect(wrapper.findComponent(EditorContent).props().editor).toBe(editor.tiptapEditor);
+ const editorContent = wrapper.findComponent(EditorContent);
+
+ expect(editorContent.props().editor).toBe(editor.tiptapEditor);
+ expect(editorContent.classes()).toContain('md');
});
it('renders top toolbar component and attaches editor instance', () => {
@@ -38,8 +41,8 @@ describe('ContentEditor', () => {
it.each`
isFocused | classes
- ${true} | ${['md', 'md-area', 'is-focused']}
- ${false} | ${['md', 'md-area']}
+ ${true} | ${['md-area', 'is-focused']}
+ ${false} | ${['md-area']}
`(
'has $classes class selectors when tiptapEditor.isFocused = $isFocused',
({ isFocused, classes }) => {
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
new file mode 100644
index 00000000000..812e769c891
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
@@ -0,0 +1,151 @@
+import { GlDropdown, GlDropdownDivider, GlFormInputGroup, GlButton } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
+import { tiptapExtension as Link } from '~/content_editor/extensions/link';
+import { hasSelection } from '~/content_editor/services/utils';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
+
+jest.mock('~/content_editor/services/utils');
+
+describe('content_editor/components/toolbar_link_button', () => {
+ let wrapper;
+ let editor;
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(ToolbarLinkButton, {
+ propsData: {
+ tiptapEditor: editor,
+ },
+ stubs: {
+ GlFormInputGroup,
+ },
+ });
+ };
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
+ const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
+ const findApplyLinkButton = () => wrapper.findComponent(GlButton);
+ const findRemoveLinkButton = () => wrapper.findByText('Remove link');
+
+ beforeEach(() => {
+ editor = createTestEditor({
+ extensions: [Link],
+ });
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ wrapper.destroy();
+ });
+
+ it('renders dropdown component', () => {
+ buildWrapper();
+
+ expect(findDropdown().html()).toMatchSnapshot();
+ });
+
+ describe('when there is an active link', () => {
+ beforeEach(() => {
+ jest.spyOn(editor, 'isActive');
+ editor.isActive.mockReturnValueOnce(true);
+ buildWrapper();
+ });
+
+ it('sets dropdown as active when link extension is active', () => {
+ expect(findDropdown().props('toggleClass')).toEqual({ active: true });
+ });
+
+ it('displays a remove link dropdown option', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
+ expect(wrapper.findByText('Remove link').exists()).toBe(true);
+ });
+
+ it('executes removeLink command when the remove link option is clicked', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'run']);
+
+ await findRemoveLinkButton().trigger('click');
+
+ expect(commands.unsetLink).toHaveBeenCalled();
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.run).toHaveBeenCalled();
+ });
+
+ it('updates the link with a new link when "Apply" button is clicked', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
+
+ await findLinkURLInput().setValue('https://example');
+ await findApplyLinkButton().trigger('click');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.unsetLink).toHaveBeenCalled();
+ expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('when there is not an active link', () => {
+ beforeEach(() => {
+ jest.spyOn(editor, 'isActive');
+ editor.isActive.mockReturnValueOnce(false);
+ buildWrapper();
+ });
+
+ it('does not set dropdown as active', () => {
+ expect(findDropdown().props('toggleClass')).toEqual({ active: false });
+ });
+
+ it('does not display a remove link dropdown option', () => {
+ expect(findDropdownDivider().exists()).toBe(false);
+ expect(wrapper.findByText('Remove link').exists()).toBe(false);
+ });
+
+ it('sets the link to the value in the URL input when "Apply" button is clicked', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
+
+ await findLinkURLInput().setValue('https://example');
+ await findApplyLinkButton().trigger('click');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.setLink).toHaveBeenCalledWith({ href: 'https://example' });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the user displays the dropdown', () => {
+ let commands;
+
+ beforeEach(() => {
+ commands = mockChainedCommands(editor, ['focus', 'extendMarkRange', 'run']);
+ });
+
+ describe('given the user has not selected text', () => {
+ beforeEach(() => {
+ hasSelection.mockReturnValueOnce(false);
+ });
+
+ it('the editor selection is extended to the current mark extent', () => {
+ buildWrapper();
+
+ findDropdown().vm.$emit('show');
+ expect(commands.extendMarkRange).toHaveBeenCalledWith(Link.name);
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+
+ describe('given the user has selected text', () => {
+ beforeEach(() => {
+ hasSelection.mockReturnValueOnce(true);
+ });
+
+ it('the editor does not modify the current selection', () => {
+ buildWrapper();
+
+ findDropdown().vm.$emit('show');
+ expect(commands.extendMarkRange).not.toHaveBeenCalled();
+ expect(commands.focus).not.toHaveBeenCalled();
+ expect(commands.run).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
new file mode 100644
index 00000000000..8c54f6bb8bb
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -0,0 +1,131 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
+import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
+import { createTestContentEditorExtension, createTestEditor } from '../test_utils';
+
+describe('content_editor/components/toolbar_headings_dropdown', () => {
+ let wrapper;
+ let tiptapEditor;
+ let commandMocks;
+
+ const buildEditor = () => {
+ const testExtension = createTestContentEditorExtension({
+ commands: TEXT_STYLE_DROPDOWN_ITEMS.map((item) => item.editorCommand),
+ });
+
+ commandMocks = testExtension.commandMocks;
+ tiptapEditor = createTestEditor({
+ extensions: [testExtension.tiptapExtension],
+ });
+
+ jest.spyOn(tiptapEditor, 'isActive');
+ };
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = shallowMountExtended(ToolbarTextStyleDropdown, {
+ stubs: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ propsData: {
+ tiptapEditor,
+ ...propsData,
+ },
+ });
+ };
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+
+ beforeEach(() => {
+ buildEditor();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders all text styles as dropdown items', () => {
+ buildWrapper();
+
+ TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle) => {
+ expect(wrapper.findByText(textStyle.label).exists()).toBe(true);
+ });
+ });
+
+ describe('when there is an active item ', () => {
+ let activeTextStyle;
+
+ beforeEach(() => {
+ [, activeTextStyle] = TEXT_STYLE_DROPDOWN_ITEMS;
+
+ tiptapEditor.isActive.mockImplementation(
+ (contentType, params) =>
+ activeTextStyle.contentType === contentType && activeTextStyle.commandParams === params,
+ );
+
+ buildWrapper();
+ });
+
+ it('displays the active text style label as the dropdown toggle text ', () => {
+ expect(findDropdown().props().text).toBe(activeTextStyle.label);
+ });
+
+ it('sets dropdown as enabled', () => {
+ expect(findDropdown().props().disabled).toBe(false);
+ });
+
+ it('sets active item as active', () => {
+ const activeItem = wrapper
+ .findAllComponents(GlDropdownItem)
+ .filter((item) => item.text() === activeTextStyle.label)
+ .at(0);
+ expect(activeItem.props().isChecked).toBe(true);
+ });
+ });
+
+ describe('when there isn’t an active item', () => {
+ beforeEach(() => {
+ tiptapEditor.isActive.mockReturnValue(false);
+ buildWrapper();
+ });
+
+ it('sets dropdown as disabled', () => {
+ expect(findDropdown().props().disabled).toBe(true);
+ });
+
+ it('sets dropdown toggle text to Text style', () => {
+ expect(findDropdown().props().text).toBe('Text style');
+ });
+ });
+
+ describe('when a text style is selected', () => {
+ it('executes the tiptap command related to that text style', () => {
+ buildWrapper();
+
+ TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
+ const { editorCommand, commandParams } = textStyle;
+
+ wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
+ expect(commandMocks[editorCommand]).toHaveBeenCalledWith(commandParams || {});
+ });
+ });
+
+ it('emits execute event with contentType and value params that indicates the heading level', () => {
+ TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
+ buildWrapper();
+ const { contentType, commandParams } = textStyle;
+
+ wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
+ expect(wrapper.emitted('execute')).toEqual([
+ [
+ {
+ contentType,
+ value: commandParams?.level,
+ },
+ ],
+ ]);
+ wrapper.destroy();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/top_toolbar_spec.js b/spec/frontend/content_editor/components/top_toolbar_spec.js
index 8f47be3f489..0a1405a1774 100644
--- a/spec/frontend/content_editor/components/top_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/top_toolbar_spec.js
@@ -39,32 +39,35 @@ describe('content_editor/components/top_toolbar', () => {
});
describe.each`
- testId | buttonProps
+ testId | controlProps
${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
- `('given a $testId toolbar control', ({ testId, buttonProps }) => {
+ ${'code-block'} | ${{ contentType: 'codeBlock', iconName: 'doc-code', label: 'Insert a code block', editorCommand: 'toggleCodeBlock' }}
+ ${'text-styles'} | ${{}}
+ ${'link'} | ${{}}
+ `('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
});
it('renders the toolbar control with the provided properties', () => {
expect(wrapper.findByTestId(testId).props()).toEqual({
- ...buttonProps,
+ ...controlProps,
tiptapEditor: contentEditor.tiptapEditor,
});
});
it.each`
- control | eventData
- ${'bold'} | ${{ contentType: 'bold' }}
- ${'blockquote'} | ${{ contentType: 'blockquote', value: 1 }}
- `('tracks the execution of toolbar controls', ({ control, eventData }) => {
+ eventData
+ ${{ contentType: 'bold' }}
+ ${{ contentType: 'blockquote', value: 1 }}
+ `('tracks the execution of toolbar controls', ({ eventData }) => {
const { contentType, value } = eventData;
- wrapper.findByTestId(control).vm.$emit('execute', eventData);
+ wrapper.findByTestId(testId).vm.$emit('execute', eventData);
expect(trackingSpy).toHaveBeenCalledWith(undefined, TOOLBAR_CONTROL_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL,
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
new file mode 100644
index 00000000000..cc695ffe241
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -0,0 +1,37 @@
+import { tiptapExtension as CodeBlockHighlight } from '~/content_editor/extensions/code_block_highlight';
+import { loadMarkdownApiResult } from '../markdown_processing_examples';
+import { createTestEditor } from '../test_utils';
+
+describe('content_editor/extensions/code_block_highlight', () => {
+ let codeBlockHtmlFixture;
+ let parsedCodeBlockHtmlFixture;
+ let tiptapEditor;
+
+ const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
+ const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
+
+ beforeEach(() => {
+ const { html } = loadMarkdownApiResult('code_block');
+
+ tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
+ codeBlockHtmlFixture = html;
+ parsedCodeBlockHtmlFixture = parseHTML(codeBlockHtmlFixture);
+
+ tiptapEditor.commands.setContent(codeBlockHtmlFixture);
+ });
+
+ it('extracts language and params attributes from Markdown API output', () => {
+ const language = preElement().getAttribute('lang');
+
+ expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({
+ language,
+ params: language,
+ });
+ });
+
+ it('adds code, highlight, and js-syntax-highlight to code block element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+
+ expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
new file mode 100644
index 00000000000..026b2a06df3
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -0,0 +1,61 @@
+import {
+ markdownLinkSyntaxInputRuleRegExp,
+ urlSyntaxRegExp,
+ extractHrefFromMarkdownLink,
+} from '~/content_editor/extensions/link';
+
+describe('content_editor/extensions/link', () => {
+ describe.each`
+ input | matches
+ ${'[gitlab](https://gitlab.com)'} | ${true}
+ ${'[documentation](readme.md)'} | ${true}
+ ${'[link 123](readme.md)'} | ${true}
+ ${'[link 123](read me.md)'} | ${true}
+ ${'text'} | ${false}
+ ${'documentation](readme.md'} | ${false}
+ ${'https://www.google.com'} | ${false}
+ `('markdownLinkSyntaxInputRuleRegExp', ({ input, matches }) => {
+ it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
+ const match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
+
+ expect(Boolean(match?.groups.href)).toBe(matches);
+ });
+ });
+
+ describe.each`
+ input | matches
+ ${'http://example.com '} | ${true}
+ ${'https://example.com '} | ${true}
+ ${'www.example.com '} | ${true}
+ ${'example.com/ab.html '} | ${false}
+ ${'text'} | ${false}
+ ${' http://example.com '} | ${true}
+ ${'https://www.google.com '} | ${true}
+ `('urlSyntaxRegExp', ({ input, matches }) => {
+ it(`${matches ? 'matches' : 'does not match'} ${input}`, () => {
+ const match = new RegExp(urlSyntaxRegExp).exec(input);
+
+ expect(Boolean(match?.groups.href)).toBe(matches);
+ });
+ });
+
+ describe('extractHrefFromMarkdownLink', () => {
+ const input = '[gitlab](https://gitlab.com)';
+ const href = 'https://gitlab.com';
+ let match;
+ let result;
+
+ beforeEach(() => {
+ match = new RegExp(markdownLinkSyntaxInputRuleRegExp).exec(input);
+ result = extractHrefFromMarkdownLink(match);
+ });
+
+ it('extracts the url from a markdown link captured by markdownLinkSyntaxInputRuleRegExp', () => {
+ expect(result).toEqual({ href });
+ });
+
+ it('makes sure that url text is the last capture group', () => {
+ expect(match[match.length - 1]).toEqual('gitlab');
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
index 437714ba938..cf74b5c56c9 100644
--- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
+++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
@@ -5,11 +5,8 @@ import { Heading } from '@tiptap/extension-heading';
import { ListItem } from '@tiptap/extension-list-item';
import { Paragraph } from '@tiptap/extension-paragraph';
import { Text } from '@tiptap/extension-text';
-import { Editor, EditorContent } from '@tiptap/vue-2';
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { Editor } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
KEYBOARD_SHORTCUT_TRACKING_ACTION,
INPUT_RULE_TRACKING_ACTION,
@@ -19,47 +16,33 @@ import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_r
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
describe('content_editor/services/track_input_rules_and_shortcuts', () => {
- let wrapper;
let trackingSpy;
let editor;
+ let trackedExtensions;
const HEADING_TEXT = 'Heading text';
-
- const buildWrapper = () => {
- wrapper = extendedWrapper(
- mount(EditorContent, {
- propsData: {
- editor,
- },
- }),
- );
- };
+ const extensions = [Document, Paragraph, Text, Heading, CodeBlockLowlight, BulletList, ListItem];
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('given the heading extension is instrumented', () => {
beforeEach(() => {
+ trackedExtensions = extensions.map(trackInputRulesAndShortcuts);
editor = new Editor({
- extensions: [
- Document,
- Paragraph,
- Text,
- Heading,
- CodeBlockLowlight,
- BulletList,
- ListItem,
- ].map(trackInputRulesAndShortcuts),
+ extensions: extensions.map(trackInputRulesAndShortcuts),
});
});
- beforeEach(async () => {
- buildWrapper();
- await nextTick();
+ it('does not remove existing keyboard shortcuts', () => {
+ extensions.forEach((extension, index) => {
+ const originalShortcuts = Object.keys(extension.addKeyboardShortcuts?.() || {});
+ const trackedShortcuts = Object.keys(
+ trackedExtensions[index].addKeyboardShortcuts?.() || {},
+ );
+
+ expect(originalShortcuts).toEqual(trackedShortcuts);
+ });
});
describe('when creating a heading using an keyboard shortcut', () => {
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index a92ceb6d058..8e73aef678b 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -1,34 +1,106 @@
import { Node } from '@tiptap/core';
+import { Document } from '@tiptap/extension-document';
+import { Paragraph } from '@tiptap/extension-paragraph';
+import { Text } from '@tiptap/extension-text';
+import { Editor } from '@tiptap/vue-2';
-export const createTestContentEditorExtension = () => ({
- tiptapExtension: Node.create({
- name: 'label',
- priority: 101,
- inline: true,
- group: 'inline',
- addAttributes() {
- return {
- labelName: {
- default: null,
- parseHTML: (element) => {
- return { labelName: element.dataset.labelName };
+/**
+ * Creates an instance of the Tiptap Editor class
+ * with a minimal configuration for testing purposes.
+ *
+ * It only includes the Document, Text, and Paragraph
+ * extensions.
+ *
+ * @param {Array} config.extensions One or more extensions to
+ * include in the editor
+ * @returns An instance of a Tiptap’s Editor class
+ */
+export const createTestEditor = ({ extensions = [] }) => {
+ return new Editor({
+ extensions: [Document, Text, Paragraph, ...extensions],
+ });
+};
+
+export const mockChainedCommands = (editor, commandNames = []) => {
+ const commandMocks = commandNames.reduce(
+ (accum, commandName) => ({
+ ...accum,
+ [commandName]: jest.fn(),
+ }),
+ {},
+ );
+
+ Object.keys(commandMocks).forEach((commandName) => {
+ commandMocks[commandName].mockReturnValue(commandMocks);
+ });
+
+ jest.spyOn(editor, 'chain').mockImplementation(() => commandMocks);
+
+ return commandMocks;
+};
+
+/**
+ * Creates a Content Editor extension for testing
+ * purposes.
+ *
+ * @param {Array} config.commands A list of command names
+ * to include in the test extension. This utility will create
+ * Jest mock functions for each command name.
+ * @returns An object with the following properties:
+ *
+ * tiptapExtension A Node tiptap extension
+ * commandMocks Jest mock functions for each created command
+ * serializer A markdown serializer for the extension
+ */
+export const createTestContentEditorExtension = ({ commands = [] } = {}) => {
+ const commandMocks = commands.reduce(
+ (accum, commandName) => ({
+ ...accum,
+ [commandName]: jest.fn(),
+ }),
+ {},
+ );
+
+ return {
+ commandMocks,
+ tiptapExtension: Node.create({
+ name: 'label',
+ priority: 101,
+ inline: true,
+ group: 'inline',
+ addCommands() {
+ return commands.reduce(
+ (accum, commandName) => ({
+ ...accum,
+ [commandName]: (...params) => () => commandMocks[commandName](...params),
+ }),
+ {},
+ );
+ },
+ addAttributes() {
+ return {
+ labelName: {
+ default: null,
+ parseHTML: (element) => {
+ return { labelName: element.dataset.labelName };
+ },
},
- },
- };
- },
- parseHTML() {
- return [
- {
- tag: 'span[data-reference="label"]',
- },
- ];
- },
- renderHTML({ HTMLAttributes }) {
- return ['span', HTMLAttributes, 0];
+ };
+ },
+ parseHTML() {
+ return [
+ {
+ tag: 'span[data-reference="label"]',
+ },
+ ];
+ },
+ renderHTML({ HTMLAttributes }) {
+ return ['span', HTMLAttributes, 0];
+ },
+ }),
+ serializer: (state, node) => {
+ state.write(`~${node.attrs.labelName}`);
+ state.closeBlock(node);
},
- }),
- serializer: (state, node) => {
- state.write(`~${node.attrs.labelName}`);
- state.closeBlock(node);
- },
-});
+ };
+};
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index f10cf4b4140..8d7b22fe4ff 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -24,7 +24,7 @@ import {
CREATE_CLUSTER_ERROR,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import createState from '~/create_cluster/eks_cluster/store/state';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/flash');
@@ -358,7 +358,9 @@ describe('EKS Cluster Store Actions', () => {
testAction(actions.createClusterError, payload, state, [
{ type: CREATE_CLUSTER_ERROR, payload },
]).then(() => {
- expect(createFlash).toHaveBeenCalledWith(payload.name[0]);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: payload.name[0],
+ });
}));
});
});
diff --git a/spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap b/spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap
new file mode 100644
index 00000000000..1af612ed029
--- /dev/null
+++ b/spec/frontend/cycle_analytics/__snapshots__/base_spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Value stream analytics component isEmptyStage = true renders the empty stage with \`Not enough data\` message 1`] = `"<gl-empty-state-stub title=\\"We don't have enough data to show this stage.\\" svgpath=\\"path/to/no/data\\" description=\\"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
+
+exports[`Value stream analytics component isEmptyStage = true with a selectedStageError renders the empty stage with \`There is too much data to calculate\` message 1`] = `"<gl-empty-state-stub title=\\"There is too much data to calculate\\" svgpath=\\"path/to/no/data\\" description=\\"\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
+
+exports[`Value stream analytics component isLoading = true renders the path navigation component with prop \`loading\` set to true 1`] = `"<path-navigation-stub loading=\\"true\\" stages=\\"\\" selectedstage=\\"[object Object]\\" class=\\"js-path-navigation gl-w-full gl-pb-2\\"></path-navigation-stub>"`;
+
+exports[`Value stream analytics component without enough permissions renders the empty stage with \`You need permission\` message 1`] = `"<gl-empty-state-stub title=\\"You need permission.\\" svgpath=\\"path/to/no/access\\" description=\\"Want to see the data? Please ask an administrator for access.\\" class=\\"js-empty-state\\"></gl-empty-state-stub>"`;
diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js
new file mode 100644
index 00000000000..2f85cc04051
--- /dev/null
+++ b/spec/frontend/cycle_analytics/base_spec.js
@@ -0,0 +1,197 @@
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import BaseComponent from '~/cycle_analytics/components/base.vue';
+import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
+import initState from '~/cycle_analytics/store/state';
+import { selectedStage, convertedEvents as selectedStageEvents } from './mock_data';
+
+const noDataSvgPath = 'path/to/no/data';
+const noAccessSvgPath = 'path/to/no/access';
+
+Vue.use(Vuex);
+
+let wrapper;
+
+function createStore({ initialState = {} }) {
+ return new Vuex.Store({
+ state: {
+ ...initState(),
+ permissions: {
+ [selectedStage.id]: true,
+ },
+ ...initialState,
+ },
+ getters: {
+ pathNavigationData: () => [],
+ },
+ });
+}
+
+function createComponent({ initialState } = {}) {
+ return extendedWrapper(
+ shallowMount(BaseComponent, {
+ store: createStore({ initialState }),
+ propsData: {
+ noDataSvgPath,
+ noAccessSvgPath,
+ },
+ }),
+ );
+}
+
+const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+const findPathNavigation = () => wrapper.findComponent(PathNavigation);
+const findOverviewMetrics = () => wrapper.findByTestId('vsa-stage-overview-metrics');
+const findStageTable = () => wrapper.findByTestId('vsa-stage-table');
+const findEmptyStage = () => wrapper.findComponent(GlEmptyState);
+const findStageEvents = () => wrapper.findByTestId('stage-table-events');
+
+describe('Value stream analytics component', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: {
+ isLoading: false,
+ isLoadingStage: false,
+ isEmptyStage: false,
+ selectedStageEvents,
+ selectedStage,
+ selectedStageError: '',
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders the path navigation component', () => {
+ expect(findPathNavigation().exists()).toBe(true);
+ });
+
+ it('renders the overview metrics', () => {
+ expect(findOverviewMetrics().exists()).toBe(true);
+ });
+
+ it('renders the stage table', () => {
+ expect(findStageTable().exists()).toBe(true);
+ });
+
+ it('renders the stage table events', () => {
+ expect(findEmptyStage().exists()).toBe(false);
+ expect(findStageEvents().exists()).toBe(true);
+ });
+
+ it('does not render the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('isLoading = true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: { isLoading: true },
+ });
+ });
+
+ it('renders the path navigation component with prop `loading` set to true', () => {
+ expect(findPathNavigation().html()).toMatchSnapshot();
+ });
+
+ it('does not render the overview metrics', () => {
+ expect(findOverviewMetrics().exists()).toBe(false);
+ });
+
+ it('does not render the stage table', () => {
+ expect(findStageTable().exists()).toBe(false);
+ });
+
+ it('renders the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('isLoadingStage = true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: { isLoadingStage: true },
+ });
+ });
+
+ it('renders the stage table with a loading icon', () => {
+ const tableWrapper = findStageTable();
+ expect(tableWrapper.exists()).toBe(true);
+ expect(tableWrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('isEmptyStage = true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: { selectedStage, isEmptyStage: true },
+ });
+ });
+
+ it('renders the empty stage with `Not enough data` message', () => {
+ expect(findEmptyStage().html()).toMatchSnapshot();
+ });
+
+ describe('with a selectedStageError', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: {
+ selectedStage,
+ isEmptyStage: true,
+ selectedStageError: 'There is too much data to calculate',
+ },
+ });
+ });
+
+ it('renders the empty stage with `There is too much data to calculate` message', () => {
+ expect(findEmptyStage().html()).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('without enough permissions', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: {
+ permissions: {
+ [selectedStage.id]: false,
+ },
+ },
+ });
+ });
+
+ it('renders the empty stage with `You need permission` message', () => {
+ expect(findEmptyStage().html()).toMatchSnapshot();
+ });
+ });
+
+ describe('without a selected stage', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: { selectedStage: null, isEmptyStage: true },
+ });
+ });
+
+ it('renders the stage table', () => {
+ expect(findStageTable().exists()).toBe(true);
+ });
+
+ it('does not render the path navigation component', () => {
+ expect(findPathNavigation().exists()).toBe(false);
+ });
+
+ it('does not render the stage table events', () => {
+ expect(findStageEvents().exists()).toBe(false);
+ });
+
+ it('does not render the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index 091b574821d..242ea1932fb 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -1,5 +1,11 @@
+import { DEFAULT_VALUE_STREAM } from '~/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+export const getStageByTitle = (stages, title) =>
+ stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {};
+
+export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging'];
+
export const summary = [
{ value: '20', title: 'New Issues' },
{ value: null, title: 'Commits' },
@@ -8,6 +14,7 @@ export const summary = [
];
const issueStage = {
+ id: 'issue',
title: 'Issue',
name: 'issue',
legend: '',
@@ -16,30 +23,34 @@ const issueStage = {
};
const planStage = {
+ id: 'plan',
title: 'Plan',
name: 'plan',
legend: '',
description: 'Time before an issue starts implementation',
- value: 'about 21 hours',
+ value: 75600,
};
const codeStage = {
+ id: 'code',
title: 'Code',
name: 'code',
legend: '',
description: 'Time until first merge request',
- value: '2 days',
+ value: 172800,
};
const testStage = {
+ id: 'test',
title: 'Test',
name: 'test',
legend: '',
description: 'Total test time for all commits/merges',
- value: 'about 5 hours',
+ value: 17550,
};
const reviewStage = {
+ id: 'review',
title: 'Review',
name: 'review',
legend: '',
@@ -48,11 +59,12 @@ const reviewStage = {
};
const stagingStage = {
+ id: 'staging',
title: 'Staging',
name: 'staging',
legend: '',
description: 'From merge request merge until deploy to production',
- value: '2 days',
+ value: 172800,
};
export const selectedStage = {
@@ -84,54 +96,6 @@ export const rawData = {
};
export const convertedData = {
- stages: [
- selectedStage,
- {
- ...planStage,
- active: false,
- isUserAllowed: true,
- emptyStageText:
- 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
- component: 'stage-plan-component',
- slug: 'plan',
- },
- {
- ...codeStage,
- active: false,
- isUserAllowed: true,
- emptyStageText:
- 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
- component: 'stage-code-component',
- slug: 'code',
- },
- {
- ...testStage,
- active: false,
- isUserAllowed: true,
- emptyStageText:
- 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
- component: 'stage-test-component',
- slug: 'test',
- },
- {
- ...reviewStage,
- active: false,
- isUserAllowed: true,
- emptyStageText:
- 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
- component: 'stage-review-component',
- slug: 'review',
- },
- {
- ...stagingStage,
- active: false,
- isUserAllowed: true,
- emptyStageText:
- 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
- component: 'stage-staging-component',
- slug: 'staging',
- },
- ],
summary: [
{ value: '20', title: 'New Issues' },
{ value: '-', title: 'Commits' },
@@ -184,3 +148,110 @@ export const rawEvents = [
export const convertedEvents = rawEvents.map((ev) =>
convertObjectPropsToCamelCase(ev, { deep: true }),
);
+
+export const pathNavIssueMetric = 172800;
+
+export const rawStageMedians = [
+ { id: 'issue', value: 172800 },
+ { id: 'plan', value: 86400 },
+ { id: 'review', value: 1036800 },
+ { id: 'code', value: 129600 },
+ { id: 'test', value: 259200 },
+ { id: 'staging', value: 388800 },
+];
+
+export const stageMedians = {
+ issue: 172800,
+ plan: 86400,
+ review: 1036800,
+ code: 129600,
+ test: 259200,
+ staging: 388800,
+};
+
+export const allowedStages = [issueStage, planStage, codeStage];
+
+export const transformedProjectStagePathData = [
+ {
+ metric: 172800,
+ selected: true,
+ stageCount: undefined,
+ icon: null,
+ id: 'issue',
+ title: 'Issue',
+ name: 'issue',
+ legend: '',
+ description: 'Time before an issue gets scheduled',
+ value: null,
+ },
+ {
+ metric: 86400,
+ selected: false,
+ stageCount: undefined,
+ icon: null,
+ id: 'plan',
+ title: 'Plan',
+ name: 'plan',
+ legend: '',
+ description: 'Time before an issue starts implementation',
+ value: 75600,
+ },
+ {
+ metric: 129600,
+ selected: false,
+ stageCount: undefined,
+ icon: null,
+ id: 'code',
+ title: 'Code',
+ name: 'code',
+ legend: '',
+ description: 'Time until first merge request',
+ value: 172800,
+ },
+];
+
+export const selectedValueStream = DEFAULT_VALUE_STREAM;
+
+export const rawValueStreamStages = [
+ {
+ title: 'Issue',
+ hidden: false,
+ legend: '',
+ description: 'Time before an issue gets scheduled',
+ id: 'issue',
+ custom: false,
+ start_event_html_description:
+ '\u003cp data-sourcepos="1:1-1:13" dir="auto"\u003eIssue created\u003c/p\u003e',
+ end_event_html_description:
+ '\u003cp data-sourcepos="1:1-1:71" dir="auto"\u003eIssue first associated with a milestone or issue first added to a board\u003c/p\u003e',
+ },
+ {
+ title: 'Plan',
+ hidden: false,
+ legend: '',
+ description: 'Time before an issue starts implementation',
+ id: 'plan',
+ custom: false,
+ start_event_html_description:
+ '\u003cp data-sourcepos="1:1-1:71" dir="auto"\u003eIssue first associated with a milestone or issue first added to a board\u003c/p\u003e',
+ end_event_html_description:
+ '\u003cp data-sourcepos="1:1-1:33" dir="auto"\u003eIssue first mentioned in a commit\u003c/p\u003e',
+ },
+ {
+ title: 'Code',
+ hidden: false,
+ legend: '',
+ description: 'Time until first merge request',
+ id: 'code',
+ custom: false,
+ start_event_html_description:
+ '\u003cp data-sourcepos="1:1-1:33" dir="auto"\u003eIssue first mentioned in a commit\u003c/p\u003e',
+ end_event_html_description:
+ '\u003cp data-sourcepos="1:1-1:21" dir="auto"\u003eMerge request created\u003c/p\u003e',
+ },
+];
+
+export const valueStreamStages = rawValueStreamStages.map((s) => ({
+ ...convertObjectPropsToCamelCase(s, { deep: true }),
+ component: `stage-${s.id}-component`,
+}));
diff --git a/spec/frontend/cycle_analytics/path_navigation_spec.js b/spec/frontend/cycle_analytics/path_navigation_spec.js
new file mode 100644
index 00000000000..c6d72d3b571
--- /dev/null
+++ b/spec/frontend/cycle_analytics/path_navigation_spec.js
@@ -0,0 +1,148 @@
+import { GlPath, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import Component from '~/cycle_analytics/components/path_navigation.vue';
+import { transformedProjectStagePathData, selectedStage } from './mock_data';
+
+describe('Project PathNavigation', () => {
+ let wrapper = null;
+ let trackingSpy = null;
+
+ const createComponent = (props) => {
+ return extendedWrapper(
+ mount(Component, {
+ propsData: {
+ stages: transformedProjectStagePathData,
+ selectedStage,
+ loading: false,
+ ...props,
+ },
+ }),
+ );
+ };
+
+ const findPathNavigation = () => {
+ return wrapper.findByTestId('gl-path-nav');
+ };
+
+ const findPathNavigationItems = () => {
+ return findPathNavigation().findAll('li');
+ };
+
+ const findPathNavigationTitles = () => {
+ return findPathNavigation()
+ .findAll('li button')
+ .wrappers.map((w) => w.html());
+ };
+
+ const clickItemAt = (index) => {
+ findPathNavigationItems().at(index).find('button').trigger('click');
+ };
+
+ const pathItemContent = () => findPathNavigationItems().wrappers.map(extendedWrapper);
+ const firstPopover = () => wrapper.findAllByTestId('stage-item-popover').at(0);
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('displays correctly', () => {
+ it('has the correct props', () => {
+ expect(wrapper.find(GlPath).props('items')).toMatchObject(transformedProjectStagePathData);
+ });
+
+ it('contains all the expected stages', () => {
+ const stageContent = findPathNavigationTitles();
+ transformedProjectStagePathData.forEach((stage, index) => {
+ expect(stageContent[index]).toContain(stage.title);
+ });
+ });
+
+ describe('loading', () => {
+ describe('is false', () => {
+ it('displays the gl-path component', () => {
+ expect(wrapper.find(GlPath).exists()).toBe(true);
+ });
+
+ it('hides the gl-skeleton-loading component', () => {
+ expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
+ });
+
+ it('renders each stage', () => {
+ const result = findPathNavigationTitles();
+ expect(result.length).toBe(transformedProjectStagePathData.length);
+ });
+
+ it('renders each stage with its median', () => {
+ const result = findPathNavigationTitles();
+ transformedProjectStagePathData.forEach(({ title, metric }, index) => {
+ expect(result[index]).toContain(title);
+ expect(result[index]).toContain(metric);
+ });
+ });
+
+ describe('popovers', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ stages: transformedProjectStagePathData });
+ });
+
+ it('renders popovers for all stages', () => {
+ pathItemContent().forEach((stage) => {
+ expect(stage.findByTestId('stage-item-popover').exists()).toBe(true);
+ });
+ });
+
+ it('shows the median stage time for the first stage item', () => {
+ expect(firstPopover().text()).toContain('Stage time (median)');
+ });
+ });
+ });
+
+ describe('is true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ loading: true });
+ });
+
+ it('hides the gl-path component', () => {
+ expect(wrapper.find(GlPath).exists()).toBe(false);
+ });
+
+ it('displays the gl-skeleton-loading component', () => {
+ expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('event handling', () => {
+ it('emits the selected event', () => {
+ expect(wrapper.emitted('selected')).toBeUndefined();
+
+ clickItemAt(0);
+ clickItemAt(1);
+ clickItemAt(2);
+
+ expect(wrapper.emitted().selected).toEqual([
+ [transformedProjectStagePathData[0]],
+ [transformedProjectStagePathData[1]],
+ [transformedProjectStagePathData[2]],
+ ]);
+ });
+
+ it('sends tracking information', () => {
+ clickItemAt(0);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_path_navigation', {
+ extra: { stage_id: selectedStage.slug },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js
index 630c5100754..4f37e1266fb 100644
--- a/spec/frontend/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/cycle_analytics/store/actions_spec.js
@@ -3,10 +3,27 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/cycle_analytics/store/actions';
import httpStatusCodes from '~/lib/utils/http_status';
-import { selectedStage } from '../mock_data';
+import { selectedStage, selectedValueStream } from '../mock_data';
const mockRequestPath = 'some/cool/path';
+const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
const mockStartDate = 30;
+const mockRequestedDataActions = ['fetchValueStreams', 'fetchCycleAnalyticsData'];
+const mockInitializeActionCommit = {
+ payload: { requestPath: mockRequestPath },
+ type: 'INITIALIZE_VSA',
+};
+const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' };
+const mockRequestedDataMutations = [
+ {
+ payload: true,
+ type: 'SET_LOADING',
+ },
+ {
+ payload: false,
+ type: 'SET_LOADING',
+ },
+];
describe('Project Value Stream Analytics actions', () => {
let state;
@@ -22,27 +39,26 @@ describe('Project Value Stream Analytics actions', () => {
state = {};
});
- it.each`
- action | type | payload | expectedActions
- ${'initializeVsa'} | ${'INITIALIZE_VSA'} | ${{ requestPath: mockRequestPath }} | ${['fetchCycleAnalyticsData']}
- ${'setDateRange'} | ${'SET_DATE_RANGE'} | ${{ startDate: 30 }} | ${[]}
- ${'setSelectedStage'} | ${'SET_SELECTED_STAGE'} | ${{ selectedStage }} | ${[]}
- `(
- '$action should dispatch $expectedActions and commit $type',
- ({ action, type, payload, expectedActions }) =>
+ const mutationTypes = (arr) => arr.map(({ type }) => type);
+
+ describe.each`
+ action | payload | expectedActions | expectedMutations
+ ${'initializeVsa'} | ${{ requestPath: mockRequestPath }} | ${mockRequestedDataActions} | ${[mockInitializeActionCommit, ...mockRequestedDataMutations]}
+ ${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockRequestedDataActions} | ${[mockSetDateActionCommit, ...mockRequestedDataMutations]}
+ ${'setSelectedStage'} | ${{ selectedStage }} | ${['fetchStageData']} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
+ ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${['fetchValueStreamStages']} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
+ `('$action', ({ action, payload, expectedActions, expectedMutations }) => {
+ const types = mutationTypes(expectedMutations);
+
+ it(`will dispatch ${expectedActions} and commit ${types}`, () =>
testAction({
action: actions[action],
state,
payload,
- expectedMutations: [
- {
- type,
- payload,
- },
- ],
+ expectedMutations,
expectedActions: expectedActions.map((a) => ({ type: a })),
- }),
- );
+ }));
+ });
describe('fetchCycleAnalyticsData', () => {
beforeEach(() => {
@@ -60,7 +76,7 @@ describe('Project Value Stream Analytics actions', () => {
{ type: 'REQUEST_CYCLE_ANALYTICS_DATA' },
{ type: 'RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS' },
],
- expectedActions: [{ type: 'setSelectedStage' }, { type: 'fetchStageData' }],
+ expectedActions: [],
}));
describe('with a failing request', () => {
@@ -85,7 +101,7 @@ describe('Project Value Stream Analytics actions', () => {
});
describe('fetchStageData', () => {
- const mockStagePath = `${mockRequestPath}/events/${selectedStage.name}.json`;
+ const mockStagePath = `${mockRequestPath}/events/${selectedStage.name}`;
beforeEach(() => {
state = {
@@ -106,6 +122,32 @@ describe('Project Value Stream Analytics actions', () => {
expectedActions: [],
}));
+ describe('with a successful request, but an error in the payload', () => {
+ const tooMuchDataError = 'Too much data';
+
+ beforeEach(() => {
+ state = {
+ requestPath: mockRequestPath,
+ startDate: mockStartDate,
+ selectedStage,
+ };
+ mock = new MockAdapter(axios);
+ mock.onGet(mockStagePath).reply(httpStatusCodes.OK, { error: tooMuchDataError });
+ });
+
+ it(`commits the 'RECEIVE_STAGE_DATA_ERROR' mutation`, () =>
+ testAction({
+ action: actions.fetchStageData,
+ state,
+ payload: { error: tooMuchDataError },
+ expectedMutations: [
+ { type: 'REQUEST_STAGE_DATA' },
+ { type: 'RECEIVE_STAGE_DATA_ERROR', payload: tooMuchDataError },
+ ],
+ expectedActions: [],
+ }));
+ });
+
describe('with a failing request', () => {
beforeEach(() => {
state = {
@@ -127,4 +169,115 @@ describe('Project Value Stream Analytics actions', () => {
}));
});
});
+
+ describe('fetchValueStreams', () => {
+ const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/;
+
+ beforeEach(() => {
+ state = {
+ fullPath: mockFullPath,
+ };
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ });
+
+ it(`commits the 'REQUEST_VALUE_STREAMS' mutation`, () =>
+ testAction({
+ action: actions.fetchValueStreams,
+ state,
+ payload: {},
+ expectedMutations: [{ type: 'REQUEST_VALUE_STREAMS' }],
+ expectedActions: [{ type: 'receiveValueStreamsSuccess' }, { type: 'setSelectedStage' }],
+ }));
+
+ describe('with a failing request', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ });
+
+ it(`commits the 'RECEIVE_VALUE_STREAMS_ERROR' mutation`, () =>
+ testAction({
+ action: actions.fetchValueStreams,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'REQUEST_VALUE_STREAMS' },
+ { type: 'RECEIVE_VALUE_STREAMS_ERROR', payload: httpStatusCodes.BAD_REQUEST },
+ ],
+ expectedActions: [],
+ }));
+ });
+ });
+
+ describe('receiveValueStreamsSuccess', () => {
+ const mockValueStream = {
+ id: 'mockDefault',
+ name: 'mock default',
+ };
+ const mockValueStreams = [mockValueStream, selectedValueStream];
+ it('with data, will set the first value stream', () => {
+ testAction({
+ action: actions.receiveValueStreamsSuccess,
+ state,
+ payload: mockValueStreams,
+ expectedMutations: [{ type: 'RECEIVE_VALUE_STREAMS_SUCCESS', payload: mockValueStreams }],
+ expectedActions: [{ type: 'setSelectedValueStream', payload: mockValueStream }],
+ });
+ });
+
+ it('without data, will set the default value stream', () => {
+ testAction({
+ action: actions.receiveValueStreamsSuccess,
+ state,
+ payload: [],
+ expectedMutations: [{ type: 'RECEIVE_VALUE_STREAMS_SUCCESS', payload: [] }],
+ expectedActions: [{ type: 'setSelectedValueStream', payload: selectedValueStream }],
+ });
+ });
+ });
+
+ describe('fetchValueStreamStages', () => {
+ const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/;
+
+ beforeEach(() => {
+ state = {
+ fullPath: mockFullPath,
+ selectedValueStream,
+ };
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ });
+
+ it(`commits the 'REQUEST_VALUE_STREAM_STAGES' and 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS' mutations`, () =>
+ testAction({
+ action: actions.fetchValueStreamStages,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'REQUEST_VALUE_STREAM_STAGES' },
+ { type: 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS' },
+ ],
+ expectedActions: [],
+ }));
+
+ describe('with a failing request', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ });
+
+ it(`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`, () =>
+ testAction({
+ action: actions.fetchValueStreamStages,
+ state,
+ payload: {},
+ expectedMutations: [
+ { type: 'REQUEST_VALUE_STREAM_STAGES' },
+ { type: 'RECEIVE_VALUE_STREAM_STAGES_ERROR', payload: httpStatusCodes.BAD_REQUEST },
+ ],
+ expectedActions: [],
+ }));
+ });
+ });
});
diff --git a/spec/frontend/cycle_analytics/store/getters_spec.js b/spec/frontend/cycle_analytics/store/getters_spec.js
new file mode 100644
index 00000000000..5745e9d7902
--- /dev/null
+++ b/spec/frontend/cycle_analytics/store/getters_spec.js
@@ -0,0 +1,16 @@
+import * as getters from '~/cycle_analytics/store/getters';
+import {
+ allowedStages,
+ stageMedians,
+ transformedProjectStagePathData,
+ selectedStage,
+} from '../mock_data';
+
+describe('Value stream analytics getters', () => {
+ describe('pathNavigationData', () => {
+ it('returns the transformed data', () => {
+ const state = { stages: allowedStages, medians: stageMedians, selectedStage };
+ expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData);
+ });
+ });
+});
diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js
index 08c70af6ef6..88e1a13f506 100644
--- a/spec/frontend/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/cycle_analytics/store/mutations_spec.js
@@ -1,6 +1,15 @@
import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations';
-import { selectedStage, rawEvents, convertedEvents, rawData, convertedData } from '../mock_data';
+import {
+ selectedStage,
+ rawEvents,
+ convertedEvents,
+ rawData,
+ convertedData,
+ selectedValueStream,
+ rawValueStreamStages,
+ valueStreamStages,
+} from '../mock_data';
let state;
const mockRequestPath = 'fake/request/path';
@@ -17,15 +26,15 @@ describe('Project Value Stream Analytics mutations', () => {
it.each`
mutation | stateKey | value
- ${types.SET_SELECTED_STAGE} | ${'isLoadingStage'} | ${false}
+ ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]}
+ ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]}
+ ${types.REQUEST_VALUE_STREAM_STAGES} | ${'stages'} | ${[]}
+ ${types.RECEIVE_VALUE_STREAM_STAGES_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
- ${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'stages'} | ${[]}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'hasError'} | ${false}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${'isLoading'} | ${false}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${'hasError'} | ${false}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'isLoading'} | ${false}
${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'hasError'} | ${true}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.REQUEST_STAGE_DATA} | ${'isEmptyStage'} | ${false}
${types.REQUEST_STAGE_DATA} | ${'hasError'} | ${false}
@@ -44,12 +53,15 @@ describe('Project Value Stream Analytics mutations', () => {
});
it.each`
- mutation | payload | stateKey | value
- ${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
- ${types.SET_SELECTED_STAGE} | ${selectedStage} | ${'selectedStage'} | ${selectedStage}
- ${types.SET_DATE_RANGE} | ${{ startDate: mockStartData }} | ${'startDate'} | ${mockStartData}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'stages'} | ${convertedData.stages}
- ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
+ mutation | payload | stateKey | value
+ ${types.INITIALIZE_VSA} | ${{ requestPath: mockRequestPath }} | ${'requestPath'} | ${mockRequestPath}
+ ${types.SET_DATE_RANGE} | ${{ startDate: mockStartData }} | ${'startDate'} | ${mockStartData}
+ ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
+ ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
+ ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
+ ${types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS} | ${rawData} | ${'summary'} | ${convertedData.summary}
+ ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
+ ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
`(
'$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => {
diff --git a/spec/frontend/cycle_analytics/utils_spec.js b/spec/frontend/cycle_analytics/utils_spec.js
index 73e26e1cdcc..15137bb0571 100644
--- a/spec/frontend/cycle_analytics/utils_spec.js
+++ b/spec/frontend/cycle_analytics/utils_spec.js
@@ -1,5 +1,22 @@
-import { decorateEvents, decorateData } from '~/cycle_analytics/utils';
-import { selectedStage, rawData, convertedData, rawEvents } from './mock_data';
+import {
+ decorateEvents,
+ decorateData,
+ transformStagesForPathNavigation,
+ timeSummaryForPathNavigation,
+ medianTimeToParsedSeconds,
+ formatMedianValues,
+ filterStagesByHiddenStatus,
+} from '~/cycle_analytics/utils';
+import {
+ selectedStage,
+ rawData,
+ convertedData,
+ rawEvents,
+ allowedStages,
+ stageMedians,
+ pathNavIssueMetric,
+ rawStageMedians,
+} from './mock_data';
describe('Value stream analytics utils', () => {
describe('decorateEvents', () => {
@@ -36,17 +53,6 @@ describe('Value stream analytics utils', () => {
expect(result.summary).toEqual(convertedData.summary);
});
- it('returns the stages data', () => {
- expect(result.stages).toEqual(convertedData.stages);
- });
-
- it('returns each of the default value stream stages', () => {
- const stages = result.stages.map(({ name }) => name);
- ['issue', 'plan', 'code', 'test', 'review', 'staging'].forEach((stageName) => {
- expect(stages).toContain(stageName);
- });
- });
-
it('returns `-` for summary data that has no value', () => {
const singleSummaryResult = decorateData({
stats: [],
@@ -55,23 +61,92 @@ describe('Value stream analytics utils', () => {
});
expect(singleSummaryResult.summary).toEqual([{ value: '-', title: 'Commits' }]);
});
+ });
+
+ describe('transformStagesForPathNavigation', () => {
+ const stages = allowedStages;
+ const response = transformStagesForPathNavigation({
+ stages,
+ medians: stageMedians,
+ selectedStage,
+ });
+
+ describe('transforms the data as expected', () => {
+ it('returns an array of stages', () => {
+ expect(Array.isArray(response)).toBe(true);
+ expect(response.length).toBe(stages.length);
+ });
+
+ it('selects the correct stage', () => {
+ const selected = response.filter((stage) => stage.selected === true)[0];
+
+ expect(selected.title).toBe(selectedStage.title);
+ });
+
+ it('includes the correct metric for the associated stage', () => {
+ const issue = response.filter((stage) => stage.name === 'issue')[0];
- it('returns additional fields for each stage', () => {
- const singleStageResult = decorateData({
- stats: [{ name: 'issue', value: null }],
- permissions: { issue: false },
+ expect(issue.metric).toBe(pathNavIssueMetric);
});
- const stage = singleStageResult.stages[0];
- const txt =
- 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.';
-
- expect(stage).toMatchObject({
- active: false,
- isUserAllowed: false,
- emptyStageText: txt,
- slug: 'issue',
- component: 'stage-issue-component',
+ });
+ });
+
+ describe('timeSummaryForPathNavigation', () => {
+ it.each`
+ unit | value | result
+ ${'months'} | ${1.5} | ${'1.5M'}
+ ${'weeks'} | ${1.25} | ${'1.5w'}
+ ${'days'} | ${2} | ${'2d'}
+ ${'hours'} | ${10} | ${'10h'}
+ ${'minutes'} | ${20} | ${'20m'}
+ ${'seconds'} | ${10} | ${'<1m'}
+ ${'seconds'} | ${0} | ${'-'}
+ `('will format $value $unit to $result', ({ unit, value, result }) => {
+ expect(timeSummaryForPathNavigation({ [unit]: value })).toBe(result);
+ });
+ });
+
+ describe('medianTimeToParsedSeconds', () => {
+ it.each`
+ value | result
+ ${1036800} | ${'1w'}
+ ${259200} | ${'3d'}
+ ${172800} | ${'2d'}
+ ${86400} | ${'1d'}
+ ${1000} | ${'16m'}
+ ${61} | ${'1m'}
+ ${59} | ${'<1m'}
+ ${0} | ${'-'}
+ `('will correctly parse $value seconds into $result', ({ value, result }) => {
+ expect(medianTimeToParsedSeconds(value)).toBe(result);
+ });
+ });
+
+ describe('formatMedianValues', () => {
+ const calculatedMedians = formatMedianValues(rawStageMedians);
+
+ it('returns an object with each stage and their median formatted for display', () => {
+ rawStageMedians.forEach(({ id, value }) => {
+ expect(calculatedMedians).toMatchObject({ [id]: medianTimeToParsedSeconds(value) });
});
});
});
+
+ describe('filterStagesByHiddenStatus', () => {
+ const hiddenStages = [{ title: 'three', hidden: true }];
+ const visibleStages = [
+ { title: 'one', hidden: false },
+ { title: 'two', hidden: false },
+ ];
+ const mockStages = [...visibleStages, ...hiddenStages];
+
+ it.each`
+ isHidden | result
+ ${false} | ${visibleStages}
+ ${undefined} | ${hiddenStages}
+ ${true} | ${hiddenStages}
+ `('with isHidden=$isHidden returns matching stages', ({ isHidden, result }) => {
+ expect(filterStagesByHiddenStatus(mockStages, isHidden)).toEqual(result);
+ });
+ });
});
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index 9c784f3c5a2..6bc9c4d374c 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -4,7 +4,7 @@ import Api from '~/api';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
@@ -189,9 +189,9 @@ describe('deploy freeze store actions', () => {
[{ type: types.REQUEST_FREEZE_PERIODS }],
[],
() =>
- expect(createFlash).toHaveBeenCalledWith(
- 'There was an error fetching the deploy freezes.',
- ),
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error fetching the deploy freezes.',
+ }),
);
});
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index db4d69f0176..7012889440c 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -73,6 +73,7 @@ describe('DiffContent', () => {
isParallelView: isParallelViewGetterMock,
getCommentFormForDiffFile: getCommentFormForDiffFileGetterMock,
diffLines: () => () => [...diffFileMockData.parallel_diff_lines],
+ fileLineCodequality: () => () => [],
},
actions: {
saveDiffDiscussion: saveDiffDiscussionMock,
diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js
index 504158fb7fc..4ef1ec55cb0 100644
--- a/spec/frontend/diffs/components/diff_stats_spec.js
+++ b/spec/frontend/diffs/components/diff_stats_spec.js
@@ -1,6 +1,9 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
import DiffStats from '~/diffs/components/diff_stats.vue';
+import mockDiffFile from '../mock_data/diff_file';
const TEST_ADDED_LINES = 100;
const TEST_REMOVED_LINES = 200;
@@ -11,13 +14,15 @@ describe('diff_stats', () => {
let wrapper;
const createComponent = (props = {}) => {
- wrapper = shallowMount(DiffStats, {
- propsData: {
- addedLines: TEST_ADDED_LINES,
- removedLines: TEST_REMOVED_LINES,
- ...props,
- },
- });
+ wrapper = extendedWrapper(
+ shallowMount(DiffStats, {
+ propsData: {
+ addedLines: TEST_ADDED_LINES,
+ removedLines: TEST_REMOVED_LINES,
+ ...props,
+ },
+ }),
+ );
};
describe('diff stats group', () => {
@@ -38,15 +43,43 @@ describe('diff_stats', () => {
});
});
+ describe('bytes changes', () => {
+ let file;
+ const getBytesContainer = () => wrapper.find('.diff-stats > div:first-child');
+
+ beforeEach(() => {
+ file = {
+ ...mockDiffFile,
+ viewer: {
+ ...mockDiffFile.viewer,
+ name: 'not_diffable',
+ },
+ };
+
+ createComponent({ diffFile: file });
+ });
+
+ it("renders the bytes changes instead of line changes when the file isn't diffable", () => {
+ const content = getBytesContainer();
+
+ expect(content.classes('gl-text-green-600')).toBe(true);
+ expect(content.text()).toBe('+1.00 KiB (+100%)');
+ });
+ });
+
describe('line changes', () => {
- const findFileLine = (name) => wrapper.find(name);
+ const findFileLine = (name) => wrapper.findByTestId(name);
+
+ beforeEach(() => {
+ createComponent();
+ });
it('shows the amount of lines added', () => {
- expect(findFileLine('.js-file-addition-line').text()).toBe(TEST_ADDED_LINES.toString());
+ expect(findFileLine('js-file-addition-line').text()).toBe(TEST_ADDED_LINES.toString());
});
it('shows the amount of lines removed', () => {
- expect(findFileLine('.js-file-deletion-line').text()).toBe(TEST_REMOVED_LINES.toString());
+ expect(findFileLine('js-file-deletion-line').text()).toBe(TEST_REMOVED_LINES.toString());
});
});
diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js
index feac88cb802..43b9c5871a6 100644
--- a/spec/frontend/diffs/components/settings_dropdown_spec.js
+++ b/spec/frontend/diffs/components/settings_dropdown_spec.js
@@ -142,7 +142,6 @@ describe('Diff settings dropdown component', () => {
expect(store.dispatch).toHaveBeenCalledWith('diffs/setShowWhitespace', {
showWhitespace: !checked,
- pushState: true,
});
});
});
diff --git a/spec/frontend/diffs/mock_data/diff_file.js b/spec/frontend/diffs/mock_data/diff_file.js
index cef776c885a..9ebcd5ef26b 100644
--- a/spec/frontend/diffs/mock_data/diff_file.js
+++ b/spec/frontend/diffs/mock_data/diff_file.js
@@ -19,6 +19,8 @@ export default {
renamed_file: false,
old_path: 'CHANGELOG',
new_path: 'CHANGELOG',
+ old_size: 1024,
+ new_size: 2048,
mode_changed: false,
a_mode: '100644',
b_mode: '100644',
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index f46a42fae7a..14f8e090be9 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -9,8 +9,6 @@ import {
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
DIFFS_PER_PAGE,
- DIFF_WHITESPACE_COOKIE_NAME,
- SHOW_WHITESPACE,
} from '~/diffs/constants';
import {
setBaseConfig,
@@ -54,7 +52,8 @@ import {
} from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import * as workerUtils from '~/diffs/utils/workers';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -252,7 +251,10 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_MERGE_REQUEST_DIFFS, payload: diffMetadata.merge_request_diffs },
{ type: types.SET_DIFF_METADATA, payload: noFilesData },
// Workers are synchronous in Jest environment (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58805)
- { type: types.SET_TREE_DATA, payload: utils.generateTreeList(diffMetadata.diff_files) },
+ {
+ type: types.SET_TREE_DATA,
+ payload: workerUtils.generateTreeList(diffMetadata.diff_files),
+ },
],
[],
() => {
@@ -293,7 +295,9 @@ describe('DiffsStoreActions', () => {
testAction(fetchCoverageFiles, {}, { endpointCoverage }, [], [], () => {
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('Something went wrong'),
+ });
done();
});
});
@@ -1013,14 +1017,26 @@ describe('DiffsStoreActions', () => {
});
describe('setShowWhitespace', () => {
+ const endpointUpdateUser = 'user/prefs';
+ let putSpy;
+ let mock;
+
beforeEach(() => {
+ mock = new MockAdapter(axios);
+ putSpy = jest.spyOn(axios, 'put');
+
+ mock.onPut(endpointUpdateUser).reply(200, {});
jest.spyOn(eventHub, '$emit').mockImplementation();
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('commits SET_SHOW_WHITESPACE', (done) => {
testAction(
setShowWhitespace,
- { showWhitespace: true },
+ { showWhitespace: true, updateDatabase: false },
{},
[{ type: types.SET_SHOW_WHITESPACE, payload: true }],
[],
@@ -1028,32 +1044,20 @@ describe('DiffsStoreActions', () => {
);
});
- it('sets cookie', () => {
- setShowWhitespace({ commit() {} }, { showWhitespace: true });
-
- expect(Cookies.get(DIFF_WHITESPACE_COOKIE_NAME)).toEqual(SHOW_WHITESPACE);
- });
-
- it('calls history pushState', () => {
- setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
-
- expect(window.history.pushState).toHaveBeenCalled();
- });
-
- it('calls history pushState with merged params', () => {
- window.history.pushState({}, '', '?test=1');
-
- setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
-
- expect(
- window.history.pushState.mock.calls[window.history.pushState.mock.calls.length - 1][2],
- ).toMatch(/(.*)\?test=1&w=0/);
+ it('saves to the database', async () => {
+ await setShowWhitespace(
+ { state: { endpointUpdateUser }, commit() {} },
+ { showWhitespace: true, updateDatabase: true },
+ );
- window.history.pushState({}, '', '?');
+ expect(putSpy).toHaveBeenCalledWith(endpointUpdateUser, { show_whitespace_in_diffs: true });
});
- it('emits eventHub event', () => {
- setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+ it('emits eventHub event', async () => {
+ await setShowWhitespace(
+ { state: {}, commit() {} },
+ { showWhitespace: true, updateDatabase: false },
+ );
expect(eventHub.$emit).toHaveBeenCalledWith('refetchDiffData');
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 6af38590610..73de0a6d381 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -685,141 +685,6 @@ describe('DiffsStoreUtils', () => {
});
});
- describe('generateTreeList', () => {
- let files;
-
- beforeAll(() => {
- files = [
- {
- new_path: 'app/index.js',
- deleted_file: false,
- new_file: false,
- removed_lines: 10,
- added_lines: 0,
- file_hash: 'test',
- },
- {
- new_path: 'app/test/index.js',
- deleted_file: false,
- new_file: true,
- removed_lines: 0,
- added_lines: 0,
- file_hash: 'test',
- },
- {
- new_path: 'app/test/filepathneedstruncating.js',
- deleted_file: false,
- new_file: true,
- removed_lines: 0,
- added_lines: 0,
- file_hash: 'test',
- },
- {
- new_path: 'package.json',
- deleted_file: true,
- new_file: false,
- removed_lines: 0,
- added_lines: 0,
- file_hash: 'test',
- },
- ];
- });
-
- it('creates a tree of files', () => {
- const { tree } = utils.generateTreeList(files);
-
- expect(tree).toEqual([
- {
- key: 'app',
- path: 'app',
- name: 'app',
- type: 'tree',
- tree: [
- {
- addedLines: 0,
- changed: true,
- deleted: false,
- fileHash: 'test',
- key: 'app/index.js',
- name: 'index.js',
- parentPath: 'app/',
- path: 'app/index.js',
- removedLines: 10,
- tempFile: false,
- type: 'blob',
- tree: [],
- },
- {
- key: 'app/test',
- path: 'app/test',
- name: 'test',
- type: 'tree',
- opened: true,
- tree: [
- {
- addedLines: 0,
- changed: true,
- deleted: false,
- fileHash: 'test',
- key: 'app/test/index.js',
- name: 'index.js',
- parentPath: 'app/test/',
- path: 'app/test/index.js',
- removedLines: 0,
- tempFile: true,
- type: 'blob',
- tree: [],
- },
- {
- addedLines: 0,
- changed: true,
- deleted: false,
- fileHash: 'test',
- key: 'app/test/filepathneedstruncating.js',
- name: 'filepathneedstruncating.js',
- parentPath: 'app/test/',
- path: 'app/test/filepathneedstruncating.js',
- removedLines: 0,
- tempFile: true,
- type: 'blob',
- tree: [],
- },
- ],
- },
- ],
- opened: true,
- },
- {
- key: 'package.json',
- parentPath: '/',
- path: 'package.json',
- name: 'package.json',
- type: 'blob',
- changed: true,
- tempFile: false,
- deleted: true,
- fileHash: 'test',
- addedLines: 0,
- removedLines: 0,
- tree: [],
- },
- ]);
- });
-
- it('creates flat list of blobs & folders', () => {
- const { treeEntries } = utils.generateTreeList(files);
-
- expect(Object.keys(treeEntries)).toEqual([
- 'app',
- 'app/index.js',
- 'app/test',
- 'app/test/index.js',
- 'app/test/filepathneedstruncating.js',
- 'package.json',
- ]);
- });
- });
-
describe('getDiffMode', () => {
it('returns mode when matched in file', () => {
expect(
@@ -842,177 +707,6 @@ describe('DiffsStoreUtils', () => {
});
});
- describe('getLowestSingleFolder', () => {
- it('returns path and tree of lowest single folder tree', () => {
- const folder = {
- name: 'app',
- type: 'tree',
- tree: [
- {
- name: 'javascripts',
- type: 'tree',
- tree: [
- {
- type: 'blob',
- name: 'index.js',
- },
- ],
- },
- ],
- };
- const { path, treeAcc } = utils.getLowestSingleFolder(folder);
-
- expect(path).toEqual('app/javascripts');
- expect(treeAcc).toEqual([
- {
- type: 'blob',
- name: 'index.js',
- },
- ]);
- });
-
- it('returns passed in folders path & tree when more than tree exists', () => {
- const folder = {
- name: 'app',
- type: 'tree',
- tree: [
- {
- name: 'spec',
- type: 'blob',
- tree: [],
- },
- ],
- };
- const { path, treeAcc } = utils.getLowestSingleFolder(folder);
-
- expect(path).toEqual('app');
- expect(treeAcc).toBeNull();
- });
- });
-
- describe('flattenTree', () => {
- it('returns flattened directory structure', () => {
- const tree = [
- {
- type: 'tree',
- name: 'app',
- tree: [
- {
- type: 'tree',
- name: 'javascripts',
- tree: [
- {
- type: 'blob',
- name: 'index.js',
- tree: [],
- },
- ],
- },
- ],
- },
- {
- type: 'tree',
- name: 'ee',
- tree: [
- {
- type: 'tree',
- name: 'lib',
- tree: [
- {
- type: 'tree',
- name: 'ee',
- tree: [
- {
- type: 'tree',
- name: 'gitlab',
- tree: [
- {
- type: 'tree',
- name: 'checks',
- tree: [
- {
- type: 'tree',
- name: 'longtreenametomakepath',
- tree: [
- {
- type: 'blob',
- name: 'diff_check.rb',
- tree: [],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- {
- type: 'tree',
- name: 'spec',
- tree: [
- {
- type: 'tree',
- name: 'javascripts',
- tree: [],
- },
- {
- type: 'blob',
- name: 'index_spec.js',
- tree: [],
- },
- ],
- },
- ];
- const flattened = utils.flattenTree(tree);
-
- expect(flattened).toEqual([
- {
- type: 'tree',
- name: 'app/javascripts',
- tree: [
- {
- type: 'blob',
- name: 'index.js',
- tree: [],
- },
- ],
- },
- {
- type: 'tree',
- name: 'ee/lib/…/…/…/longtreenametomakepath',
- tree: [
- {
- name: 'diff_check.rb',
- tree: [],
- type: 'blob',
- },
- ],
- },
- {
- type: 'tree',
- name: 'spec',
- tree: [
- {
- type: 'tree',
- name: 'javascripts',
- tree: [],
- },
- {
- type: 'blob',
- name: 'index_spec.js',
- tree: [],
- },
- ],
- },
- ]);
- });
- });
-
describe('convertExpandLines', () => {
it('converts expanded lines to normal lines', () => {
const diffLines = [
@@ -1058,28 +752,6 @@ describe('DiffsStoreUtils', () => {
});
});
- describe('getDefaultWhitespace', () => {
- it('defaults to true if querystring and cookie are undefined', () => {
- expect(utils.getDefaultWhitespace()).toBe(true);
- });
-
- it('returns false if querystring is `1`', () => {
- expect(utils.getDefaultWhitespace('1', '0')).toBe(false);
- });
-
- it('returns true if querystring is `0`', () => {
- expect(utils.getDefaultWhitespace('0', undefined)).toBe(true);
- });
-
- it('returns false if cookie is `1`', () => {
- expect(utils.getDefaultWhitespace(undefined, '1')).toBe(false);
- });
-
- it('returns true if cookie is `0`', () => {
- expect(utils.getDefaultWhitespace(undefined, '0')).toBe(true);
- });
- });
-
describe('isAdded', () => {
it.each`
type | expected
diff --git a/spec/frontend/diffs/utils/diff_file_spec.js b/spec/frontend/diffs/utils/diff_file_spec.js
index c6cfdfced65..3223b6c2dab 100644
--- a/spec/frontend/diffs/utils/diff_file_spec.js
+++ b/spec/frontend/diffs/utils/diff_file_spec.js
@@ -1,4 +1,11 @@
-import { prepareRawDiffFile, getShortShaFromFile } from '~/diffs/utils/diff_file';
+import {
+ prepareRawDiffFile,
+ getShortShaFromFile,
+ stats,
+ isNotDiffable,
+} from '~/diffs/utils/diff_file';
+import { diffViewerModes } from '~/ide/constants';
+import mockDiffFile from '../mock_data/diff_file';
function getDiffFiles() {
const loadFull = 'namespace/project/-/merge_requests/12345/diff_for_path?file_identifier=abc';
@@ -154,4 +161,73 @@ describe('diff_file utilities', () => {
expect(getShortShaFromFile({ content_sha: cs })).toBe(response);
});
});
+
+ describe('stats', () => {
+ const noFile = [
+ "returns empty stats when the file isn't provided",
+ undefined,
+ {
+ text: '',
+ percent: 0,
+ changed: 0,
+ classes: '',
+ sign: '',
+ valid: false,
+ },
+ ];
+ const validFile = [
+ 'computes the correct stats from a file',
+ mockDiffFile,
+ {
+ changed: 1024,
+ percent: 100,
+ classes: 'gl-text-green-600',
+ sign: '+',
+ text: '+1.00 KiB (+100%)',
+ valid: true,
+ },
+ ];
+ const negativeChange = [
+ 'computed the correct states from a file with a negative size change',
+ {
+ ...mockDiffFile,
+ new_size: 0,
+ old_size: 1024,
+ },
+ {
+ changed: -1024,
+ percent: -100,
+ classes: 'gl-text-red-500',
+ sign: '',
+ text: '-1.00 KiB (-100%)',
+ valid: true,
+ },
+ ];
+
+ it.each([noFile, validFile, negativeChange])('%s', (_, file, output) => {
+ expect(stats(file)).toEqual(output);
+ });
+ });
+
+ describe('isNotDiffable', () => {
+ it.each`
+ bool | vw
+ ${true} | ${diffViewerModes.not_diffable}
+ ${false} | ${diffViewerModes.text}
+ ${false} | ${diffViewerModes.image}
+ `('returns $bool when the viewer is $vw', ({ bool, vw }) => {
+ expect(isNotDiffable({ viewer: { name: vw } })).toBe(bool);
+ });
+
+ it.each`
+ file
+ ${undefined}
+ ${null}
+ ${{}}
+ ${{ viewer: undefined }}
+ ${{ viewer: null }}
+ `('reports `false` when the file is `$file`', ({ file }) => {
+ expect(isNotDiffable(file)).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/diffs/utils/workers_spec.js b/spec/frontend/diffs/utils/workers_spec.js
new file mode 100644
index 00000000000..25d8183b777
--- /dev/null
+++ b/spec/frontend/diffs/utils/workers_spec.js
@@ -0,0 +1,309 @@
+import { generateTreeList, getLowestSingleFolder, flattenTree } from '~/diffs/utils/workers';
+
+describe('~/diffs/utils/workers', () => {
+ describe('generateTreeList', () => {
+ let files;
+
+ beforeAll(() => {
+ files = [
+ {
+ new_path: 'app/index.js',
+ deleted_file: false,
+ new_file: false,
+ removed_lines: 10,
+ added_lines: 0,
+ file_hash: 'test',
+ },
+ {
+ new_path: 'app/test/index.js',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 0,
+ file_hash: 'test',
+ },
+ {
+ new_path: 'app/test/filepathneedstruncating.js',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 0,
+ file_hash: 'test',
+ },
+ {
+ new_path: 'package.json',
+ deleted_file: true,
+ new_file: false,
+ removed_lines: 0,
+ added_lines: 0,
+ file_hash: 'test',
+ },
+ ];
+ });
+
+ it('creates a tree of files', () => {
+ const { tree } = generateTreeList(files);
+
+ expect(tree).toEqual([
+ {
+ key: 'app',
+ path: 'app',
+ name: 'app',
+ type: 'tree',
+ tree: [
+ {
+ addedLines: 0,
+ changed: true,
+ deleted: false,
+ fileHash: 'test',
+ key: 'app/index.js',
+ name: 'index.js',
+ parentPath: 'app/',
+ path: 'app/index.js',
+ removedLines: 10,
+ tempFile: false,
+ type: 'blob',
+ tree: [],
+ },
+ {
+ key: 'app/test',
+ path: 'app/test',
+ name: 'test',
+ type: 'tree',
+ opened: true,
+ tree: [
+ {
+ addedLines: 0,
+ changed: true,
+ deleted: false,
+ fileHash: 'test',
+ key: 'app/test/index.js',
+ name: 'index.js',
+ parentPath: 'app/test/',
+ path: 'app/test/index.js',
+ removedLines: 0,
+ tempFile: true,
+ type: 'blob',
+ tree: [],
+ },
+ {
+ addedLines: 0,
+ changed: true,
+ deleted: false,
+ fileHash: 'test',
+ key: 'app/test/filepathneedstruncating.js',
+ name: 'filepathneedstruncating.js',
+ parentPath: 'app/test/',
+ path: 'app/test/filepathneedstruncating.js',
+ removedLines: 0,
+ tempFile: true,
+ type: 'blob',
+ tree: [],
+ },
+ ],
+ },
+ ],
+ opened: true,
+ },
+ {
+ key: 'package.json',
+ parentPath: '/',
+ path: 'package.json',
+ name: 'package.json',
+ type: 'blob',
+ changed: true,
+ tempFile: false,
+ deleted: true,
+ fileHash: 'test',
+ addedLines: 0,
+ removedLines: 0,
+ tree: [],
+ },
+ ]);
+ });
+
+ it('creates flat list of blobs & folders', () => {
+ const { treeEntries } = generateTreeList(files);
+
+ expect(Object.keys(treeEntries)).toEqual([
+ 'app',
+ 'app/index.js',
+ 'app/test',
+ 'app/test/index.js',
+ 'app/test/filepathneedstruncating.js',
+ 'package.json',
+ ]);
+ });
+ });
+
+ describe('getLowestSingleFolder', () => {
+ it('returns path and tree of lowest single folder tree', () => {
+ const folder = {
+ name: 'app',
+ type: 'tree',
+ tree: [
+ {
+ name: 'javascripts',
+ type: 'tree',
+ tree: [
+ {
+ type: 'blob',
+ name: 'index.js',
+ },
+ ],
+ },
+ ],
+ };
+ const { path, treeAcc } = getLowestSingleFolder(folder);
+
+ expect(path).toEqual('app/javascripts');
+ expect(treeAcc).toEqual([
+ {
+ type: 'blob',
+ name: 'index.js',
+ },
+ ]);
+ });
+
+ it('returns passed in folders path & tree when more than tree exists', () => {
+ const folder = {
+ name: 'app',
+ type: 'tree',
+ tree: [
+ {
+ name: 'spec',
+ type: 'blob',
+ tree: [],
+ },
+ ],
+ };
+ const { path, treeAcc } = getLowestSingleFolder(folder);
+
+ expect(path).toEqual('app');
+ expect(treeAcc).toBeNull();
+ });
+ });
+
+ describe('flattenTree', () => {
+ it('returns flattened directory structure', () => {
+ const tree = [
+ {
+ type: 'tree',
+ name: 'app',
+ tree: [
+ {
+ type: 'tree',
+ name: 'javascripts',
+ tree: [
+ {
+ type: 'blob',
+ name: 'index.js',
+ tree: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'tree',
+ name: 'ee',
+ tree: [
+ {
+ type: 'tree',
+ name: 'lib',
+ tree: [
+ {
+ type: 'tree',
+ name: 'ee',
+ tree: [
+ {
+ type: 'tree',
+ name: 'gitlab',
+ tree: [
+ {
+ type: 'tree',
+ name: 'checks',
+ tree: [
+ {
+ type: 'tree',
+ name: 'longtreenametomakepath',
+ tree: [
+ {
+ type: 'blob',
+ name: 'diff_check.rb',
+ tree: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'tree',
+ name: 'spec',
+ tree: [
+ {
+ type: 'tree',
+ name: 'javascripts',
+ tree: [],
+ },
+ {
+ type: 'blob',
+ name: 'index_spec.js',
+ tree: [],
+ },
+ ],
+ },
+ ];
+ const flattened = flattenTree(tree);
+
+ expect(flattened).toEqual([
+ {
+ type: 'tree',
+ name: 'app/javascripts',
+ tree: [
+ {
+ type: 'blob',
+ name: 'index.js',
+ tree: [],
+ },
+ ],
+ },
+ {
+ type: 'tree',
+ name: 'ee/lib/…/…/…/longtreenametomakepath',
+ tree: [
+ {
+ name: 'diff_check.rb',
+ tree: [],
+ type: 'blob',
+ },
+ ],
+ },
+ {
+ type: 'tree',
+ name: 'spec',
+ tree: [
+ {
+ type: 'tree',
+ name: 'javascripts',
+ tree: [],
+ },
+ {
+ type: 'blob',
+ name: 'index_spec.js',
+ tree: [],
+ },
+ ],
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/editor/editor_ci_schema_ext_spec.js b/spec/frontend/editor/editor_ci_schema_ext_spec.js
index 17a9ae7335f..2f0ecfb151e 100644
--- a/spec/frontend/editor/editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/editor_ci_schema_ext_spec.js
@@ -4,6 +4,8 @@ import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
import EditorLite from '~/editor/editor_lite';
import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
+const mockRef = 'AABBCCDD';
+
describe('~/editor/editor_ci_config_ext', () => {
const defaultBlobPath = '.gitlab-ci.yml';
@@ -75,8 +77,6 @@ describe('~/editor/editor_ci_config_ext', () => {
});
it('with an schema uri that contains project and ref', () => {
- const mockRef = 'AABBCCDD';
-
instance.registerCiSchema({
projectNamespace: mockProjectNamespace,
projectPath: mockProjectPath,
@@ -95,10 +95,11 @@ describe('~/editor/editor_ci_config_ext', () => {
instance.registerCiSchema({
projectNamespace: mockProjectNamespace,
projectPath: mockProjectPath,
+ ref: mockRef,
});
expect(getConfiguredYmlSchema()).toEqual({
- uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
+ uri: `${TEST_HOST}/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
fileMatch: ['another-ci-filename.yml'],
});
});
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index dac4fded260..e96920d1112 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -7,6 +7,10 @@ import axios from '~/lib/utils/axios_utils';
jest.mock('@sentry/browser');
describe('Awards app actions', () => {
+ afterEach(() => {
+ window.gon = {};
+ });
+
describe('setInitialData', () => {
it('commits SET_INITIAL_DATA', async () => {
await testAction(
@@ -31,21 +35,36 @@ describe('Awards app actions', () => {
});
describe('success', () => {
- beforeEach(() => {
- mock
- .onGet('/awards', { params: { per_page: 100, page: '1' } })
- .reply(200, ['thumbsup'], { 'x-next-page': '2' });
- mock.onGet('/awards', { params: { per_page: 100, page: '2' } }).reply(200, ['thumbsdown']);
- });
+ describe.each`
+ relativeRootUrl
+ ${null}
+ ${'/gitlab'}
+ `('with relative_root_url as $relativeRootUrl', ({ relativeRootUrl }) => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: relativeRootUrl };
+ mock
+ .onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '1' } })
+ .reply(200, ['thumbsup'], { 'x-next-page': '2' });
+ mock
+ .onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '2' } })
+ .reply(200, ['thumbsdown']);
+ });
+
+ it('commits FETCH_AWARDS_SUCCESS', async () => {
+ window.gon.current_user_id = 1;
- it('commits FETCH_AWARDS_SUCCESS', async () => {
- await testAction(
- actions.fetchAwards,
- '1',
- { path: '/awards' },
- [{ type: 'FETCH_AWARDS_SUCCESS', payload: ['thumbsup'] }],
- [{ type: 'fetchAwards', payload: '2' }],
- );
+ await testAction(
+ actions.fetchAwards,
+ '1',
+ { path: '/awards' },
+ [{ type: 'FETCH_AWARDS_SUCCESS', payload: ['thumbsup'] }],
+ [{ type: 'fetchAwards', payload: '2' }],
+ );
+ });
+
+ it('does not commit FETCH_AWARDS_SUCCESS when user signed out', async () => {
+ await testAction(actions.fetchAwards, '1', { path: '/awards' }, [], []);
+ });
});
});
@@ -55,6 +74,8 @@ describe('Awards app actions', () => {
});
it('calls Sentry.captureException', async () => {
+ window.gon = { current_user_id: 1 };
+
await testAction(actions.fetchAwards, null, { path: '/awards' }, [], [], () => {
expect(Sentry.captureException).toHaveBeenCalled();
});
@@ -73,81 +94,91 @@ describe('Awards app actions', () => {
mock.restore();
});
- describe('adding new award', () => {
- describe('success', () => {
- beforeEach(() => {
- mock.onPost('/awards').reply(200, { id: 1 });
- });
-
- it('commits ADD_NEW_AWARD', async () => {
- testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
- { type: 'ADD_NEW_AWARD', payload: { id: 1 } },
- ]);
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onPost('/awards').reply(500);
- });
-
- it('calls Sentry.captureException', async () => {
- await testAction(
- actions.toggleAward,
- null,
- { path: '/awards', awards: [] },
- [],
- [],
- () => {
- expect(Sentry.captureException).toHaveBeenCalled();
- },
- );
- });
+ describe.each`
+ relativeRootUrl
+ ${null}
+ ${'/gitlab'}
+ `('with relative_root_url as $relativeRootUrl', ({ relativeRootUrl }) => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: relativeRootUrl };
});
- });
- describe('removing a award', () => {
- const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
-
- describe('success', () => {
- beforeEach(() => {
- mock.onDelete('/awards/1').reply(200);
+ describe('adding new award', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 });
+ });
+
+ it('commits ADD_NEW_AWARD', async () => {
+ testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
+ { type: 'ADD_NEW_AWARD', payload: { id: 1 } },
+ ]);
+ });
});
- it('commits REMOVE_AWARD', async () => {
- testAction(
- actions.toggleAward,
- 'thumbsup',
- {
- path: '/awards',
- currentUserId: 1,
- awards: [mockData],
- },
- [{ type: 'REMOVE_AWARD', payload: 1 }],
- );
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onPost(`${relativeRootUrl || ''}/awards`).reply(500);
+ });
+
+ it('calls Sentry.captureException', async () => {
+ await testAction(
+ actions.toggleAward,
+ null,
+ { path: '/awards', awards: [] },
+ [],
+ [],
+ () => {
+ expect(Sentry.captureException).toHaveBeenCalled();
+ },
+ );
+ });
});
});
- describe('error', () => {
- beforeEach(() => {
- mock.onDelete('/awards/1').reply(500);
+ describe('removing a award', () => {
+ const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(200);
+ });
+
+ it('commits REMOVE_AWARD', async () => {
+ testAction(
+ actions.toggleAward,
+ 'thumbsup',
+ {
+ path: '/awards',
+ currentUserId: 1,
+ awards: [mockData],
+ },
+ [{ type: 'REMOVE_AWARD', payload: 1 }],
+ );
+ });
});
- it('calls Sentry.captureException', async () => {
- await testAction(
- actions.toggleAward,
- 'thumbsup',
- {
- path: '/awards',
- currentUserId: 1,
- awards: [mockData],
- },
- [],
- [],
- () => {
- expect(Sentry.captureException).toHaveBeenCalled();
- },
- );
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500);
+ });
+
+ it('calls Sentry.captureException', async () => {
+ await testAction(
+ actions.toggleAward,
+ 'thumbsup',
+ {
+ path: '/awards',
+ currentUserId: 1,
+ awards: [mockData],
+ },
+ [],
+ [],
+ () => {
+ expect(Sentry.captureException).toHaveBeenCalled();
+ },
+ );
+ });
});
});
});
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index b469a855d23..babbc0c8a4d 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -21,7 +21,7 @@ import {
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
} from '~/error_tracking/utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import Tracking from '~/tracking';
@@ -160,10 +160,10 @@ describe('ErrorDetails', () => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(GlLink).exists()).toBe(false);
- expect(createFlash).toHaveBeenCalledWith(
- 'Could not connect to Sentry. Refresh the page to try again.',
- 'warning',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Could not connect to Sentry. Refresh the page to try again.',
+ type: 'warning',
+ });
expect(mocks.$apollo.queries.error.stopPolling).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index 9d598344acd..aaaa1194a29 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 0c19dce7bad..623cb82851d 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 39481a8576f..5465bde397c 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
diff --git a/spec/frontend/feature_flags/components/feature_flags_tab_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js
index c2170e8a768..86d0c1a05fd 100644
--- a/spec/frontend/feature_flags/components/feature_flags_tab_spec.js
+++ b/spec/frontend/feature_flags/components/empty_state_spec.js
@@ -1,16 +1,14 @@
-import { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import FeatureFlagsTab from '~/feature_flags/components/feature_flags_tab.vue';
+import EmptyState from '~/feature_flags/components/empty_state.vue';
const DEFAULT_PROPS = {
- title: 'test',
- count: 5,
alerts: ['an alert', 'another alert'],
isLoading: false,
loadingLabel: 'test loading',
errorState: false,
errorTitle: 'test title',
- emptyState: true,
+ emptyState: false,
emptyTitle: 'test empty',
emptyDescription: 'empty description',
};
@@ -27,13 +25,10 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
mount(
{
components: {
- GlTabs,
- FeatureFlagsTab,
+ EmptyState,
},
render(h) {
- return h(GlTabs, [
- h(FeatureFlagsTab, { props: this.$attrs, on: this.$listeners }, this.$slots.default),
- ]);
+ return h(EmptyState, { props: this.$attrs, on: this.$listeners }, this.$slots.default);
},
},
{
@@ -72,7 +67,7 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
it('should emit a dismiss event for a dismissed alert', () => {
alerts.at(0).vm.$emit('dismiss');
- expect(wrapper.find(FeatureFlagsTab).emitted('dismissAlert')).toEqual([[0]]);
+ expect(wrapper.find(EmptyState).emitted('dismissAlert')).toEqual([[0]]);
});
});
@@ -138,30 +133,4 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
expect(slot.text()).toBe('testing');
});
});
-
- describe('count', () => {
- it('should display a count if there is one', async () => {
- wrapper = factory();
- await wrapper.vm.$nextTick();
-
- expect(wrapper.find(GlBadge).text()).toBe(DEFAULT_PROPS.count.toString());
- });
- it('should display 0 if there is no count', async () => {
- wrapper = factory({ count: undefined });
- await wrapper.vm.$nextTick();
-
- expect(wrapper.find(GlBadge).text()).toBe('0');
- });
- });
-
- describe('title', () => {
- it('should show the title', async () => {
- wrapper = factory();
- await wrapper.vm.$nextTick();
-
- expect(wrapper.find('[data-testid="feature-flags-tab-title"]').text()).toBe(
- DEFAULT_PROPS.title,
- );
- });
- });
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index b519aab0dc4..db4bdc736de 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -1,19 +1,17 @@
-import { GlAlert, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
-import Api from '~/api';
import ConfigureFeatureFlagsModal from '~/feature_flags/components/configure_feature_flags_modal.vue';
+import EmptyState from '~/feature_flags/components/empty_state.vue';
import FeatureFlagsComponent from '~/feature_flags/components/feature_flags.vue';
-import FeatureFlagsTab from '~/feature_flags/components/feature_flags_tab.vue';
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
-import UserListsTable from '~/feature_flags/components/user_lists_table.vue';
-import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '~/feature_flags/constants';
import createStore from '~/feature_flags/store/index';
import axios from '~/lib/utils/axios_utils';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import { getRequestData, userList } from '../mock_data';
+import { getRequestData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -28,7 +26,7 @@ describe('Feature flags', () => {
featureFlagsLimit: '200',
featureFlagsLimitExceeded: false,
newFeatureFlagPath: 'feature-flags/new',
- newUserListPath: '/user-list/new',
+ userListPath: '/user-list',
unleashApiUrl: `${TEST_HOST}/api/unleash`,
projectName: 'fakeProjectName',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
@@ -44,36 +42,25 @@ describe('Feature flags', () => {
let mock;
let store;
- const factory = (provide = mockData, fn = shallowMount) => {
+ const factory = (provide = mockData, fn = mount) => {
store = createStore(mockState);
wrapper = fn(FeatureFlagsComponent, {
localVue,
store,
provide,
stubs: {
- FeatureFlagsTab,
+ EmptyState,
},
});
};
const configureButton = () => wrapper.find('[data-testid="ff-configure-button"]');
const newButton = () => wrapper.find('[data-testid="ff-new-button"]');
- const newUserListButton = () => wrapper.find('[data-testid="ff-new-list-button"]');
- const limitAlert = () => wrapper.find(GlAlert);
+ const userListButton = () => wrapper.find('[data-testid="ff-user-list-button"]');
+ const limitAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
mock = new MockAdapter(axios);
- jest.spyOn(Api, 'fetchFeatureFlagUserLists').mockResolvedValue({
- data: [userList],
- headers: {
- 'x-next-page': '2',
- 'x-page': '1',
- 'X-Per-Page': '8',
- 'X-Prev-Page': '',
- 'X-TOTAL': '40',
- 'X-Total-Pages': '5',
- },
- });
});
afterEach(() => {
@@ -87,7 +74,7 @@ describe('Feature flags', () => {
beforeEach((done) => {
mock
- .onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
+ .onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory(provideData);
setImmediate(done);
@@ -101,9 +88,7 @@ describe('Feature flags', () => {
it('shows a feature flags limit reached alert', () => {
expect(limitAlert().exists()).toBe(true);
- expect(limitAlert().find(GlSprintf).attributes('message')).toContain(
- 'Feature flags limit reached',
- );
+ expect(limitAlert().text()).toContain('Feature flags limit reached');
});
describe('when the alert is dismissed', () => {
@@ -129,12 +114,12 @@ describe('Feature flags', () => {
canUserConfigure: false,
canUserRotateToken: false,
newFeatureFlagPath: null,
- newUserListPath: null,
+ userListPath: null,
};
beforeEach((done) => {
mock
- .onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
+ .onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory(provideData);
setImmediate(done);
@@ -148,20 +133,20 @@ describe('Feature flags', () => {
expect(newButton().exists()).toBe(false);
});
- it('does not render new user list button', () => {
- expect(newUserListButton().exists()).toBe(false);
+ it('does not render view user list button', () => {
+ expect(userListButton().exists()).toBe(false);
});
});
describe('loading state', () => {
it('renders a loading icon', () => {
mock
- .onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
+ .onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.replyOnce(200, getRequestData, {});
factory();
- const loadingElement = wrapper.find(GlLoadingIcon);
+ const loadingElement = wrapper.findComponent(GlLoadingIcon);
expect(loadingElement.exists()).toBe(true);
expect(loadingElement.props('label')).toEqual('Loading feature flags');
@@ -173,7 +158,7 @@ describe('Feature flags', () => {
let emptyState;
beforeEach(async () => {
- mock.onGet(mockState.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } }).reply(
+ mock.onGet(mockState.endpoint, { params: { page: '1' } }).reply(
200,
{
feature_flags: [],
@@ -187,9 +172,10 @@ describe('Feature flags', () => {
);
factory();
+ await waitForPromises();
await wrapper.vm.$nextTick();
- emptyState = wrapper.find(GlEmptyState);
+ emptyState = wrapper.findComponent(GlEmptyState);
});
it('should render the empty state', async () => {
@@ -204,9 +190,9 @@ describe('Feature flags', () => {
expect(newButton().exists()).toBe(true);
});
- it('renders new user list button', () => {
- expect(newUserListButton().exists()).toBe(true);
- expect(newUserListButton().attributes('href')).toBe('/user-list/new');
+ it('renders view user list button', () => {
+ expect(userListButton().exists()).toBe(true);
+ expect(userListButton().attributes('href')).toBe(mockData.userListPath);
});
describe('in feature flags tab', () => {
@@ -218,16 +204,14 @@ describe('Feature flags', () => {
describe('with paginated feature flags', () => {
beforeEach((done) => {
- mock
- .onGet(mockState.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, 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(200, 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');
@@ -235,9 +219,9 @@ describe('Feature flags', () => {
});
it('should render a table with feature flags', () => {
- const table = wrapper.find(FeatureFlagsTable);
+ const table = wrapper.findComponent(FeatureFlagsTable);
expect(table.exists()).toBe(true);
- expect(table.props(FEATURE_FLAG_SCOPE)).toEqual(
+ expect(table.props('featureFlags')).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: getRequestData.feature_flags[0].name,
@@ -248,9 +232,9 @@ describe('Feature flags', () => {
});
it('should toggle a flag when receiving the toggle-flag event', () => {
- const table = wrapper.find(FeatureFlagsTable);
+ const table = wrapper.findComponent(FeatureFlagsTable);
- const [flag] = table.props(FEATURE_FLAG_SCOPE);
+ const [flag] = table.props('featureFlags');
table.vm.$emit('toggle-flag', flag);
expect(store.dispatch).toHaveBeenCalledWith('toggleFeatureFlag', flag);
@@ -264,71 +248,38 @@ describe('Feature flags', () => {
expect(newButton().exists()).toBe(true);
});
- it('renders new user list button', () => {
- expect(newUserListButton().exists()).toBe(true);
- expect(newUserListButton().attributes('href')).toBe('/user-list/new');
+ it('renders view user list button', () => {
+ expect(userListButton().exists()).toBe(true);
+ expect(userListButton().attributes('href')).toBe(mockData.userListPath);
});
describe('pagination', () => {
it('should render pagination', () => {
- expect(wrapper.find(TablePagination).exists()).toBe(true);
+ expect(wrapper.findComponent(TablePagination).exists()).toBe(true);
});
it('should make an API request when page is clicked', () => {
jest.spyOn(wrapper.vm, 'updateFeatureFlagOptions');
- wrapper.find(TablePagination).vm.change(4);
+ wrapper.findComponent(TablePagination).vm.change(4);
expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
- scope: FEATURE_FLAG_SCOPE,
page: '4',
});
});
-
- it('should make an API request when using tabs', () => {
- jest.spyOn(wrapper.vm, 'updateFeatureFlagOptions');
- wrapper.find('[data-testid="user-lists-tab"]').vm.$emit('changeTab');
-
- expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
- scope: USER_LIST_SCOPE,
- page: '1',
- });
- });
- });
- });
-
- describe('in user lists tab', () => {
- beforeEach((done) => {
- factory();
- setImmediate(done);
- });
- beforeEach(() => {
- wrapper.find('[data-testid="user-lists-tab"]').vm.$emit('changeTab');
- return wrapper.vm.$nextTick();
- });
-
- it('should display the user list table', () => {
- expect(wrapper.find(UserListsTable).exists()).toBe(true);
- });
-
- it('should set the user lists to display', () => {
- expect(wrapper.find(UserListsTable).props('userLists')).toEqual([userList]);
});
});
});
describe('unsuccessful request', () => {
beforeEach((done) => {
- mock
- .onGet(mockState.endpoint, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
- .replyOnce(500, {});
- Api.fetchFeatureFlagUserLists.mockRejectedValueOnce();
+ mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(500, {});
factory();
setImmediate(done);
});
it('should render error state', () => {
- const emptyState = wrapper.find(GlEmptyState);
+ const emptyState = wrapper.findComponent(GlEmptyState);
expect(emptyState.props('title')).toEqual('There was an error fetching the feature flags.');
expect(emptyState.props('description')).toEqual(
'Try again in a few moments or contact your support team.',
@@ -343,16 +294,16 @@ describe('Feature flags', () => {
expect(newButton().exists()).toBe(true);
});
- it('renders new user list button', () => {
- expect(newUserListButton().exists()).toBe(true);
- expect(newUserListButton().attributes('href')).toBe('/user-list/new');
+ it('renders view user list button', () => {
+ expect(userListButton().exists()).toBe(true);
+ expect(userListButton().attributes('href')).toBe(mockData.userListPath);
});
});
describe('rotate instance id', () => {
beforeEach((done) => {
mock
- .onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: FEATURE_FLAG_SCOPE, page: '1' } })
+ .onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
.reply(200, getRequestData, {});
factory();
setImmediate(done);
@@ -360,7 +311,7 @@ describe('Feature flags', () => {
it('should fire the rotate action when a `token` event is received', () => {
const actionSpy = jest.spyOn(wrapper.vm, 'rotateInstanceId');
- const modal = wrapper.find(ConfigureFeatureFlagsModal);
+ const modal = wrapper.findComponent(ConfigureFeatureFlagsModal);
modal.vm.$emit('token');
expect(actionSpy).toHaveBeenCalled();
diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js
index a7ab2e92cb2..ec311ef92a3 100644
--- a/spec/frontend/feature_flags/store/index/actions_spec.js
+++ b/spec/frontend/feature_flags/store/index/actions_spec.js
@@ -1,7 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
-import Api from '~/api';
import { mapToScopesViewModel } from '~/feature_flags/store/helpers';
import {
requestFeatureFlags,
@@ -17,18 +16,12 @@ import {
updateFeatureFlag,
receiveUpdateFeatureFlagSuccess,
receiveUpdateFeatureFlagError,
- requestUserLists,
- receiveUserListsSuccess,
- receiveUserListsError,
- fetchUserLists,
- deleteUserList,
- receiveDeleteUserListError,
clearAlert,
} from '~/feature_flags/store/index/actions';
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 { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
+import { getRequestData, rotateData, featureFlag } from '../../mock_data';
jest.mock('~/api.js');
@@ -154,99 +147,6 @@ describe('Feature flags actions', () => {
});
});
- describe('fetchUserLists', () => {
- beforeEach(() => {
- Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList], headers: {} });
- });
-
- describe('success', () => {
- it('dispatches requestUserLists and receiveUserListsSuccess ', (done) => {
- testAction(
- fetchUserLists,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestUserLists',
- },
- {
- payload: { data: [userList], headers: {} },
- type: 'receiveUserListsSuccess',
- },
- ],
- done,
- );
- });
- });
-
- describe('error', () => {
- it('dispatches requestUserLists and receiveUserListsError ', (done) => {
- Api.fetchFeatureFlagUserLists.mockRejectedValue();
-
- testAction(
- fetchUserLists,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestUserLists',
- },
- {
- type: 'receiveUserListsError',
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('requestUserLists', () => {
- it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
- testAction(
- requestUserLists,
- null,
- mockedState,
- [{ type: types.REQUEST_USER_LISTS }],
- [],
- done,
- );
- });
- });
-
- describe('receiveUserListsSuccess', () => {
- it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
- testAction(
- receiveUserListsSuccess,
- { data: [userList], headers: {} },
- mockedState,
- [
- {
- type: types.RECEIVE_USER_LISTS_SUCCESS,
- payload: { data: [userList], headers: {} },
- },
- ],
- [],
- done,
- );
- });
- });
-
- describe('receiveUserListsError', () => {
- it('should commit RECEIVE_USER_LISTS_ERROR mutation', (done) => {
- testAction(
- receiveUserListsError,
- null,
- mockedState,
- [{ type: types.RECEIVE_USER_LISTS_ERROR }],
- [],
- done,
- );
- });
- });
-
describe('rotateInstanceId', () => {
let mock;
@@ -482,69 +382,6 @@ describe('Feature flags actions', () => {
);
});
});
- describe('deleteUserList', () => {
- beforeEach(() => {
- mockedState.userLists = [userList];
- });
-
- describe('success', () => {
- beforeEach(() => {
- Api.deleteFeatureFlagUserList.mockResolvedValue();
- });
-
- it('should refresh the user lists', (done) => {
- testAction(
- deleteUserList,
- userList,
- mockedState,
- [],
- [{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }],
- done,
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } });
- });
-
- it('should dispatch receiveDeleteUserListError', (done) => {
- testAction(
- deleteUserList,
- userList,
- mockedState,
- [],
- [
- { type: 'requestDeleteUserList', payload: userList },
- {
- type: 'receiveDeleteUserListError',
- payload: { list: userList, error: 'some error' },
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('receiveDeleteUserListError', () => {
- it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', (done) => {
- testAction(
- receiveDeleteUserListError,
- { list: userList, error: 'mock error' },
- mockedState,
- [
- {
- type: 'RECEIVE_DELETE_USER_LIST_ERROR',
- payload: { list: userList, error: 'mock error' },
- },
- ],
- [],
- done,
- );
- });
- });
describe('clearAlert', () => {
it('should commit RECEIVE_CLEAR_ALERT', (done) => {
diff --git a/spec/frontend/feature_flags/store/index/mutations_spec.js b/spec/frontend/feature_flags/store/index/mutations_spec.js
index 08b5868d1b4..b9354196c68 100644
--- a/spec/frontend/feature_flags/store/index/mutations_spec.js
+++ b/spec/frontend/feature_flags/store/index/mutations_spec.js
@@ -3,7 +3,7 @@ import * as types from '~/feature_flags/store/index/mutation_types';
import mutations from '~/feature_flags/store/index/mutations';
import state from '~/feature_flags/store/index/state';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { getRequestData, rotateData, featureFlag, userList } from '../../mock_data';
+import { getRequestData, rotateData, featureFlag } from '../../mock_data';
describe('Feature flags store Mutations', () => {
let stateCopy;
@@ -59,13 +59,11 @@ describe('Feature flags store Mutations', () => {
});
it('should set count with the given data', () => {
- expect(stateCopy.count.featureFlags).toEqual(37);
+ expect(stateCopy.count).toEqual(37);
});
it('should set pagination', () => {
- expect(stateCopy.pageInfo.featureFlags).toEqual(
- parseIntPagination(normalizeHeaders(headers)),
- );
+ expect(stateCopy.pageInfo).toEqual(parseIntPagination(normalizeHeaders(headers)));
});
});
@@ -83,58 +81,6 @@ describe('Feature flags store Mutations', () => {
});
});
- describe('REQUEST_USER_LISTS', () => {
- it('sets isLoading to true', () => {
- mutations[types.REQUEST_USER_LISTS](stateCopy);
- expect(stateCopy.isLoading).toBe(true);
- });
- });
-
- describe('RECEIVE_USER_LISTS_SUCCESS', () => {
- const headers = {
- 'x-next-page': '2',
- 'x-page': '1',
- 'X-Per-Page': '2',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '5',
- };
-
- beforeEach(() => {
- mutations[types.RECEIVE_USER_LISTS_SUCCESS](stateCopy, { data: [userList], headers });
- });
-
- it('sets isLoading to false', () => {
- expect(stateCopy.isLoading).toBe(false);
- });
-
- it('sets userLists to the received userLists', () => {
- expect(stateCopy.userLists).toEqual([userList]);
- });
-
- it('sets pagination info for user lits', () => {
- expect(stateCopy.pageInfo.userLists).toEqual(parseIntPagination(normalizeHeaders(headers)));
- });
-
- it('sets the count for user lists', () => {
- expect(stateCopy.count.userLists).toBe(parseInt(headers['X-TOTAL'], 10));
- });
- });
-
- describe('RECEIVE_USER_LISTS_ERROR', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_USER_LISTS_ERROR](stateCopy);
- });
-
- it('should set isLoading to false', () => {
- expect(stateCopy.isLoading).toEqual(false);
- });
-
- it('should set hasError to true', () => {
- expect(stateCopy.hasError).toEqual(true);
- });
- });
-
describe('REQUEST_ROTATE_INSTANCE_ID', () => {
beforeEach(() => {
mutations[types.REQUEST_ROTATE_INSTANCE_ID](stateCopy);
@@ -214,7 +160,7 @@ describe('Feature flags store Mutations', () => {
...flagState,
scopes: mapToScopesViewModel(flag.scopes || []),
}));
- stateCopy.count.featureFlags = stateCount;
+ stateCopy.count = stateCount;
mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](stateCopy, {
...featureFlag,
@@ -241,8 +187,6 @@ describe('Feature flags store Mutations', () => {
...flag,
scopes: mapToScopesViewModel(flag.scopes || []),
}));
- stateCopy.count = { enabled: 1, disabled: 0 };
-
mutations[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](stateCopy, featureFlag.id);
});
@@ -257,36 +201,6 @@ describe('Feature flags store Mutations', () => {
});
});
- describe('REQUEST_DELETE_USER_LIST', () => {
- beforeEach(() => {
- stateCopy.userLists = [userList];
- mutations[types.REQUEST_DELETE_USER_LIST](stateCopy, userList);
- });
-
- it('should remove the deleted list', () => {
- expect(stateCopy.userLists).not.toContain(userList);
- });
- });
-
- describe('RECEIVE_DELETE_USER_LIST_ERROR', () => {
- beforeEach(() => {
- stateCopy.userLists = [];
- mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](stateCopy, {
- list: userList,
- error: 'some error',
- });
- });
-
- it('should set isLoading to false and hasError to false', () => {
- expect(stateCopy.isLoading).toBe(false);
- expect(stateCopy.hasError).toBe(false);
- });
-
- it('should add the user list back to the list of user lists', () => {
- expect(stateCopy.userLists).toContain(userList);
- });
- });
-
describe('RECEIVE_CLEAR_ALERT', () => {
it('clears the alert', () => {
stateCopy.alerts = ['a server error'];
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 465e1ee1ef1..c03c8f6c529 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -185,7 +185,7 @@ describe('Filtered Search Manager', () => {
});
describe('search', () => {
- const defaultParams = '?scope=all&utf8=%E2%9C%93';
+ const defaultParams = '?scope=all';
const defaultState = '&state=opened';
it('should search with a single word', (done) => {
diff --git a/spec/frontend/fixtures/api_markdown.rb b/spec/frontend/fixtures/api_markdown.rb
index e012d922aad..1c3967b2c36 100644
--- a/spec/frontend/fixtures/api_markdown.rb
+++ b/spec/frontend/fixtures/api_markdown.rb
@@ -25,7 +25,7 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
let(:markdown) { markdown_example.fetch(:markdown) }
it "#{fixture_subdir}/#{name}.json" do
- post api("/markdown"), params: { text: markdown }
+ post api("/markdown"), params: { text: markdown, gfm: true }
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/releases.rb b/spec/frontend/fixtures/releases.rb
index 7ec155fcb10..1882ac49fd6 100644
--- a/spec/frontend/fixtures/releases.rb
+++ b/spec/frontend/fixtures/releases.rb
@@ -100,6 +100,17 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
link_type: :image)
end
+ let_it_be(:another_release) do
+ create(:release,
+ project: project,
+ tag: 'v1.2',
+ name: 'The second release',
+ author: admin,
+ description: 'An okay release :shrug:',
+ created_at: Time.zone.parse('2019-01-03'),
+ released_at: Time.zone.parse('2019-01-10'))
+ end
+
after(:all) do
remove_repository(project)
end
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
new file mode 100644
index 00000000000..b88fb840137
--- /dev/null
+++ b/spec/frontend/fixtures/runner.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Runner (JavaScript fixtures)' do
+ include AdminModeHelper
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, :public) }
+
+ let_it_be(:instance_runner) { create(:ci_runner, :instance, version: '1.0.0', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
+
+ query_path = 'runner/graphql/'
+ fixtures_path = 'graphql/runner/'
+
+ before(:all) do
+ clean_frontend_fixtures(fixtures_path)
+ end
+
+ after(:all) do
+ remove_repository(project)
+ end
+
+ before do
+ sign_in(admin)
+ enable_admin_mode!(admin)
+ end
+
+ describe GraphQL::Query, type: :request do
+ get_runners_query_name = 'get_runners.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runners_query_name}", [
+ 'runner/graphql/runner_node.fragment.graphql',
+ 'graphql_shared/fragments/pageInfo.fragment.graphql'
+ ])
+ end
+
+ it "#{fixtures_path}#{get_runners_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {})
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "#{fixtures_path}#{get_runners_query_name}.paginated.json" do
+ post_graphql(query, current_user: admin, variables: { first: 2 })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ describe GraphQL::Query, type: :request do
+ get_runner_query_name = 'get_runner.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_query_name}", [
+ 'runner/graphql/runner_details.fragment.graphql'
+ ])
+ end
+
+ it "#{fixtures_path}#{get_runner_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: instance_runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/services.rb b/spec/frontend/fixtures/services.rb
index 7472af802f3..91e6c2eb280 100644
--- a/spec/frontend/fixtures/services.rb
+++ b/spec/frontend/fixtures/services.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::ServicesController, '(JavaScript fixtures)', type: :con
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
- let!(:service) { create(:custom_issue_tracker_service, project: project) }
+ let!(:service) { create(:custom_issue_tracker_integration, project: project) }
let(:user) { project.owner }
render_views
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
new file mode 100644
index 00000000000..003f7b768dd
--- /dev/null
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Startup CSS fixtures', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:use_full_html) { true }
+
+ render_views
+
+ before(:all) do
+ stub_feature_flags(combined_menu: true)
+ stub_feature_flags(sidebar_refactor: true)
+ clean_frontend_fixtures('startup_css/')
+ end
+
+ shared_examples 'startup css project fixtures' do |type|
+ let(:user) { create(:user, :admin) }
+ let(:project) { create(:project, :public, :repository, description: 'Code and stuff', creator: user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it "startup_css/project-#{type}-legacy-menu.html" do
+ stub_feature_flags(combined_menu: false)
+
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ id: project
+ }
+
+ expect(response).to be_successful
+ end
+
+ it "startup_css/project-#{type}.html" do
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ id: project
+ }
+
+ expect(response).to be_successful
+ end
+
+ it "startup_css/project-#{type}-legacy-sidebar.html" do
+ stub_feature_flags(sidebar_refactor: false)
+
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ id: project
+ }
+
+ expect(response).to be_successful
+ end
+
+ it "startup_css/project-#{type}-signed-out.html" do
+ sign_out(user)
+
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ id: project
+ }
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
+ it_behaves_like 'startup css project fixtures', 'general'
+ end
+
+ describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
+ before do
+ user.update!(theme_id: 11)
+ end
+
+ it_behaves_like 'startup css project fixtures', 'dark'
+ end
+
+ describe RegistrationsController, '(Startup CSS fixtures)', type: :controller do
+ it 'startup_css/sign-in.html' do
+ get :new
+
+ expect(response).to be_successful
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/static/projects.json b/spec/frontend/fixtures/static/projects.json
index f28d9899099..d843549039b 100644
--- a/spec/frontend/fixtures/static/projects.json
+++ b/spec/frontend/fixtures/static/projects.json
@@ -3,6 +3,7 @@
"description": "",
"default_branch": null,
"tag_list": [],
+ "topics": [],
"public": true,
"archived": false,
"visibility_level": 20,
@@ -54,6 +55,7 @@
"description": "Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": false,
"archived": false,
"visibility_level": 0,
@@ -114,6 +116,7 @@
"description": "Modi odio mollitia dolorem qui.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": false,
"archived": false,
"visibility_level": 0,
@@ -162,6 +165,7 @@
"description": "Omnis asperiores ipsa et beatae quidem necessitatibus quia.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": true,
"archived": false,
"visibility_level": 20,
@@ -210,6 +214,7 @@
"description": "Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": false,
"archived": false,
"visibility_level": 0,
@@ -258,6 +263,7 @@
"description": "Aut molestias quas est ut aperiam officia quod libero.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": true,
"archived": false,
"visibility_level": 20,
@@ -309,6 +315,7 @@
"description": "Excepturi molestiae quia repellendus omnis est illo illum eligendi.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": true,
"archived": false,
"visibility_level": 20,
@@ -357,6 +364,7 @@
"description": "Adipisci quaerat dignissimos enim sed ipsam dolorem quia.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": false,
"archived": false,
"visibility_level": 10,
@@ -408,6 +416,7 @@
"description": "Vel voluptatem maxime saepe ex quia.",
"default_branch": "master",
"tag_list": [],
+ "topics": [],
"public": false,
"archived": false,
"visibility_level": 0,
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 6a5ac76a4d0..28e8522cc12 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -357,27 +357,46 @@ describe('Flash', () => {
});
describe('removeFlashClickListener', () => {
- beforeEach(() => {
- document.body.innerHTML += `
- <div class="flash-container">
- <div class="flash">
- <div class="close-icon js-close-icon"></div>
+ 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>
- </div>
- `;
- });
+ `;
+ });
- it('removes global flash on click', (done) => {
- const flashEl = document.querySelector('.flash');
+ it('removes global flash on click', (done) => {
+ removeFlashClickListener(el, false);
- removeFlashClickListener(flashEl, false);
+ el.querySelector('.js-close-icon').click();
- flashEl.querySelector('.js-close-icon').click();
+ setImmediate(() => {
+ expect(document.querySelector('.flash')).toBeNull();
- setImmediate(() => {
- expect(document.querySelector('.flash')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('without close icon', () => {
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.innerHTML = `
+ <div class="flash-container">
+ <div class="flash">
+ </div>
+ </div>
+ `;
+ });
- done();
+ it('does not throw', () => {
+ expect(() => removeFlashClickListener(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 7a1026e8bfc..a94cb3e2fcc 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -21,13 +21,14 @@ const TEST_NAMESPACE = 'projects';
const TEST_VUEX_MODULE = 'frequentProjects';
const TEST_PROJECT = currentSession[TEST_NAMESPACE].project;
const TEST_STORAGE_KEY = currentSession[TEST_NAMESPACE].storageKey;
+const TEST_SEARCH_CLASS = 'test-search-class';
describe('Frequent Items App Component', () => {
let wrapper;
let mock;
let store;
- const createComponent = ({ currentItem = null } = {}) => {
+ const createComponent = (props = {}) => {
const session = currentSession[TEST_NAMESPACE];
gon.api_version = session.apiVersion;
@@ -36,7 +37,8 @@ describe('Frequent Items App Component', () => {
propsData: {
namespace: TEST_NAMESPACE,
currentUserName: session.username,
- currentItem: currentItem || session.project,
+ currentItem: session.project,
+ ...props,
},
provide: {
vuexModule: TEST_VUEX_MODULE,
@@ -88,7 +90,7 @@ describe('Frequent Items App Component', () => {
});
it('should render search input', () => {
- expect(findSearchInput().exists()).toBe(true);
+ expect(findSearchInput().classes()).toEqual(['search-input-container']);
});
it('should render loading animation', async () => {
@@ -159,6 +161,16 @@ describe('Frequent Items App Component', () => {
});
});
+ describe('with searchClass', () => {
+ beforeEach(() => {
+ createComponent({ searchClass: TEST_SEARCH_CLASS });
+ });
+
+ it('should render search input with searchClass', () => {
+ expect(findSearchInput().classes()).toEqual(['search-input-container', TEST_SEARCH_CLASS]);
+ });
+ });
+
describe('logging', () => {
it('when created, it should create a project storage entry and adds a project', () => {
createComponent();
diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
index 1d2a5d636bc..33e2c0db5e5 100644
--- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
+++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
@@ -43,19 +43,25 @@ exports[`grafana integration component default state to match the default snapsh
class="settings-content"
>
<form>
- <gl-form-checkbox-stub
- class="mb-4"
- id="grafana-integration-enabled"
+ <gl-form-group-stub
+ label="Enable authentication"
+ label-for="grafana-integration-enabled"
+ labeldescription=""
>
+ <gl-form-checkbox-stub
+ id="grafana-integration-enabled"
+ >
+
+ Active
- Active
-
- </gl-form-checkbox-stub>
+ </gl-form-checkbox-stub>
+ </gl-form-group-stub>
<gl-form-group-stub
description="Enter the base URL of the Grafana instance."
label="Grafana URL"
label-for="grafana-url"
+ labeldescription=""
>
<gl-form-input-stub
id="grafana-url"
@@ -67,6 +73,7 @@ exports[`grafana integration component default state to match the default snapsh
<gl-form-group-stub
label="API token"
label-for="grafana-token"
+ labeldescription=""
>
<gl-form-input-stub
id="grafana-token"
@@ -76,32 +83,19 @@ exports[`grafana integration component default state to match the default snapsh
<p
class="form-text text-muted"
>
-
- Enter the Grafana API token.
-
- <a
- href="https://grafana.com/docs/http_api/auth/#create-api-token"
- rel="noopener noreferrer"
- target="_blank"
- >
-
- More information.
-
- <gl-icon-stub
- class="vertical-align-middle"
- name="external-link"
- size="16"
- />
- </a>
+ <gl-sprintf-stub
+ message="Enter the %{docLinkStart}Grafana API token%{docLinkEnd}."
+ />
</p>
</gl-form-group-stub>
<gl-button-stub
buttontextclasses=""
category="primary"
+ data-testid="save-grafana-settings-button"
icon=""
size="medium"
- variant="success"
+ variant="confirm"
>
Save changes
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index f1a8e6fe2dc..3cb4dd41574 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -1,7 +1,8 @@
import { GlButton } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
@@ -51,8 +52,7 @@ describe('grafana integration component', () => {
it('renders as an expand button by default', () => {
wrapper = shallowMount(GrafanaIntegration, { store });
- const button = wrapper.find(GlButton);
-
+ const button = wrapper.findComponent(GlButton);
expect(button.text()).toBe('Expand');
});
});
@@ -70,6 +70,7 @@ describe('grafana integration component', () => {
describe('form', () => {
beforeEach(() => {
jest.spyOn(axios, 'patch').mockImplementation();
+ wrapper = mountExtended(GrafanaIntegration, { store });
});
afterEach(() => {
@@ -77,7 +78,7 @@ describe('grafana integration component', () => {
});
describe('submit button', () => {
- const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
+ const findSubmitButton = () => wrapper.findByTestId('save-grafana-settings-button');
const endpointRequest = [
operationsSettingsEndpoint,
@@ -93,9 +94,7 @@ describe('grafana integration component', () => {
];
it('submits form on click', () => {
- wrapper = mount(GrafanaIntegration, { store });
axios.patch.mockResolvedValue();
-
findSubmitButton(wrapper).trigger('click');
expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
@@ -104,7 +103,6 @@ describe('grafana integration component', () => {
it('creates flash banner on error', () => {
const message = 'mockErrorMessage';
- wrapper = mount(GrafanaIntegration, { store });
axios.patch.mockRejectedValue({ response: { data: { message } } });
findSubmitButton().trigger('click');
@@ -114,10 +112,10 @@ describe('grafana integration component', () => {
.$nextTick()
.then(jest.runAllTicks)
.then(() =>
- expect(createFlash).toHaveBeenCalledWith(
- `There was an error saving your changes. ${message}`,
- 'alert',
- ),
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `There was an error saving your changes. ${message}`,
+ type: 'alert',
+ }),
);
});
});
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 5a9f640392f..546cdd3cd6f 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -1,34 +1,33 @@
+import { mount } from '@vue/test-utils';
import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import groupFolderComponent from '~/groups/components/group_folder.vue';
-import groupItemComponent from '~/groups/components/group_item.vue';
+import GroupFolder from '~/groups/components/group_folder.vue';
+import GroupItem from '~/groups/components/group_item.vue';
+import ItemActions from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub';
import { getGroupItemMicrodata } from '~/groups/store/utils';
import * as urlUtilities from '~/lib/utils/url_utility';
import { mockParentGroupItem, mockChildren } from '../mock_data';
-const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
- const Component = Vue.extend(groupItemComponent);
-
- return mountComponent(Component, {
- group,
- parentGroup,
+const createComponent = (
+ propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] },
+) => {
+ return mount(GroupItem, {
+ propsData,
+ components: { GroupFolder },
});
};
describe('GroupItemComponent', () => {
- let vm;
+ let wrapper;
beforeEach(() => {
- Vue.component('GroupFolder', groupFolderComponent);
-
- vm = createComponent();
+ wrapper = createComponent();
return Vue.nextTick();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
const withMicrodata = (group) => ({
@@ -39,14 +38,14 @@ describe('GroupItemComponent', () => {
describe('computed', () => {
describe('groupDomId', () => {
it('should return ID string suffixed with group ID', () => {
- expect(vm.groupDomId).toBe('group-55');
+ expect(wrapper.vm.groupDomId).toBe('group-55');
});
});
describe('rowClass', () => {
it('should return map of classes based on group details', () => {
const classes = ['is-open', 'has-children', 'has-description', 'being-removed'];
- const { rowClass } = vm;
+ const { rowClass } = wrapper.vm;
expect(Object.keys(rowClass).length).toBe(classes.length);
Object.keys(rowClass).forEach((className) => {
@@ -57,58 +56,55 @@ describe('GroupItemComponent', () => {
describe('hasChildren', () => {
it('should return boolean value representing if group has any children present', () => {
- let newVm;
const group = { ...mockParentGroupItem };
group.childrenCount = 5;
- newVm = createComponent(group);
+ wrapper = createComponent({ group });
- expect(newVm.hasChildren).toBeTruthy();
- newVm.$destroy();
+ expect(wrapper.vm.hasChildren).toBe(true);
+ wrapper.destroy();
group.childrenCount = 0;
- newVm = createComponent(group);
+ wrapper = createComponent({ group });
- expect(newVm.hasChildren).toBeFalsy();
- newVm.$destroy();
+ expect(wrapper.vm.hasChildren).toBe(false);
+ wrapper.destroy();
});
});
describe('hasAvatar', () => {
it('should return boolean value representing if group has any avatar present', () => {
- let newVm;
const group = { ...mockParentGroupItem };
group.avatarUrl = null;
- newVm = createComponent(group);
+ wrapper = createComponent({ group });
- expect(newVm.hasAvatar).toBeFalsy();
- newVm.$destroy();
+ expect(wrapper.vm.hasAvatar).toBe(false);
+ wrapper.destroy();
group.avatarUrl = '/uploads/group_avatar.png';
- newVm = createComponent(group);
+ wrapper = createComponent({ group });
- expect(newVm.hasAvatar).toBeTruthy();
- newVm.$destroy();
+ expect(wrapper.vm.hasAvatar).toBe(true);
+ wrapper.destroy();
});
});
describe('isGroup', () => {
it('should return boolean value representing if group item is of type `group` or not', () => {
- let newVm;
const group = { ...mockParentGroupItem };
group.type = 'group';
- newVm = createComponent(group);
+ wrapper = createComponent({ group });
- expect(newVm.isGroup).toBeTruthy();
- newVm.$destroy();
+ expect(wrapper.vm.isGroup).toBe(true);
+ wrapper.destroy();
group.type = 'project';
- newVm = createComponent(group);
+ wrapper = createComponent({ group });
- expect(newVm.isGroup).toBeFalsy();
- newVm.$destroy();
+ expect(wrapper.vm.isGroup).toBe(false);
+ wrapper.destroy();
});
});
});
@@ -137,22 +133,22 @@ describe('GroupItemComponent', () => {
it('should emit `toggleChildren` event when expand is clicked on a group and it has children present', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- vm.onClickRowGroup(event);
+ wrapper.vm.onClickRowGroup(event);
- expect(eventHub.$emit).toHaveBeenCalledWith('toggleChildren', vm.group);
+ expect(eventHub.$emit).toHaveBeenCalledWith('toggleChildren', wrapper.vm.group);
});
it('should navigate page to group homepage if group does not have any children present', () => {
jest.spyOn(urlUtilities, 'visitUrl').mockImplementation();
const group = { ...mockParentGroupItem };
group.childrenCount = 0;
- const newVm = createComponent(group);
+ wrapper = createComponent({ group });
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- newVm.onClickRowGroup(event);
+ wrapper.vm.onClickRowGroup(event);
expect(eventHub.$emit).not.toHaveBeenCalled();
- expect(urlUtilities.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ expect(urlUtilities.visitUrl).toHaveBeenCalledWith(wrapper.vm.group.relativePath);
});
});
});
@@ -163,11 +159,11 @@ describe('GroupItemComponent', () => {
describe('for a group pending deletion', () => {
beforeEach(() => {
group = { ...mockParentGroupItem, pendingRemoval: true };
- vm = createComponent(group);
+ wrapper = createComponent({ group });
});
it('renders the group pending removal badge', () => {
- const badgeEl = vm.$el.querySelector('.badge-warning');
+ const badgeEl = wrapper.vm.$el.querySelector('.badge-warning');
expect(badgeEl).toBeDefined();
expect(badgeEl.innerHTML).toContain('pending removal');
@@ -177,21 +173,41 @@ describe('GroupItemComponent', () => {
describe('for a group not scheduled for deletion', () => {
beforeEach(() => {
group = { ...mockParentGroupItem, pendingRemoval: false };
- vm = createComponent(group);
+ wrapper = createComponent({ group });
});
it('does not render the group pending removal badge', () => {
- const groupTextContainer = vm.$el.querySelector('.group-text-container');
+ const groupTextContainer = wrapper.vm.$el.querySelector('.group-text-container');
expect(groupTextContainer).not.toContain('pending removal');
});
+
+ it('renders `item-actions` component and passes correct props to it', () => {
+ wrapper = createComponent({
+ group: mockParentGroupItem,
+ parentGroup: mockChildren[0],
+ action: 'subgroups_and_projects',
+ });
+ const itemActionsComponent = wrapper.findComponent(ItemActions);
+
+ expect(itemActionsComponent.exists()).toBe(true);
+ expect(itemActionsComponent.props()).toEqual({
+ group: mockParentGroupItem,
+ parentGroup: mockChildren[0],
+ action: 'subgroups_and_projects',
+ });
+ });
});
it('should render component template correctly', () => {
- const visibilityIconEl = vm.$el.querySelector('[data-testid="group-visibility-icon"]');
+ const visibilityIconEl = wrapper.vm.$el.querySelector(
+ '[data-testid="group-visibility-icon"]',
+ );
+
+ const { vm } = wrapper;
expect(vm.$el.getAttribute('id')).toBe('group-55');
- expect(vm.$el.classList.contains('group-row')).toBeTruthy();
+ expect(vm.$el.classList.contains('group-row')).toBe(true);
expect(vm.$el.querySelector('.group-row-contents')).toBeDefined();
expect(vm.$el.querySelector('.group-row-contents .controls')).toBeDefined();
@@ -220,13 +236,13 @@ describe('GroupItemComponent', () => {
describe('schema.org props', () => {
describe('when showSchemaMarkup is disabled on the group', () => {
it.each(['itemprop', 'itemtype', 'itemscope'], 'it does not set %s', (attr) => {
- expect(vm.$el.getAttribute(attr)).toBeNull();
+ expect(wrapper.vm.$el.getAttribute(attr)).toBeNull();
});
it.each(
['.js-group-avatar', '.js-group-name', '.js-group-description'],
'it does not set `itemprop` on sub-nodes',
(selector) => {
- expect(vm.$el.querySelector(selector).getAttribute('itemprop')).toBeNull();
+ expect(wrapper.vm.$el.querySelector(selector).getAttribute('itemprop')).toBeNull();
},
);
});
@@ -238,7 +254,7 @@ describe('GroupItemComponent', () => {
description: 'Foo Bar',
});
- vm = createComponent(group);
+ wrapper = createComponent({ group });
});
it.each`
@@ -247,7 +263,7 @@ describe('GroupItemComponent', () => {
${'itemtype'} | ${'https://schema.org/Organization'}
${'itemprop'} | ${'subOrganization'}
`('it does set correct $attr', ({ attr, value } = {}) => {
- expect(vm.$el.getAttribute(attr)).toBe(value);
+ expect(wrapper.vm.$el.getAttribute(attr)).toBe(value);
});
it.each`
@@ -256,7 +272,7 @@ describe('GroupItemComponent', () => {
${'[data-testid="group-name"]'} | ${'name'}
${'[data-testid="group-description"]'} | ${'description'}
`('it does set correct $selector', ({ selector, propValue } = {}) => {
- expect(vm.$el.querySelector(selector).getAttribute('itemprop')).toBe(propValue);
+ expect(wrapper.vm.$el.querySelector(selector).getAttribute('itemprop')).toBe(propValue);
});
});
});
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js
index f90c298c401..271d0600e16 100644
--- a/spec/frontend/ide/components/branches/item_spec.js
+++ b/spec/frontend/ide/components/branches/item_spec.js
@@ -7,7 +7,7 @@ import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { projectData } from '../../mock_data';
const TEST_BRANCH = {
- name: 'master',
+ name: 'main',
committedDate: '2018-01-05T05:50Z',
};
const TEST_PROJECT_ID = projectData.name_with_namespace;
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index c4dccf26af3..ed9d11246ae 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -10,7 +10,7 @@ import {
const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction';
-const BRANCH_DEFAULT = 'master';
+const BRANCH_DEFAULT = 'main';
const BRANCH_PROTECTED = 'protected/access';
const BRANCH_PROTECTED_NO_ACCESS = 'protected/no-access';
const BRANCH_REGULAR = 'regular';
@@ -20,11 +20,7 @@ describe('IDE commit sidebar actions', () => {
let store;
let vm;
- const createComponent = ({
- hasMR = false,
- currentBranchId = 'master',
- emptyRepo = false,
- } = {}) => {
+ const createComponent = ({ hasMR = false, currentBranchId = 'main', emptyRepo = false } = {}) => {
const Component = Vue.extend(commitActions);
vm = createComponentWithStore(Component, store);
@@ -72,7 +68,7 @@ describe('IDE commit sidebar actions', () => {
it('renders current branch text', () => {
createComponent();
- expect(findText()).toContain('Commit to master branch');
+ expect(findText()).toContain('Commit to main branch');
});
it('hides merge request option when project merge requests are disabled', (done) => {
@@ -112,7 +108,7 @@ describe('IDE commit sidebar actions', () => {
it('calls again after staged changes', (done) => {
createComponent({ currentBranchId: null });
- vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.currentBranchId = 'main';
vm.$store.state.changedFiles.push({});
vm.$store.state.stagedFiles.push({});
@@ -158,7 +154,7 @@ describe('IDE commit sidebar actions', () => {
it('only renders commit to current branch', () => {
expect(findRadios().length).toBe(1);
- expect(findText()).toContain('Commit to master branch');
+ expect(findText()).toContain('Commit to main branch');
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index f5916b021aa..83d1bbb842e 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -67,7 +67,7 @@ describe('IDE commit form', () => {
store = createStore();
store.state.stagedFiles.push('test');
store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
Vue.set(store.state.projects, 'abcproject', {
...projectData,
userPermissions: { pushCode: true },
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 253c2a426ee..4474647552d 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -24,7 +24,7 @@ describe('create new MR checkbox', () => {
store.state.projects[store.state.currentProjectId].userPermissions = permissions;
};
- const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => {
+ const createComponent = ({ currentBranchId = 'main', createNewBranch = false } = {}) => {
const Component = Vue.extend(NewMergeRequestOption);
vm = createComponentWithStore(Component, store);
@@ -63,7 +63,7 @@ describe('create new MR checkbox', () => {
describe('is rendered when pushing to a new branch', () => {
beforeEach(() => {
createComponent({
- currentBranchId: 'master',
+ currentBranchId: 'main',
createNewBranch: true,
});
});
@@ -87,7 +87,7 @@ describe('create new MR checkbox', () => {
describe('is NOT rendered when pushing to the same branch', () => {
beforeEach(() => {
createComponent({
- currentBranchId: 'master',
+ currentBranchId: 'main',
createNewBranch: false,
});
});
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index 740b7ada521..7a92f59641f 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -19,9 +19,9 @@ describe('IDE review mode', () => {
beforeEach(() => {
store = createStore();
store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects.abcproject = { ...projectData };
- Vue.set(store.state.trees, 'abcproject/master', {
+ Vue.set(store.state.trees, 'abcproject/main', {
tree: [file('fileName')],
loading: false,
});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index b23a78a035d..f8d29fc7b47 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -22,9 +22,9 @@ describe('WebIDE', () => {
const createComponent = ({ projData = emptyProjData, state = {} } = {}) => {
store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects.abcproject = projData && { ...projData };
- store.state.trees['abcproject/master'] = {
+ store.state.trees['abcproject/main'] = {
tree: [],
loading: false,
};
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index 9d33a1e2554..f1a0b64caf2 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -24,7 +24,7 @@ describe('ideStatusBar', () => {
store = createStore();
store.state.currentProjectId = TEST_PROJECT_ID;
store.state.projects[TEST_PROJECT_ID] = _.clone(projectData);
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
});
afterEach(() => {
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
index c8153ea339e..85d9feb0c09 100644
--- a/spec/frontend/ide/components/ide_tree_list_spec.js
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -14,9 +14,9 @@ describe('IDE tree list', () => {
const bootstrapWithTree = (tree = normalBranchTree) => {
store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects.abcproject = { ...projectData };
- Vue.set(store.state.trees, 'abcproject/master', {
+ Vue.set(store.state.trees, 'abcproject/main', {
tree,
loading: false,
});
@@ -42,7 +42,7 @@ describe('IDE tree list', () => {
});
it('renders loading indicator', (done) => {
- store.state.trees['abcproject/master'].loading = true;
+ store.state.trees['abcproject/main'].loading = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index 6eef646b012..0792b88aeb6 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -18,9 +18,9 @@ describe('IdeTree', () => {
store = createStore();
store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects.abcproject = { ...projectData };
- Vue.set(store.state.trees, 'abcproject/master', {
+ Vue.set(store.state.trees, 'abcproject/main', {
tree: [file('fileName')],
loading: false,
});
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
index 85acabca38b..610e20d5868 100644
--- a/spec/frontend/ide/components/merge_requests/list_spec.js
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -21,7 +21,7 @@ describe('IDE merge requests list', () => {
const fakeStore = new Vuex.Store({
state: {
currentMergeRequestId: '1',
- currentProjectId: 'project/master',
+ currentProjectId: 'project/main',
...restOfState,
},
modules: {
diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js
index 4ddb3930764..6a1be7ee964 100644
--- a/spec/frontend/ide/components/nav_dropdown_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_spec.js
@@ -14,14 +14,14 @@ describe('IDE NavDropdown', () => {
store = createStore();
Object.assign(store.state, {
currentProjectId: TEST_PROJECT_ID,
- currentBranchId: 'master',
+ currentBranchId: 'main',
projects: {
[TEST_PROJECT_ID]: {
userPermissions: {
[PERMISSION_READ_MR]: true,
},
branches: {
- master: { id: 'master' },
+ main: { id: 'main' },
},
},
},
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index 5a1c0471206..fa34d1b257f 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -13,7 +13,7 @@ describe('new dropdown component', () => {
const component = Vue.extend(newDropdown);
vm = createComponentWithStore(component, store, {
- branch: 'master',
+ branch: 'main',
path: '',
mouseOver: false,
type: 'tree',
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index 0600fcea917..fce6ccf4b58 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import modal from '~/ide/components/new_dropdown/modal.vue';
import { createStore } from '~/ide/stores';
@@ -182,14 +182,14 @@ describe('new file modal component', () => {
vm.submitForm();
- expect(createFlash).toHaveBeenCalledWith(
- 'The name "test-path/test" is already taken in this directory.',
- 'alert',
- expect.anything(),
- null,
- false,
- true,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'The name "test-path/test" is already taken in this directory.',
+ type: 'alert',
+ parent: expect.anything(),
+ actionConfig: null,
+ fadeTransition: false,
+ addBodyClass: true,
+ });
});
it('does not throw error when target entry does not exist', () => {
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index c174f5e2006..db4181395d3 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -22,11 +22,11 @@ describe('RepoCommitSection', () => {
store.state.noChangesStateSvgPath = 'svg';
store.state.committedStateSvgPath = 'commitsvg';
store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects.abcproject = {
web_url: '',
branches: {
- master: {
+ main: {
workingReference: '1',
},
},
@@ -39,7 +39,7 @@ describe('RepoCommitSection', () => {
}),
);
- store.state.currentBranch = 'master';
+ store.state.currentBranch = 'main';
store.state.changedFiles = [];
store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }];
store.state.stagedFiles.forEach((f) =>
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 646e51160d8..8e8fb31b15a 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -63,8 +63,8 @@ const prepareStore = (state, activeFile) => {
projects: {
'gitlab-org/gitlab': {
branches: {
- master: {
- name: 'master',
+ main: {
+ name: 'main',
commit: {
id: 'abcdefgh',
},
@@ -73,7 +73,7 @@ const prepareStore = (state, activeFile) => {
},
},
currentProjectId: 'gitlab-org/gitlab',
- currentBranchId: 'master',
+ currentBranchId: 'main',
entries: {
[activeFile.path]: activeFile,
},
@@ -656,7 +656,7 @@ describe('RepoEditor', () => {
});
it("does not add file to state or set markdown image syntax if the file isn't markdown", async () => {
- wrapper.setProps({
+ await wrapper.setProps({
file: setFileName('myfile.txt'),
});
pasteImage();
diff --git a/spec/frontend/ide/ide_router_spec.js b/spec/frontend/ide/ide_router_spec.js
index acab2c6aeef..3fb7781b176 100644
--- a/spec/frontend/ide/ide_router_spec.js
+++ b/spec/frontend/ide/ide_router_spec.js
@@ -18,14 +18,14 @@ describe('IDE router', () => {
});
[
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/master/-/src/blob/`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/master/-/src/blob`,
+ `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`,
+ `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`,
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/master/-/src/tree/`,
+ `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`,
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/master/-/src/blob`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/master/-/src/edit`,
- `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/master/-/src/merge_requests/2`,
+ `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`,
+ `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`,
+ `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`,
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`,
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`,
`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`,
diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js
index c8925e6745d..557626b3cca 100644
--- a/spec/frontend/ide/mock_data.js
+++ b/spec/frontend/ide/mock_data.js
@@ -8,8 +8,8 @@ export const projectData = {
path: '',
name_with_namespace: 'namespace/abcproject',
branches: {
- master: {
- treeId: 'abcproject/master',
+ main: {
+ treeId: 'abcproject/main',
can_push: true,
commit: {
id: '123',
@@ -19,13 +19,13 @@ export const projectData = {
mergeRequests: {},
merge_requests_enabled: true,
userPermissions: {},
- default_branch: 'master',
+ default_branch: 'main',
};
export const pipelines = [
{
id: 1,
- ref: 'master',
+ ref: 'main',
sha: '123',
details: {
status: {
@@ -38,7 +38,7 @@ export const pipelines = [
},
{
id: 2,
- ref: 'master',
+ ref: 'main',
sha: '213',
details: {
status: {
@@ -178,9 +178,9 @@ export const mergeRequests = [
export const branches = [
{
id: 1,
- name: 'master',
+ name: 'main',
commit: {
- message: 'Update master branch',
+ message: 'Update main branch',
committed_date: '2018-08-01T00:20:05Z',
},
can_push: true,
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 4a726cff3b6..925446aa280 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -15,7 +15,7 @@ jest.mock('~/ide/services/gql');
const TEST_NAMESPACE = 'alice';
const TEST_PROJECT = 'wonderland';
const TEST_PROJECT_ID = `${TEST_NAMESPACE}/${TEST_PROJECT}`;
-const TEST_BRANCH = 'master-patch-123';
+const TEST_BRANCH = 'main-patch-123';
const TEST_COMMIT_SHA = '123456789';
const TEST_FILE_PATH = 'README2.md';
const TEST_FILE_OLD_PATH = 'OLD_README2.md';
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 6178fb08d8c..6b94d7cf6f1 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -29,7 +29,7 @@ describe('IDE store file actions', () => {
store = createStore();
store.state.currentProjectId = 'test/test';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
router = createRouter(store);
@@ -85,7 +85,7 @@ describe('IDE store file actions', () => {
.dispatch('closeFile', localFile)
.then(Vue.nextTick)
.then(() => {
- expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/master/-/newOpenFile/');
+ expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/main/-/newOpenFile/');
});
});
@@ -177,11 +177,11 @@ describe('IDE store file actions', () => {
store.state.entries[localFile.path] = localFile;
store.state.currentProjectId = 'test/test';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects['test/test'] = {
branches: {
- master: {
+ main: {
commit: {
id: '7297abc',
},
@@ -260,7 +260,7 @@ describe('IDE store file actions', () => {
it('sets document title with the branchId', () => {
return store.dispatch('getFileData', { path: localFile.path }).then(() => {
- expect(document.title).toBe(`${localFile.path} · master · test/test · GitLab`);
+ expect(document.title).toBe(`${localFile.path} · main · test/test · GitLab`);
});
});
@@ -329,7 +329,7 @@ describe('IDE store file actions', () => {
it('sets document title considering `prevPath` on a file', () => {
return store.dispatch('getFileData', { path: localFile.path }).then(() => {
- expect(document.title).toBe(`new-shiny-file · master · test/test · GitLab`);
+ expect(document.title).toBe(`new-shiny-file · main · test/test · GitLab`);
});
});
});
@@ -702,7 +702,7 @@ describe('IDE store file actions', () => {
});
it('pushes route for active file', () => {
- expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/master/-/tempFile/');
+ expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/main/-/tempFile/');
});
});
});
@@ -778,7 +778,7 @@ describe('IDE store file actions', () => {
it('pushes router URL when added', () => {
return store.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }).then(() => {
- expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/master/');
+ expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/main/');
});
});
});
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index 600bd5fe9e1..e62811a4517 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { range } from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
@@ -145,7 +145,9 @@ describe('IDE store merge request actions', () => {
.dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
.catch(() => {
expect(createFlash).toHaveBeenCalled();
- expect(createFlash.mock.calls[0][0]).toBe('Error fetching merge requests for bar');
+ expect(createFlash.mock.calls[0][0].message).toBe(
+ 'Error fetching merge requests for bar',
+ );
})
.then(done)
.catch(done.fail);
@@ -461,11 +463,11 @@ describe('IDE store merge request actions', () => {
};
store.state.currentProjectId = 'test/test';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects['test/test'] = {
branches: {
- master: {
+ main: {
commit: {
id: '7297abc',
},
@@ -562,7 +564,9 @@ describe('IDE store merge request actions', () => {
openMergeRequest(store, mr)
.catch(() => {
- expect(createFlash).toHaveBeenCalledWith(expect.any(String));
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.any(String),
+ });
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
index 23ffb5ff56b..ca6f7169059 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -37,11 +37,11 @@ describe('IDE store project actions', () => {
describe('refreshLastCommitData', () => {
beforeEach(() => {
store.state.currentProjectId = 'abc/def';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects['abc/def'] = {
id: 4,
branches: {
- master: {
+ main: {
commit: null,
},
},
@@ -60,7 +60,7 @@ describe('IDE store project actions', () => {
branchId: store.state.currentBranchId,
})
.then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'master');
+ expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'main');
done();
})
@@ -81,7 +81,7 @@ describe('IDE store project actions', () => {
type: 'SET_BRANCH_COMMIT',
payload: {
projectId: TEST_PROJECT_ID,
- branchId: 'master',
+ branchId: 'main',
commit: { id: '123' },
},
},
@@ -97,17 +97,17 @@ describe('IDE store project actions', () => {
it('dispatches setErrorMessage', (done) => {
testAction(
showBranchNotFoundError,
- 'master',
+ 'main',
null,
[],
[
{
type: 'setErrorMessage',
payload: {
- text: "Branch <strong>master</strong> was not found in this project's repository.",
+ text: "Branch <strong>main</strong> was not found in this project's repository.",
action: expect.any(Function),
actionText: 'Create branch',
- actionPayload: 'master',
+ actionPayload: 'main',
},
},
],
@@ -131,7 +131,7 @@ describe('IDE store project actions', () => {
},
getters: {
currentProject: {
- default_branch: 'master',
+ default_branch: 'main',
},
},
dispatch() {},
@@ -140,7 +140,7 @@ describe('IDE store project actions', () => {
)
.then(() => {
expect(api.createBranch).toHaveBeenCalledWith('project-path', {
- ref: 'master',
+ ref: 'main',
branch: 'new-branch-name',
});
})
@@ -158,7 +158,7 @@ describe('IDE store project actions', () => {
},
getters: {
currentProject: {
- default_branch: 'master',
+ default_branch: 'main',
},
},
dispatch: dispatchSpy,
@@ -180,7 +180,7 @@ describe('IDE store project actions', () => {
},
getters: {
currentProject: {
- default_branch: 'master',
+ default_branch: 'main',
},
},
dispatch() {},
@@ -199,13 +199,13 @@ describe('IDE store project actions', () => {
it('creates a blank tree and sets loading state to false', (done) => {
testAction(
loadEmptyBranch,
- { projectId: TEST_PROJECT_ID, branchId: 'master' },
+ { projectId: TEST_PROJECT_ID, branchId: 'main' },
store.state,
[
- { type: 'CREATE_TREE', payload: { treePath: `${TEST_PROJECT_ID}/master` } },
+ { type: 'CREATE_TREE', payload: { treePath: `${TEST_PROJECT_ID}/main` } },
{
type: 'TOGGLE_LOADING',
- payload: { entry: store.state.trees[`${TEST_PROJECT_ID}/master`], forceValue: false },
+ payload: { entry: store.state.trees[`${TEST_PROJECT_ID}/main`], forceValue: false },
},
],
expect.any(Object),
@@ -214,11 +214,11 @@ describe('IDE store project actions', () => {
});
it('does nothing, if tree already exists', (done) => {
- const trees = { [`${TEST_PROJECT_ID}/master`]: [] };
+ const trees = { [`${TEST_PROJECT_ID}/main`]: [] };
testAction(
loadEmptyBranch,
- { projectId: TEST_PROJECT_ID, branchId: 'master' },
+ { projectId: TEST_PROJECT_ID, branchId: 'main' },
{ trees },
[],
[],
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
index 8de2188a5f4..8d7328725e9 100644
--- a/spec/frontend/ide/stores/actions/tree_spec.js
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -18,8 +18,8 @@ describe('Multi-file store tree actions', () => {
const basicCallParameters = {
endpoint: 'rootEndpoint',
projectId: 'abcproject',
- branch: 'master',
- branchId: 'master',
+ branch: 'main',
+ branchId: 'main',
ref: '12345678',
};
@@ -31,7 +31,7 @@ describe('Multi-file store tree actions', () => {
mock = new MockAdapter(axios);
store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
store.state.projects.abcproject = {
web_url: '',
path_with_namespace: 'foo/abcproject',
@@ -66,7 +66,7 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getFiles', basicCallParameters)
.then(() => {
- projectTree = store.state.trees['abcproject/master'];
+ projectTree = store.state.trees['abcproject/main'];
expect(projectTree.tree.length).toBe(2);
expect(projectTree.tree[0].type).toBe('tree');
@@ -89,7 +89,7 @@ describe('Multi-file store tree actions', () => {
'abc/def': {
web_url: `${TEST_HOST}/files`,
branches: {
- 'master-testing': {
+ 'main-testing': {
commit: {
id: '12345',
},
@@ -98,7 +98,7 @@ describe('Multi-file store tree actions', () => {
},
};
const getters = {
- findBranch: () => store.state.projects['abc/def'].branches['master-testing'],
+ findBranch: () => store.state.projects['abc/def'].branches['main-testing'],
};
mock.onGet(/(.*)/).replyOnce(500);
@@ -112,7 +112,7 @@ describe('Multi-file store tree actions', () => {
},
{
projectId: 'abc/def',
- branchId: 'master-testing',
+ branchId: 'main-testing',
},
)
.then(done.fail)
@@ -121,7 +121,7 @@ describe('Multi-file store tree actions', () => {
text: 'An error occurred while loading all the files.',
action: expect.any(Function),
actionText: 'Please try again',
- actionPayload: { projectId: 'abc/def', branchId: 'master-testing' },
+ actionPayload: { projectId: 'abc/def', branchId: 'main-testing' },
});
done();
});
@@ -178,17 +178,17 @@ describe('Multi-file store tree actions', () => {
describe('setDirectoryData', () => {
it('sets tree correctly if there are no opened files yet', (done) => {
const treeFile = file({ name: 'README.md' });
- store.state.trees['abcproject/master'] = {};
+ store.state.trees['abcproject/main'] = {};
testAction(
setDirectoryData,
- { projectId: 'abcproject', branchId: 'master', treeList: [treeFile] },
+ { projectId: 'abcproject', branchId: 'main', treeList: [treeFile] },
store.state,
[
{
type: types.SET_DIRECTORY_DATA,
payload: {
- treePath: 'abcproject/master',
+ treePath: 'abcproject/main',
data: [treeFile],
},
},
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index ad55313da93..e575667b8c6 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -777,7 +777,7 @@ describe('Multi-file store actions', () => {
it('routes to the renamed file if the original file has been opened', (done) => {
store.state.currentProjectId = 'test/test';
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
Object.assign(store.state.entries.orig, {
opened: true,
@@ -790,7 +790,7 @@ describe('Multi-file store actions', () => {
})
.then(() => {
expect(router.push.mock.calls).toHaveLength(1);
- expect(router.push).toHaveBeenCalledWith(`/project/test/test/tree/master/-/renamed/`);
+ expect(router.push).toHaveBeenCalledWith(`/project/test/test/tree/main/-/renamed/`);
})
.then(done)
.catch(done.fail);
@@ -1019,7 +1019,7 @@ describe('Multi-file store actions', () => {
},
{
projectId: 'abc/def',
- branchId: 'master-testing',
+ branchId: 'main-testing',
},
];
dispatch = jest.fn();
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 06456cdb12a..53d161ae5c9 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -209,12 +209,12 @@ describe('IDE store getters', () => {
describe('currentBranch', () => {
it('returns current projects branch', () => {
localState.currentProjectId = 'abcproject';
- localState.currentBranchId = 'master';
+ localState.currentBranchId = 'main';
localState.projects.abcproject = {
name: 'abcproject',
branches: {
- master: {
- name: 'master',
+ main: {
+ name: 'main',
},
},
};
@@ -223,7 +223,7 @@ describe('IDE store getters', () => {
};
getters.currentBranch(localState, localGetters);
- expect(localGetters.findBranch).toHaveBeenCalledWith('abcproject', 'master');
+ expect(localGetters.findBranch).toHaveBeenCalledWith('abcproject', 'main');
});
});
@@ -243,12 +243,12 @@ describe('IDE store getters', () => {
it('returns the selected branch from a project', () => {
localState.currentProjectId = 'abcproject';
- localState.currentBranchId = 'master';
+ localState.currentBranchId = 'main';
localState.projects.abcproject = {
name: 'abcproject',
branches: {
- master: {
- name: 'master',
+ main: {
+ name: 'main',
},
},
};
@@ -256,9 +256,9 @@ describe('IDE store getters', () => {
findProject: () => localState.projects.abcproject,
};
- result = getters.findBranch(localState, localGetters)('abcproject', 'master');
+ result = getters.findBranch(localState, localGetters)('abcproject', 'main');
- expect(result.name).toBe('master');
+ expect(result.name).toBe('main');
});
});
@@ -274,9 +274,9 @@ describe('IDE store getters', () => {
it("returns true when project's default branch matches current branch", () => {
const localGetters = {
currentProject: {
- default_branch: 'master',
+ default_branch: 'main',
},
- branchName: 'master',
+ branchName: 'main',
};
expect(getters.isOnDefaultBranch({}, localGetters)).toBeTruthy();
@@ -285,7 +285,7 @@ describe('IDE store getters', () => {
it("returns false when project's default branch doesn't match current branch", () => {
const localGetters = {
currentProject: {
- default_branch: 'master',
+ default_branch: 'main',
},
branchName: 'feature',
};
@@ -620,10 +620,10 @@ describe('IDE store getters', () => {
describe('getUrlForPath', () => {
it('returns a route url for the given path', () => {
localState.currentProjectId = 'test/test';
- localState.currentBranchId = 'master';
+ localState.currentBranchId = 'main';
expect(localStore.getters.getUrlForPath('path/to/foo/bar-1.jpg')).toBe(
- `/project/test/test/tree/master/-/path/to/foo/bar-1.jpg/`,
+ `/project/test/test/tree/main/-/path/to/foo/bar-1.jpg/`,
);
});
});
@@ -631,13 +631,13 @@ describe('IDE store getters', () => {
describe('getJsonSchemaForPath', () => {
beforeEach(() => {
localState.currentProjectId = 'path/to/some/project';
- localState.currentBranchId = 'master';
+ localState.currentBranchId = 'main';
});
it('returns a json schema uri and match config for a json/yaml file that can be loaded by monaco', () => {
expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
fileMatch: ['*.gitlab-ci.yml'],
- uri: `${TEST_HOST}/path/to/some/project/-/schema/master/.gitlab-ci.yml`,
+ uri: `${TEST_HOST}/path/to/some/project/-/schema/main/.gitlab-ci.yml`,
});
});
@@ -645,8 +645,8 @@ describe('IDE store getters', () => {
localState.projects['path/to/some/project'] = {
name: 'project',
branches: {
- master: {
- name: 'master',
+ main: {
+ name: 'main',
commit: {
id: 'abcdef123456',
},
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index b124eb391f3..cb6bb7c1202 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -47,7 +47,7 @@ describe('IDE commit module actions', () => {
jest.spyOn(router, 'push').mockImplementation();
mock
- .onGet('/api/v1/projects/abcproject/repository/branches/master')
+ .onGet('/api/v1/projects/abcproject/repository/branches/main')
.reply(200, { commit: COMMIT_RESPONSE });
});
@@ -101,7 +101,7 @@ describe('IDE commit module actions', () => {
originalGon = window.gon;
window.gon = { current_username: 'johndoe' };
- store.state.currentBranchId = 'master';
+ store.state.currentBranchId = 'main';
});
afterEach(() => {
@@ -177,7 +177,7 @@ describe('IDE commit module actions', () => {
committed_date: '123',
committer_name: 'root',
};
- const branch = 'master';
+ const branch = 'main';
let f;
beforeEach(() => {
@@ -192,12 +192,12 @@ describe('IDE commit module actions', () => {
Object.assign(store.state, {
currentProjectId: 'abcproject',
- currentBranchId: 'master',
+ currentBranchId: 'main',
projects: {
abcproject: {
web_url: 'web_url',
branches: {
- master: {
+ main: {
workingReference: '',
commit: {
short_id: TEST_COMMIT_SHA,
@@ -228,7 +228,7 @@ describe('IDE commit module actions', () => {
branch,
})
.then(() => {
- expect(store.state.projects.abcproject.branches.master.workingReference).toBe(data.id);
+ expect(store.state.projects.abcproject.branches.main.workingReference).toBe(data.id);
})
.then(done)
.catch(done.fail);
@@ -310,14 +310,14 @@ describe('IDE commit module actions', () => {
changedFiles: [f],
openFiles: [f],
currentProjectId: 'abcproject',
- currentBranchId: 'master',
+ currentBranchId: 'main',
projects: {
abcproject: {
- default_branch: 'master',
+ default_branch: 'main',
web_url: 'webUrl',
branches: {
- master: {
- name: 'master',
+ main: {
+ name: 'main',
workingReference: '1',
commit: {
id: TEST_COMMIT_SHA,
@@ -460,7 +460,7 @@ describe('IDE commit module actions', () => {
.dispatch('commit/commitChanges')
.then(() => {
expect(visitUrl).toHaveBeenCalledWith(
- `webUrl/-/merge_requests/new?merge_request[source_branch]=${store.getters['commit/placeholderBranchName']}&merge_request[target_branch]=master&nav_source=webide`,
+ `webUrl/-/merge_requests/new?merge_request[source_branch]=${store.getters['commit/placeholderBranchName']}&merge_request[target_branch]=main&nav_source=webide`,
);
done();
diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js
index 0dc938bb637..7a07ed05201 100644
--- a/spec/frontend/ide/stores/modules/commit/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js
@@ -46,7 +46,7 @@ describe('IDE commit module getters', () => {
describe('branchName', () => {
const rootState = {
- currentBranchId: 'master',
+ currentBranchId: 'main',
};
const localGetters = {
placeholderBranchName: 'placeholder-branch-name',
@@ -61,7 +61,7 @@ describe('IDE commit module getters', () => {
it('defaults to currentBranchId when not committing to a new branch', () => {
localGetters.isCreatingNewBranch = false;
- expect(getters.branchName(state, localGetters, rootState)).toBe('master');
+ expect(getters.branchName(state, localGetters, rootState)).toBe('main');
});
describe('commit to a new branch', () => {
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 e5887ca0a33..fc00bd075e7 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -13,7 +13,7 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/root';
-const TEST_BRANCH_ID = 'master';
+const TEST_BRANCH_ID = 'main';
const TEST_YAML_HELP_PATH = `${TEST_HOST}/test/yaml/help`;
const TEST_RUNNERS_HELP_PATH = `${TEST_HOST}/test/runners/help`;
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 e42e760b841..ecda7f304ba 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
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -11,7 +11,7 @@ import httpStatus from '~/lib/utils/http_status';
jest.mock('~/flash');
const TEST_PROJECT_PATH = 'lorem/root';
-const TEST_BRANCH_ID = 'master';
+const TEST_BRANCH_ID = 'main';
const TEST_SESSION = {
id: 7,
status: PENDING,
@@ -89,7 +89,9 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveStartSessionError({ dispatch });
- expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STARTING);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: messages.UNEXPECTED_ERROR_STARTING,
+ });
});
it('sets session status', () => {
@@ -161,7 +163,9 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveStopSessionError({ dispatch });
- expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STOPPING);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: messages.UNEXPECTED_ERROR_STOPPING,
+ });
});
it('kills the session', () => {
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 0227955754c..eabc69b23aa 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
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -115,7 +115,9 @@ describe('IDE store terminal session controls actions', () => {
it('flashes message', () => {
actions.receiveSessionStatusError({ dispatch });
- expect(createFlash).toHaveBeenCalledWith(messages.UNEXPECTED_ERROR_STATUS);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: messages.UNEXPECTED_ERROR_STATUS,
+ });
});
it('kills the session', () => {
diff --git a/spec/frontend/ide/stores/mutations/branch_spec.js b/spec/frontend/ide/stores/mutations/branch_spec.js
index 0900b25d5d3..30a688d2bb0 100644
--- a/spec/frontend/ide/stores/mutations/branch_spec.js
+++ b/spec/frontend/ide/stores/mutations/branch_spec.js
@@ -10,9 +10,9 @@ describe('Multi-file store branch mutations', () => {
describe('SET_CURRENT_BRANCH', () => {
it('sets currentBranch', () => {
- mutations.SET_CURRENT_BRANCH(localState, 'master');
+ mutations.SET_CURRENT_BRANCH(localState, 'main');
- expect(localState.currentBranchId).toBe('master');
+ expect(localState.currentBranchId).toBe('main');
});
});
@@ -21,20 +21,20 @@ describe('Multi-file store branch mutations', () => {
localState.projects = {
Example: {
branches: {
- master: {},
+ main: {},
},
},
};
mutations.SET_BRANCH_COMMIT(localState, {
projectId: 'Example',
- branchId: 'master',
+ branchId: 'main',
commit: {
title: 'Example commit',
},
});
- expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
+ expect(localState.projects.Example.branches.main.commit.title).toBe('Example commit');
});
});
diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js
index 825d2a546cd..1453f26c1d9 100644
--- a/spec/frontend/ide/stores/mutations/file_spec.js
+++ b/spec/frontend/ide/stores/mutations/file_spec.js
@@ -319,8 +319,8 @@ describe('IDE store file mutations', () => {
localFile.content = 'test';
localFile.changed = true;
localState.currentProjectId = 'gitlab-ce';
- localState.currentBranchId = 'master';
- localState.trees['gitlab-ce/master'] = {
+ localState.currentBranchId = 'main';
+ localState.trees['gitlab-ce/main'] = {
tree: [],
};
});
@@ -337,7 +337,7 @@ describe('IDE store file mutations', () => {
mutations.DISCARD_FILE_CHANGES(localState, localFile.path);
- expect(localState.trees['gitlab-ce/master'].tree).toEqual([{ ...localFile, deleted: false }]);
+ expect(localState.trees['gitlab-ce/main'].tree).toEqual([{ ...localFile, deleted: false }]);
});
it('adds to parent tree if deleted', () => {
diff --git a/spec/frontend/ide/stores/mutations/tree_spec.js b/spec/frontend/ide/stores/mutations/tree_spec.js
index a4b98aa9d5a..6935e57578f 100644
--- a/spec/frontend/ide/stores/mutations/tree_spec.js
+++ b/spec/frontend/ide/stores/mutations/tree_spec.js
@@ -33,16 +33,16 @@ describe('Multi-file store tree mutations', () => {
});
it('adds directory data', () => {
- localState.trees['project/master'] = {
+ localState.trees['project/main'] = {
tree: [],
};
mutations.SET_DIRECTORY_DATA(localState, {
data,
- treePath: 'project/master',
+ treePath: 'project/main',
});
- const tree = localState.trees['project/master'];
+ const tree = localState.trees['project/main'];
expect(tree.tree.length).toBe(3);
expect(tree.tree[0].name).toBe('tree');
@@ -52,30 +52,30 @@ describe('Multi-file store tree mutations', () => {
it('keeps loading state', () => {
mutations.CREATE_TREE(localState, {
- treePath: 'project/master',
+ treePath: 'project/main',
});
mutations.SET_DIRECTORY_DATA(localState, {
data,
- treePath: 'project/master',
+ treePath: 'project/main',
});
- expect(localState.trees['project/master'].loading).toBe(true);
+ expect(localState.trees['project/main'].loading).toBe(true);
});
it('does not override tree already in state, but merges the two with correct order', () => {
const openedFile = file('new');
- localState.trees['project/master'] = {
+ localState.trees['project/main'] = {
loading: true,
tree: [openedFile],
};
mutations.SET_DIRECTORY_DATA(localState, {
data,
- treePath: 'project/master',
+ treePath: 'project/main',
});
- const { tree } = localState.trees['project/master'];
+ const { tree } = localState.trees['project/main'];
expect(tree.length).toBe(4);
expect(tree[0].name).toBe('blob');
@@ -86,17 +86,17 @@ describe('Multi-file store tree mutations', () => {
it('returns tree unchanged if the opened file is already in the tree', () => {
const openedFile = file('foo');
- localState.trees['project/master'] = {
+ localState.trees['project/main'] = {
loading: true,
tree: [openedFile],
};
mutations.SET_DIRECTORY_DATA(localState, {
data,
- treePath: 'project/master',
+ treePath: 'project/main',
});
- const { tree } = localState.trees['project/master'];
+ const { tree } = localState.trees['project/main'];
expect(tree.length).toBe(3);
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 09e9481e5d4..23fe23bdef9 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -98,8 +98,8 @@ describe('Multi-file store mutations', () => {
describe('CREATE_TMP_ENTRY', () => {
beforeEach(() => {
localState.currentProjectId = 'gitlab-ce';
- localState.currentBranchId = 'master';
- localState.trees['gitlab-ce/master'] = {
+ localState.currentBranchId = 'main';
+ localState.trees['gitlab-ce/main'] = {
tree: [],
};
});
@@ -115,7 +115,7 @@ describe('Multi-file store mutations', () => {
},
});
- expect(localState.trees['gitlab-ce/master'].tree.length).toEqual(1);
+ expect(localState.trees['gitlab-ce/main'].tree.length).toEqual(1);
expect(localState.entries.test.tempFile).toEqual(true);
});
});
@@ -163,8 +163,8 @@ describe('Multi-file store mutations', () => {
describe('DELETE_ENTRY', () => {
beforeEach(() => {
localState.currentProjectId = 'gitlab-ce';
- localState.currentBranchId = 'master';
- localState.trees['gitlab-ce/master'] = {
+ localState.currentBranchId = 'main';
+ localState.trees['gitlab-ce/main'] = {
tree: [],
};
});
@@ -184,11 +184,11 @@ describe('Multi-file store mutations', () => {
path: 'filePath',
deleted: false,
};
- localState.trees['gitlab-ce/master'].tree.push(localState.entries.filePath);
+ localState.trees['gitlab-ce/main'].tree.push(localState.entries.filePath);
mutations.DELETE_ENTRY(localState, 'filePath');
- expect(localState.trees['gitlab-ce/master'].tree).toEqual([]);
+ expect(localState.trees['gitlab-ce/main'].tree).toEqual([]);
});
it('removes from parent tree', () => {
@@ -279,12 +279,12 @@ describe('Multi-file store mutations', () => {
describe('RENAME_ENTRY', () => {
beforeEach(() => {
localState.trees = {
- 'gitlab-ce/master': {
+ 'gitlab-ce/main': {
tree: [],
},
};
localState.currentProjectId = 'gitlab-ce';
- localState.currentBranchId = 'master';
+ localState.currentBranchId = 'main';
localState.entries = {
oldPath: file('oldPath', 'oldPath', 'blob'),
};
@@ -462,7 +462,7 @@ describe('Multi-file store mutations', () => {
gamma,
};
- localState.trees['gitlab-ce/master'].tree = [alpha, beta, gamma];
+ localState.trees['gitlab-ce/main'].tree = [alpha, beta, gamma];
mutations.RENAME_ENTRY(localState, {
path: 'alpha',
@@ -471,7 +471,7 @@ describe('Multi-file store mutations', () => {
parentPath: '',
});
- expect(localState.trees['gitlab-ce/master'].tree).toEqual([
+ expect(localState.trees['gitlab-ce/main'].tree).toEqual([
expect.objectContaining({
name: 'beta',
}),
diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js
index 46a0794b2e6..8f7b8c5e311 100644
--- a/spec/frontend/ide/stores/utils_spec.js
+++ b/spec/frontend/ide/stores/utils_spec.js
@@ -18,13 +18,13 @@ describe('Multi-file store utils', () => {
};
const state = {
- currentBranchId: 'master',
+ currentBranchId: 'main',
currentProjectId: 'test/test',
};
utils.setPageTitleForFile(state, f);
- expect(document.title).toBe('README.md · master · test/test · GitLab');
+ expect(document.title).toBe('README.md · main · test/test · GitLab');
});
});
@@ -52,10 +52,10 @@ describe('Multi-file store utils', () => {
{ ...file('deletedFile'), path: 'deletedFile', deleted: true },
{ ...file('renamedFile'), path: 'renamedFile', prevPath: 'prevPath' },
],
- currentBranchId: 'master',
+ currentBranchId: 'main',
};
const payload = utils.createCommitPayload({
- branch: 'master',
+ branch: 'main',
newBranch: false,
state,
rootState,
@@ -63,7 +63,7 @@ describe('Multi-file store utils', () => {
});
expect(payload).toEqual({
- branch: 'master',
+ branch: 'main',
commit_message: 'commit message',
actions: [
{
@@ -122,10 +122,10 @@ describe('Multi-file store utils', () => {
lastCommitSha: '123456789',
},
],
- currentBranchId: 'master',
+ currentBranchId: 'main',
};
const payload = utils.createCommitPayload({
- branch: 'master',
+ branch: 'main',
newBranch: false,
state: {},
rootState,
@@ -135,7 +135,7 @@ describe('Multi-file store utils', () => {
});
expect(payload).toEqual({
- branch: 'master',
+ branch: 'main',
commit_message: 'prebuilt test commit message',
actions: [
{
@@ -377,7 +377,7 @@ describe('Multi-file store utils', () => {
let localState;
let branchInfo;
const currentProjectId = '123-foo';
- const currentBranchId = 'master';
+ const currentBranchId = 'main';
beforeEach(() => {
localState = {
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index f467d174eeb..00733615f81 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -274,33 +274,33 @@ describe('WebIDE utils', () => {
* hello.md -> hello-1.md
* hello_2.md -> hello_3.md
* hello_ -> hello_1
- * master-patch-22432 -> master-patch-22433
+ * main-patch-22432 -> main-patch-22433
* patch_332 -> patch_333
*/
describe('addNumericSuffix', () => {
it.each`
- input | output
- ${'hello'} | ${'hello-1'}
- ${'hello2'} | ${'hello-3'}
- ${'hello.md'} | ${'hello-1.md'}
- ${'hello_2.md'} | ${'hello_3.md'}
- ${'hello_'} | ${'hello_1'}
- ${'master-patch-22432'} | ${'master-patch-22433'}
- ${'patch_332'} | ${'patch_333'}
+ input | output
+ ${'hello'} | ${'hello-1'}
+ ${'hello2'} | ${'hello-3'}
+ ${'hello.md'} | ${'hello-1.md'}
+ ${'hello_2.md'} | ${'hello_3.md'}
+ ${'hello_'} | ${'hello_1'}
+ ${'main-patch-22432'} | ${'main-patch-22433'}
+ ${'patch_332'} | ${'patch_333'}
`('adds a numeric suffix to a given filename/branch name: $input', ({ input, output }) => {
expect(addNumericSuffix(input)).toBe(output);
});
it.each`
- input | output
- ${'hello'} | ${'hello-39135'}
- ${'hello2'} | ${'hello-39135'}
- ${'hello.md'} | ${'hello-39135.md'}
- ${'hello_2.md'} | ${'hello_39135.md'}
- ${'hello_'} | ${'hello_39135'}
- ${'master-patch-22432'} | ${'master-patch-39135'}
- ${'patch_332'} | ${'patch_39135'}
+ input | output
+ ${'hello'} | ${'hello-39135'}
+ ${'hello2'} | ${'hello-39135'}
+ ${'hello.md'} | ${'hello-39135.md'}
+ ${'hello_2.md'} | ${'hello_39135.md'}
+ ${'hello_'} | ${'hello_39135'}
+ ${'main-patch-22432'} | ${'main-patch-39135'}
+ ${'patch_332'} | ${'patch_39135'}
`('adds a random suffix if randomize=true is passed for name: $input', ({ input, output }) => {
jest.spyOn(Math, 'random').mockReturnValue(0.391352525);
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
index 0c69cfb3bc5..aa6a40cad18 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
@@ -5,11 +5,15 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { STATUSES } from '~/import_entities/constants';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
-import groupQuery from '~/import_entities/import_groups/graphql/queries/group.query.graphql';
+import addValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql';
+import removeValidationErrorMutation from '~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql';
+import groupAndProjectQuery from '~/import_entities/import_groups/graphql/queries/groupAndProject.query.graphql';
import { availableNamespacesFixture } from '../graphql/fixtures';
Vue.use(VueApollo);
+const { i18n: I18N } = ImportTableRow;
+
const getFakeGroup = (status) => ({
web_url: 'https://fake.host/',
full_path: 'fake_group_1',
@@ -25,6 +29,7 @@ const getFakeGroup = (status) => ({
const EXISTING_GROUP_TARGET_NAMESPACE = 'existing-group';
const EXISTING_GROUP_PATH = 'existing-path';
+const EXISTING_PROJECT_PATH = 'existing-project-path';
describe('import table row', () => {
let wrapper;
@@ -41,13 +46,19 @@ describe('import table row', () => {
const createComponent = (props) => {
apolloProvider = createMockApollo([
[
- groupQuery,
+ groupAndProjectQuery,
({ fullPath }) => {
const existingGroup =
fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_GROUP_PATH}`
? { id: 1 }
: null;
- return Promise.resolve({ data: { existingGroup } });
+
+ const existingProject =
+ fullPath === `${EXISTING_GROUP_TARGET_NAMESPACE}/${EXISTING_PROJECT_PATH}`
+ ? { id: 1 }
+ : null;
+
+ return Promise.resolve({ data: { existingGroup, existingProject } });
},
],
]);
@@ -173,7 +184,7 @@ describe('import table row', () => {
});
describe('validations', () => {
- it('Reports invalid group name when name is not matching regex', () => {
+ it('reports invalid group name when name is not matching regex', () => {
createComponent({
group: {
...getFakeGroup(STATUSES.NONE),
@@ -188,7 +199,7 @@ describe('import table row', () => {
expect(wrapper.text()).toContain('Please choose a group URL with no special characters.');
});
- it('Reports invalid group name if relevant validation error exists', async () => {
+ it('reports invalid group name if relevant validation error exists', async () => {
const FAKE_ERROR_MESSAGE = 'fake error';
createComponent({
@@ -208,5 +219,101 @@ describe('import table row', () => {
expect(wrapper.text()).toContain(FAKE_ERROR_MESSAGE);
});
+
+ it('sets validation error when targetting existing group', async () => {
+ const testGroup = getFakeGroup(STATUSES.NONE);
+
+ createComponent({
+ group: {
+ ...testGroup,
+ import_target: {
+ target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
+ new_name: EXISTING_GROUP_PATH,
+ },
+ },
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate');
+
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: addValidationErrorMutation,
+ variables: {
+ field: 'new_name',
+ message: I18N.NAME_ALREADY_EXISTS,
+ sourceGroupId: testGroup.id,
+ },
+ });
+ });
+
+ it('sets validation error when targetting existing project', async () => {
+ const testGroup = getFakeGroup(STATUSES.NONE);
+
+ createComponent({
+ group: {
+ ...testGroup,
+ import_target: {
+ target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
+ new_name: EXISTING_PROJECT_PATH,
+ },
+ },
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate');
+
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: addValidationErrorMutation,
+ variables: {
+ field: 'new_name',
+ message: I18N.NAME_ALREADY_EXISTS,
+ sourceGroupId: testGroup.id,
+ },
+ });
+ });
+
+ it('clears validation error when target is updated', async () => {
+ const testGroup = getFakeGroup(STATUSES.NONE);
+
+ createComponent({
+ group: {
+ ...testGroup,
+ import_target: {
+ target_namespace: EXISTING_GROUP_TARGET_NAMESPACE,
+ new_name: EXISTING_PROJECT_PATH,
+ },
+ },
+ });
+
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate');
+
+ await wrapper.setProps({
+ group: {
+ ...testGroup,
+ import_target: {
+ target_namespace: 'valid_namespace',
+ new_name: 'valid_path',
+ },
+ },
+ });
+
+ jest.runOnlyPendingTimers();
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: removeValidationErrorMutation,
+ variables: {
+ field: 'new_name',
+ sourceGroupId: testGroup.id,
+ },
+ });
+ });
});
});
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 9bff77cd34a..f2bfc61381c 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import actionsFactory from '~/import_entities/import_projects/store/actions';
import { getImportTarget } from '~/import_entities/import_projects/store/getters';
@@ -168,7 +168,9 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith('Provider rate limit exceeded. Try again later');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Provider rate limit exceeded. Try again later',
+ });
});
});
@@ -245,7 +247,9 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith('Importing the project failed');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Importing the project failed',
+ });
});
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => {
@@ -266,7 +270,9 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith(`Importing the project failed: ${ERROR_MESSAGE}`);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `Importing the project failed: ${ERROR_MESSAGE}`,
+ });
});
});
@@ -365,7 +371,9 @@ describe('import_projects store actions', () => {
[],
);
- expect(createFlash).toHaveBeenCalledWith('Requesting namespaces failed');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Requesting namespaces failed',
+ });
});
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index c7286d70b94..8d4ccab2a40 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -43,12 +43,10 @@ describe('Incidents List', () => {
const findLoader = () => wrapper.find(GlLoadingIcon);
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
- const findIncidentSlaHeader = () => wrapper.find('[data-testid="incident-management-sla"]');
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findEmptyState = () => wrapper.find(GlEmptyState);
const findSeverity = () => wrapper.findAll(SeverityToken);
- const findIncidentSla = () => wrapper.findAll("[data-testid='incident-sla']");
function mountComponent({ data = {}, loading = false, provide = {} } = {}) {
wrapper = mount(IncidentsList, {
@@ -188,35 +186,6 @@ describe('Incidents List', () => {
joinPaths(`/project/issues/incident`, mockIncidents[0].iid),
);
});
-
- describe('Incident SLA field', () => {
- it('displays the column when the feature is available', () => {
- mountComponent({
- data: { incidents: { list: mockIncidents } },
- provide: { slaFeatureAvailable: true },
- });
-
- expect(findIncidentSlaHeader().text()).toContain('Time to SLA');
- });
-
- it('does not display the column when the feature is not available', () => {
- mountComponent({
- data: { incidents: { list: mockIncidents } },
- provide: { slaFeatureAvailable: false },
- });
-
- expect(findIncidentSlaHeader().exists()).toBe(false);
- });
-
- it('renders an SLA for each incident', () => {
- mountComponent({
- data: { incidents: { list: mockIncidents } },
- provide: { slaFeatureAvailable: true },
- });
-
- expect(findIncidentSla().length).toBe(mockIncidents.length);
- });
- });
});
describe('Create Incident', () => {
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
index 07f90a12f0f..4f70f908c4a 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
@@ -2,7 +2,7 @@
exports[`IncidentsSettingTabs should render the component 1`] = `
<section
- class="settings no-animate qa-incident-management-settings"
+ class="settings no-animate"
data-qa-selector="incidents_settings_content"
id="incident-management-settings"
>
@@ -30,7 +30,7 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
<p>
- Set up integrations with external tools to help better manage incidents.
+ Fine-tune incident settings and set up integrations with external tools to help better manage incidents.
</p>
</div>
@@ -41,15 +41,8 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
<gl-tabs-stub
theme="indigo"
>
- <gl-tab-stub
- title="Alert integration"
- titlelinkclass=""
- >
- <alertssettingsform-stub
- class="gl-pt-3"
- data-testid="AlertsSettingsForm-tab"
- />
- </gl-tab-stub>
+ <!---->
+
<gl-tab-stub
title="PagerDuty integration"
titlelinkclass=""
@@ -59,9 +52,6 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
data-testid="PagerDutySettingsForm-tab"
/>
</gl-tab-stub>
- <!---->
-
- <!---->
</gl-tabs-stub>
</div>
</section>
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
index 79ad5ad1bb9..2a976c04319 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
@@ -13,6 +13,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
<form>
<gl-form-group-stub
class="col-8 col-md-9 gl-p-0"
+ labeldescription=""
>
<gl-toggle-stub
id="active"
@@ -26,6 +27,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
class="col-8 col-md-9 gl-p-0"
label="Webhook URL"
label-for="url"
+ labeldescription=""
>
<gl-form-input-group-stub
data-testid="webhook-url"
@@ -66,20 +68,6 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
</gl-modal-stub>
</gl-form-group-stub>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="js-no-auto-disable"
- icon=""
- size="medium"
- type="submit"
- variant="success"
- >
-
- Save changes
-
- </gl-button-stub>
</form>
</div>
`;
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 5476e895c68..f4342c56f98 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -1,5 +1,5 @@
import AxiosMockAdapter from 'axios-mock-adapter';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { ERROR_MSG } from '~/incidents_settings/constants';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
@@ -37,7 +37,10 @@ describe('IncidentsSettingsService', () => {
mock.onPatch().reply(httpStatusCodes.BAD_REQUEST);
return service.updateSettings({}).then(() => {
- expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringContaining(ERROR_MSG),
+ type: 'alert',
+ });
});
});
});
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
index 2ffd1292ddc..d2b591d427d 100644
--- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -1,5 +1,5 @@
-import { GlAlert, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlModal, GlToggle } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue';
@@ -8,13 +8,13 @@ describe('Alert integration settings form', () => {
const resetWebhookUrl = jest.fn();
const service = { updateSettings: jest.fn().mockResolvedValue(), resetWebhookUrl };
- const findForm = () => wrapper.find({ ref: 'settingsForm' });
- const findWebhookInput = () => wrapper.find('[data-testid="webhook-url"]');
- const findModal = () => wrapper.find(GlModal);
- const findAlert = () => wrapper.find(GlAlert);
+ const findWebhookInput = () => wrapper.findByTestId('webhook-url');
+ const findFormToggle = () => wrapper.findComponent(GlToggle);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
- wrapper = shallowMount(PagerDutySettingsForm, {
+ wrapper = shallowMountExtended(PagerDutySettingsForm, {
provide: {
service,
pagerDutySettings: {
@@ -27,18 +27,15 @@ describe('Alert integration settings form', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
it('should match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('should call service `updateSettings` on form submit', () => {
- findForm().trigger('submit');
+ it('should call service `updateSettings` on toggle change', () => {
+ findFormToggle().vm.$emit('change', true);
expect(service.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({ pagerduty_active: wrapper.vm.active }),
);
diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
index 0e56fb6454e..df7ffd19747 100644
--- a/spec/frontend/integrations/edit/components/active_checkbox_spec.js
+++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
@@ -1,5 +1,6 @@
import { GlFormCheckbox } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import { createStore } from '~/integrations/edit/store';
diff --git a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
index 1c126f60c37..805d3971994 100644
--- a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
+++ b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
@@ -1,5 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import { createStore } from '~/integrations/edit/store';
@@ -13,13 +14,10 @@ describe('ConfirmationModal', () => {
};
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
- const findGlModal = () => wrapper.find(GlModal);
+ const findGlModal = () => wrapper.findComponent(GlModal);
describe('template', () => {
beforeEach(() => {
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index 2ebb3333c0f..8784b3c2b00 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -1,5 +1,6 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
describe('DynamicField', () => {
@@ -24,17 +25,14 @@ describe('DynamicField', () => {
};
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
- const findGlFormGroup = () => wrapper.find(GlFormGroup);
- const findGlFormCheckbox = () => wrapper.find(GlFormCheckbox);
- const findGlFormInput = () => wrapper.find(GlFormInput);
- const findGlFormSelect = () => wrapper.find(GlFormSelect);
- const findGlFormTextarea = () => wrapper.find(GlFormTextarea);
+ const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findGlFormInput = () => wrapper.findComponent(GlFormInput);
+ const findGlFormSelect = () => wrapper.findComponent(GlFormSelect);
+ const findGlFormTextarea = () => wrapper.findComponent(GlFormTextarea);
describe('template', () => {
describe.each([
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index c015fd0b9e0..cbce26762b1 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
@@ -23,42 +23,37 @@ describe('IntegrationForm', () => {
initialState = {},
props = {},
} = {}) => {
- wrapper = extendedWrapper(
- shallowMount(IntegrationForm, {
- propsData: { ...props },
- store: createStore({
- customState: { ...mockIntegrationProps, ...customStateProps },
- ...initialState,
- }),
- stubs: {
- OverrideDropdown,
- ActiveCheckbox,
- ConfirmationModal,
- JiraTriggerFields,
- TriggerFields,
- },
- provide: {
- glFeatures: featureFlags,
- },
+ wrapper = shallowMountExtended(IntegrationForm, {
+ propsData: { ...props },
+ store: createStore({
+ customState: { ...mockIntegrationProps, ...customStateProps },
+ ...initialState,
}),
- );
+ stubs: {
+ OverrideDropdown,
+ ActiveCheckbox,
+ ConfirmationModal,
+ JiraTriggerFields,
+ TriggerFields,
+ },
+ provide: {
+ glFeatures: featureFlags,
+ },
+ });
};
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
- const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
- const findActiveCheckbox = () => wrapper.find(ActiveCheckbox);
- const findConfirmationModal = () => wrapper.find(ConfirmationModal);
- const findResetConfirmationModal = () => wrapper.find(ResetConfirmationModal);
- const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
- const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
- const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
- const findTriggerFields = () => wrapper.find(TriggerFields);
+ const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown);
+ const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
+ const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
+ const findResetConfirmationModal = () => wrapper.findComponent(ResetConfirmationModal);
+ const findResetButton = () => wrapper.findByTestId('reset-button');
+ const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
+ const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
+ const findTriggerFields = () => wrapper.findComponent(TriggerFields);
describe('template', () => {
describe('showActive is true', () => {
@@ -286,7 +281,7 @@ describe('IntegrationForm', () => {
</div>
`);
- it('renders `helpHtml`', async () => {
+ it('renders `helpHtml`', () => {
const mockHelpHtml = document.querySelector(`[data-testid="${mockTestId}"]`);
createComponent({
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index f121a148f27..eb5f7e9fe40 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,10 +1,13 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import JiraUpgradeCta from '~/integrations/edit/components/jira_upgrade_cta.vue';
import eventHub from '~/integrations/edit/event_hub';
+import { createStore } from '~/integrations/edit/store';
describe('JiraIssuesFields', () => {
+ let store;
let wrapper;
const defaultProps = {
@@ -13,25 +16,29 @@ describe('JiraIssuesFields', () => {
showJiraVulnerabilitiesIntegration: true,
};
- const createComponent = ({ props, ...options } = {}) => {
- wrapper = mount(JiraIssuesFields, {
+ const createComponent = ({ isInheriting = false, props, ...options } = {}) => {
+ store = createStore({
+ defaultState: isInheriting ? {} : undefined,
+ });
+
+ wrapper = mountExtended(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
+ store,
stubs: ['jira-issue-creation-vulnerabilities'],
...options,
});
};
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
const findEnableCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findEnableCheckboxDisabled = () =>
+ findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput);
const findJiraUpgradeCta = () => wrapper.findComponent(JiraUpgradeCta);
- const findJiraForVulnerabilities = () => wrapper.find('[data-testid="jira-for-vulnerabilities"]');
+ const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
@@ -79,6 +86,19 @@ describe('JiraIssuesFields', () => {
createComponent({ props: { initialProjectKey: '' } });
});
+ it('renders enabled checkbox', () => {
+ expect(findEnableCheckbox().exists()).toBe(true);
+ expect(findEnableCheckboxDisabled()).toBeUndefined();
+ });
+
+ it('renders disabled project_key input', () => {
+ const projectKey = findProjectKey();
+
+ expect(projectKey.exists()).toBe(true);
+ expect(projectKey.attributes('disabled')).toBe('disabled');
+ expect(projectKey.attributes('required')).toBeUndefined();
+ });
+
it('does not show upgrade banner', () => {
expect(findJiraUpgradeCta().exists()).toBe(false);
});
@@ -89,24 +109,20 @@ describe('JiraIssuesFields', () => {
expect(wrapper.find('input[name="service[issues_enabled]"]').exists()).toBe(true);
});
- it('disables project_key input', () => {
- expect(findProjectKey().attributes('disabled')).toBe('disabled');
- });
+ describe('when isInheriting = true', () => {
+ it('disables checkbox and sets input as readonly', () => {
+ createComponent({ isInheriting: true });
- it('does not require project_key', () => {
- expect(findProjectKey().attributes('required')).toBeUndefined();
+ expect(findEnableCheckboxDisabled()).toBe('disabled');
+ expect(findProjectKey().attributes('readonly')).toBe('readonly');
+ });
});
describe('on enable issues', () => {
- it('enables project_key input', async () => {
+ it('enables project_key input as required', async () => {
await setEnableCheckbox(true);
expect(findProjectKey().attributes('disabled')).toBeUndefined();
- });
-
- it('requires project_key input', async () => {
- await setEnableCheckbox(true);
-
expect(findProjectKey().attributes('required')).toBe('required');
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
index 5c04add61a1..9e01371f542 100644
--- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
@@ -1,5 +1,6 @@
import { GlFormCheckbox } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
describe('JiraTriggerFields', () => {
@@ -12,7 +13,7 @@ describe('JiraTriggerFields', () => {
};
const createComponent = (props, isInheriting = false) => {
- wrapper = mount(JiraTriggerFields, {
+ wrapper = mountExtended(JiraTriggerFields, {
propsData: { ...defaultProps, ...props },
computed: {
isInheriting: () => isInheriting,
@@ -21,18 +22,15 @@ describe('JiraTriggerFields', () => {
};
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
- const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]');
- const findCommentDetail = () => wrapper.find('[data-testid="comment-detail"]');
- const findCommentSettingsCheckbox = () => findCommentSettings().find(GlFormCheckbox);
+ const findCommentSettings = () => wrapper.findByTestId('comment-settings');
+ const findCommentDetail = () => wrapper.findByTestId('comment-detail');
+ const findCommentSettingsCheckbox = () => findCommentSettings().findComponent(GlFormCheckbox);
const findIssueTransitionEnabled = () =>
wrapper.find('[data-testid="issue-transition-enabled"] input[type="checkbox"]');
- const findIssueTransitionMode = () => wrapper.find('[data-testid="issue-transition-mode"]');
+ const findIssueTransitionMode = () => wrapper.findByTestId('issue-transition-mode');
const findIssueTransitionModeRadios = () =>
findIssueTransitionMode().findAll('input[type="radio"]');
const findIssueTransitionIdsField = () =>
diff --git a/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js b/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js
index e49a1619627..e90e9a5d2ac 100644
--- a/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_upgrade_cta_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+
import JiraUpgradeCta from '~/integrations/edit/components/jira_upgrade_cta.vue';
describe('JiraUpgradeCta', () => {
@@ -18,13 +19,13 @@ describe('JiraUpgradeCta', () => {
it('displays the correct message for premium and lower users', () => {
createComponent({ showPremiumMessage: true });
- expect(wrapper.html()).toContain('This is a Premium feature');
- expect(wrapper.html()).toContain(contentMessage);
+ expect(wrapper.text()).toContain('This is a Premium feature');
+ expect(wrapper.text()).toContain(contentMessage);
});
it('displays the correct message for ultimate and lower users', () => {
createComponent({ showUltimateMessage: true });
- expect(wrapper.html()).toContain('This is an Ultimate feature');
- expect(wrapper.html()).toContain(contentMessage);
+ expect(wrapper.text()).toContain('This is an Ultimate feature');
+ expect(wrapper.text()).toContain(contentMessage);
});
});
diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
index 592f4514e45..eb43d940f5e 100644
--- a/spec/frontend/integrations/edit/components/override_dropdown_spec.js
+++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import { integrationLevels, overrideDropdownDescriptions } from '~/integrations/edit/constants';
import { createStore } from '~/integrations/edit/store';
@@ -26,14 +27,11 @@ describe('OverrideDropdown', () => {
};
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
- const findGlLink = () => wrapper.find(GlLink);
- const findGlDropdown = () => wrapper.find(GlDropdown);
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
describe('template', () => {
describe('override prop is true', () => {
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index b9d16464e72..a0816682741 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -1,5 +1,6 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
describe('TriggerFields', () => {
@@ -10,7 +11,7 @@ describe('TriggerFields', () => {
};
const createComponent = (props, isInheriting = false) => {
- wrapper = mount(TriggerFields, {
+ wrapper = mountExtended(TriggerFields, {
propsData: { ...defaultProps, ...props },
computed: {
isInheriting: () => isInheriting,
@@ -19,21 +20,19 @@ describe('TriggerFields', () => {
};
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
+ wrapper.destroy();
});
+ const findTriggerLabel = () => wrapper.findByTestId('trigger-fields-group').find('label');
const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAll(GlFormGroup);
- const findAllGlFormCheckboxes = () => wrapper.findAll(GlFormCheckbox);
- const findAllGlFormInputs = () => wrapper.findAll(GlFormInput);
+ const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
+ const findAllGlFormInputs = () => wrapper.findAllComponents(GlFormInput);
describe.each([true, false])('template, isInheriting = `%p`', (isInheriting) => {
it('renders a label with text "Trigger"', () => {
createComponent();
- const triggerLabel = wrapper.find('[data-testid="trigger-fields-group"]').find('label');
+ const triggerLabel = findTriggerLabel();
expect(triggerLabel.exists()).toBe(true);
expect(triggerLabel.text()).toBe('Trigger');
});
@@ -68,7 +67,7 @@ describe('TriggerFields', () => {
});
it('renders GlFormInput with description for each event', () => {
- const groups = wrapper.find('#trigger-fields').findAll(GlFormGroup);
+ const groups = findAllGlFormGroups();
expect(groups).toHaveLength(2);
groups.wrappers.forEach((group, index) => {
@@ -138,11 +137,11 @@ describe('TriggerFields', () => {
const expectedResults = [
{
name: 'service[push_channel]',
- placeholder: 'general, development',
+ placeholder: '#general, #development',
},
{
name: 'service[merge_request_channel]',
- placeholder: 'general, development',
+ placeholder: '#general, #development',
},
];
diff --git a/spec/frontend/integrations/index/components/integrations_list_spec.js b/spec/frontend/integrations/index/components/integrations_list_spec.js
index 94fd7fc84ee..ee54a5fd359 100644
--- a/spec/frontend/integrations/index/components/integrations_list_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_list_spec.js
@@ -1,5 +1,5 @@
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
import IntegrationsList from '~/integrations/index/components/integrations_list.vue';
import { mockActiveIntegrations, mockInactiveIntegrations } from '../mock_data';
@@ -10,7 +10,7 @@ describe('IntegrationsList', () => {
const findInactiveIntegrationsTable = () => wrapper.findByTestId('inactive-integrations-table');
const createComponent = (propsData = {}) => {
- wrapper = extendedWrapper(shallowMount(IntegrationsList, { propsData }));
+ wrapper = shallowMountExtended(IntegrationsList, { propsData });
};
afterEach(() => {
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index 2a6985de136..2ef8fe07650 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -1,22 +1,22 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import Api from '~/api';
+import * as groupsApi from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue';
const createComponent = () => {
return mount(GroupSelect, {});
};
-const group1 = { id: 1, full_name: 'Group One' };
-const group2 = { id: 2, full_name: 'Group Two' };
+const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
+const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
describe('GroupSelect', () => {
let wrapper;
beforeEach(() => {
- jest.spyOn(Api, 'groups').mockResolvedValue(allGroups);
+ jest.spyOn(groupsApi, 'getGroups').mockResolvedValue(allGroups);
wrapper = createComponent();
});
@@ -29,10 +29,10 @@ describe('GroupSelect', () => {
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]');
- const findDropdownItemByText = (text) =>
+ const findAvatarByLabel = (text) =>
wrapper
- .findAllComponents(GlDropdownItem)
- .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text);
+ .findAllComponents(GlAvatarLabeled)
+ .wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('label') === text);
it('renders GlSearchBoxByType with default attributes', () => {
expect(findSearchBoxByType().exists()).toBe(true);
@@ -45,7 +45,7 @@ describe('GroupSelect', () => {
let resolveApiRequest;
beforeEach(() => {
- jest.spyOn(Api, 'groups').mockImplementation(
+ jest.spyOn(groupsApi, 'getGroups').mockImplementation(
() =>
new Promise((resolve) => {
resolveApiRequest = resolve;
@@ -58,7 +58,7 @@ describe('GroupSelect', () => {
it('calls the API', () => {
resolveApiRequest({ data: allGroups });
- expect(Api.groups).toHaveBeenCalledWith(group1.name, {
+ expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
active: true,
exclude_internal: true,
});
@@ -74,9 +74,20 @@ describe('GroupSelect', () => {
});
});
+ describe('avatar label', () => {
+ it('includes the correct attributes with name and avatar_url', () => {
+ expect(findAvatarByLabel(group1.full_name).attributes()).toMatchObject({
+ src: group1.avatar_url,
+ 'entity-id': `${group1.id}`,
+ 'entity-name': group1.full_name,
+ size: '32',
+ });
+ });
+ });
+
describe('when group is selected from the dropdown', () => {
beforeEach(() => {
- findDropdownItemByText(group1.full_name).vm.$emit('click');
+ findAvatarByLabel(group1.full_name).trigger('click');
});
it('emits `input` event used by `v-model`', () => {
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 7ed18775693..eabbea84234 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -15,6 +15,7 @@ const isProject = false;
const inviteeType = 'members';
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const defaultAccessLevel = 10;
+const inviteSource = 'unknown';
const helpLink = 'https://example.com';
const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
@@ -173,6 +174,7 @@ describe('InviteMembersModal', () => {
user_id: '1',
access_level: defaultAccessLevel,
expires_at: undefined,
+ invite_source: inviteSource,
format: 'json',
};
@@ -245,6 +247,7 @@ describe('InviteMembersModal', () => {
access_level: defaultAccessLevel,
expires_at: undefined,
email: 'email@example.com',
+ invite_source: inviteSource,
format: 'json',
};
@@ -293,6 +296,7 @@ describe('InviteMembersModal', () => {
const postData = {
access_level: defaultAccessLevel,
expires_at: undefined,
+ invite_source: inviteSource,
format: 'json',
};
@@ -308,20 +312,39 @@ describe('InviteMembersModal', () => {
jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData });
jest.spyOn(wrapper.vm, 'showToastMessageSuccess');
jest.spyOn(wrapper.vm, 'trackInvite');
-
- clickInviteButton();
});
- it('calls Api inviteGroupMembersByEmail with the correct params', () => {
- expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, emailPostData);
- });
+ describe('when triggered from regular mounting', () => {
+ beforeEach(() => {
+ clickInviteButton();
+ });
- it('calls Api addGroupMembersByUserId with the correct params', () => {
- expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, idPostData);
+ it('calls Api inviteGroupMembersByEmail with the correct params', () => {
+ expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, emailPostData);
+ });
+
+ it('calls Api addGroupMembersByUserId with the correct params', () => {
+ expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, idPostData);
+ });
+
+ it('displays the successful toastMessage', () => {
+ expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
+ });
});
- it('displays the successful toastMessage', () => {
- expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled();
+ it('calls Apis with the invite source passed through to openModal', () => {
+ wrapper.vm.openModal({ inviteeType: 'members', source: '_invite_source_' });
+
+ clickInviteButton();
+
+ expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, {
+ ...emailPostData,
+ invite_source: '_invite_source_',
+ });
+ expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, {
+ ...idPostData,
+ invite_source: '_invite_source_',
+ });
});
});
@@ -403,18 +426,11 @@ describe('InviteMembersModal', () => {
});
describe('tracking', () => {
- const postData = {
- user_id: '1',
- access_level: defaultAccessLevel,
- expires_at: undefined,
- format: 'json',
- };
-
beforeEach(() => {
wrapper = createComponent({ newUsersToInvite: [user3] });
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({});
});
it('tracks the invite', () => {
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index b569b6286e0..f57af61ad5b 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -7,6 +7,8 @@ import eventHub from '~/invite_members/event_hub';
jest.mock('~/experimentation/experiment_tracking');
const displayText = 'Invite team members';
+const triggerSource = '_trigger_source_';
+
let wrapper;
let triggerProps;
let findButton;
@@ -26,7 +28,7 @@ const createComponent = (props = {}) => {
};
describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement) => {
- triggerProps = { triggerElement };
+ triggerProps = { triggerElement, triggerSource };
findButton = () => wrapper.findComponent(triggerComponent[triggerElement]);
afterEach(() => {
@@ -48,22 +50,14 @@ describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement
spy = jest.spyOn(eventHub, '$emit');
});
- it('emits openModal from an unknown source', () => {
- createComponent();
-
- findButton().vm.$emit('click');
-
- expect(spy).toHaveBeenCalledWith('openModal', { inviteeType: 'members', source: 'unknown' });
- });
-
it('emits openModal from a named source', () => {
- createComponent({ triggerSource: '_trigger_source_' });
+ createComponent();
findButton().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('openModal', {
inviteeType: 'members',
- source: '_trigger_source_',
+ source: triggerSource,
});
});
});
diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js
index 7eb85a946ae..34094d22e68 100644
--- a/spec/frontend/issuable/components/csv_export_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_export_modal_spec.js
@@ -1,7 +1,6 @@
import { GlModal, GlIcon, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CsvExportModal from '~/issuable/components/csv_export_modal.vue';
describe('CsvExportModal', () => {
@@ -9,26 +8,24 @@ describe('CsvExportModal', () => {
function createComponent(options = {}) {
const { injectedProperties = {}, props = {} } = options;
- return extendedWrapper(
- mount(CsvExportModal, {
- propsData: {
- modalId: 'csv-export-modal',
- exportCsvPath: 'export/csv/path',
- issuableCount: 1,
- ...props,
- },
- provide: {
- issuableType: 'issues',
- ...injectedProperties,
- },
- stubs: {
- GlModal: stubComponent(GlModal, {
- template:
- '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
- }),
- },
- }),
- );
+ return mount(CsvExportModal, {
+ propsData: {
+ modalId: 'csv-export-modal',
+ exportCsvPath: 'export/csv/path',
+ issuableCount: 1,
+ ...props,
+ },
+ provide: {
+ issuableType: 'issues',
+ ...injectedProperties,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ },
+ });
}
afterEach(() => {
@@ -61,14 +58,13 @@ describe('CsvExportModal', () => {
describe('issuable count info text', () => {
it('displays the info text when issuableCount is > -1', () => {
wrapper = createComponent({ props: { issuableCount: 10 } });
- expect(wrapper.findByTestId('issuable-count-note').exists()).toBe(true);
- expect(wrapper.findByTestId('issuable-count-note').text()).toContain('10 issues selected');
+ expect(wrapper.text()).toContain('10 issues selected');
expect(findIcon().exists()).toBe(true);
});
it("doesn't display the info text when issuableCount is -1", () => {
wrapper = createComponent({ props: { issuableCount: -1 } });
- expect(wrapper.findByTestId('issuable-count-note').exists()).toBe(false);
+ expect(wrapper.text()).not.toContain('issues selected');
});
});
diff --git a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
index 2fe8d28a333..118c12d968b 100644
--- a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
+++ b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlDropdown } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import CsvExportModal from '~/issuable/components/csv_export_modal.vue';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import CsvImportModal from '~/issuable/components/csv_import_modal.vue';
@@ -14,35 +14,33 @@ describe('CsvImportExportButtons', () => {
function createComponent(injectedProperties = {}) {
glModalDirective = jest.fn();
- return extendedWrapper(
- shallowMount(CsvImportExportButtons, {
- directives: {
- GlTooltip: createMockDirective(),
- glModal: {
- bind(_, { value }) {
- glModalDirective(value);
- },
+ return mountExtended(CsvImportExportButtons, {
+ directives: {
+ GlTooltip: createMockDirective(),
+ glModal: {
+ bind(_, { value }) {
+ glModalDirective(value);
},
},
- provide: {
- ...injectedProperties,
- },
- propsData: {
- exportCsvPath,
- issuableCount,
- },
- }),
- );
+ },
+ provide: {
+ ...injectedProperties,
+ },
+ propsData: {
+ exportCsvPath,
+ issuableCount,
+ },
+ });
}
afterEach(() => {
wrapper.destroy();
});
- const findExportCsvButton = () => wrapper.findByTestId('export-csv-button');
- const findImportDropdown = () => wrapper.findByTestId('import-csv-dropdown');
- const findImportCsvButton = () => wrapper.findByTestId('import-csv-dropdown');
- const findImportFromJiraLink = () => wrapper.findByTestId('import-from-jira-link');
+ const findExportCsvButton = () => wrapper.findComponent(GlButton);
+ const findImportDropdown = () => wrapper.findComponent(GlDropdown);
+ const findImportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Import CSV' });
+ const findImportFromJiraLink = () => wrapper.findByRole('menuitem', { name: 'Import from Jira' });
const findExportCsvModal = () => wrapper.findComponent(CsvExportModal);
const findImportCsvModal = () => wrapper.findComponent(CsvImportModal);
@@ -97,7 +95,7 @@ describe('CsvImportExportButtons', () => {
expect(findImportDropdown().exists()).toBe(true);
});
- it('renders the import button', () => {
+ it('renders the import csv menu item', () => {
expect(findImportCsvButton().exists()).toBe(true);
});
@@ -106,8 +104,11 @@ describe('CsvImportExportButtons', () => {
wrapper = createComponent({ showImportButton: true, showLabel: false });
});
- it('does not have a button text', () => {
- expect(findImportCsvButton().props('text')).toBe(null);
+ it('hides button text', () => {
+ expect(findImportDropdown().props()).toMatchObject({
+ text: 'Import issues',
+ textSrOnly: true,
+ });
});
it('import button has a tooltip', () => {
@@ -124,7 +125,10 @@ describe('CsvImportExportButtons', () => {
});
it('displays a button text', () => {
- expect(findImportCsvButton().props('text')).toBe('Import issues');
+ expect(findImportDropdown().props()).toMatchObject({
+ text: 'Import issues',
+ textSrOnly: false,
+ });
});
it('import button has no tooltip', () => {
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index ce9d738f77b..0c88b6b1283 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -1,8 +1,6 @@
-import { GlModal } from '@gitlab/ui';
-import { getByRole, getByLabelText } from '@testing-library/dom';
-import { mount } from '@vue/test-utils';
+import { GlButton, GlModal } from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import CsvImportModal from '~/issuable/components/csv_import_modal.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -13,23 +11,21 @@ describe('CsvImportModal', () => {
function createComponent(options = {}) {
const { injectedProperties = {}, props = {} } = options;
- return extendedWrapper(
- mount(CsvImportModal, {
- propsData: {
- modalId: 'csv-import-modal',
- ...props,
- },
- provide: {
- issuableType: 'issues',
- ...injectedProperties,
- },
- stubs: {
- GlModal: stubComponent(GlModal, {
- template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
- }),
- },
- }),
- );
+ return mountExtended(CsvImportModal, {
+ propsData: {
+ modalId: 'csv-import-modal',
+ ...props,
+ },
+ provide: {
+ issuableType: 'issues',
+ ...injectedProperties,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ },
+ });
}
beforeEach(() => {
@@ -41,9 +37,9 @@ describe('CsvImportModal', () => {
});
const findModal = () => wrapper.findComponent(GlModal);
- const findPrimaryButton = () => getByRole(wrapper.element, 'button', { name: 'Import issues' });
- const findForm = () => wrapper.findByTestId('import-csv-form');
- const findFileInput = () => getByLabelText(wrapper.element, 'Upload CSV file');
+ const findPrimaryButton = () => wrapper.findComponent(GlButton);
+ const findForm = () => wrapper.find('form');
+ const findFileInput = () => wrapper.findByLabelText('Upload CSV file');
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
describe('template', () => {
@@ -76,8 +72,8 @@ describe('CsvImportModal', () => {
expect(findPrimaryButton()).toExist();
});
- it('submits the form when the primary action is clicked', async () => {
- findPrimaryButton().click();
+ it('submits the form when the primary action is clicked', () => {
+ findPrimaryButton().trigger('click');
expect(formSubmitSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
index 08a99f29479..f11c41fe25d 100644
--- a/spec/frontend/issuable/components/issuable_by_email_spec.js
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -58,10 +58,11 @@ describe('IssuableByEmail', () => {
mockAxios.restore();
});
- const findFormInputGroup = () => wrapper.find(GlFormInputGroup);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
const clickResetEmail = async () => {
- wrapper.findByTestId('incoming-email-token-reset').vm.$emit('click');
+ wrapper.findAllComponents(GlButton).at(2).trigger('click');
await waitForPromises();
};
@@ -75,14 +76,14 @@ describe('IssuableByEmail', () => {
'renders a link with "$buttonText" when type is "$issuableType"',
({ issuableType, buttonText }) => {
wrapper = createComponent({ issuableType });
- expect(wrapper.findByTestId('issuable-email-modal-btn').text()).toBe(buttonText);
+ expect(findButton().text()).toBe(buttonText);
},
);
it('opens the modal when the user clicks the button', () => {
wrapper = createComponent();
- wrapper.findByTestId('issuable-email-modal-btn').vm.$emit('click');
+ findButton().trigger('click');
expect(glModalDirective).toHaveBeenCalled();
});
@@ -105,7 +106,7 @@ describe('IssuableByEmail', () => {
initialEmail,
});
- expect(wrapper.findByTestId('mail-to-btn').attributes('href')).toBe(
+ expect(wrapper.findAllComponents(GlButton).at(1).attributes('href')).toBe(
`mailto:${initialEmail}?subject=${subject}&body=${body}`,
);
});
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
index 990fac67f7e..9cbf023dbd6 100644
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ b/spec/frontend/issuable/components/status_box_spec.js
@@ -1,4 +1,4 @@
-import { GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusBox from '~/issuable/components/status_box.vue';
@@ -64,7 +64,7 @@ describe('Merge request status box component', () => {
initialState: testCase.state,
});
- expect(wrapper.find('[data-testid="status-icon"]').props('name')).toBe(testCase.icon);
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe(testCase.icon);
});
});
});
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 e5e3478dc59..3099e0b639b 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
@@ -6,7 +6,7 @@ import {
issuable1,
issuable2,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import { linkedIssueTypesMap } from '~/related_issues/constants';
@@ -195,7 +195,9 @@ describe('RelatedIssuesRoot', () => {
wrapper.vm.onPendingFormSubmit(input);
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith(message);
+ expect(createFlash).toHaveBeenCalledWith({
+ message,
+ });
});
});
});
diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js
index e324f071966..ea36d59ff83 100644
--- a/spec/frontend/issuable_list/components/issuable_item_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_item_spec.js
@@ -336,7 +336,7 @@ describe('IssuableItem', () => {
const createdAtEl = wrapper.find('[data-testid="issuable-created-at"]');
expect(createdAtEl.exists()).toBe(true);
- expect(createdAtEl.attributes('title')).toBe('Jun 29, 2020 1:52pm GMT+0000');
+ expect(createdAtEl.attributes('title')).toBe('Jun 29, 2020 1:52pm UTC');
expect(createdAtEl.text()).toBe(wrapper.vm.createdAt);
});
@@ -450,7 +450,7 @@ describe('IssuableItem', () => {
const updatedAtEl = wrapper.find('[data-testid="issuable-updated-at"]');
expect(updatedAtEl.exists()).toBe(true);
- expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am GMT+0000');
+ expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am UTC');
expect(updatedAtEl.text()).toBe(wrapper.vm.updatedAt);
});
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index b8860e93a22..4c06f2dca1b 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -1,6 +1,7 @@
import { GlIntersectionObserver } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import '~/behaviors/markdown/render_gfm';
import IssuableApp from '~/issue_show/components/app.vue';
@@ -17,7 +18,7 @@ import {
publishedIncidentUrl,
secondRequest,
zoomMeetingUrl,
-} from '../mock_data';
+} from '../mock_data/mock_data';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
@@ -36,12 +37,11 @@ describe('Issuable output', () => {
let wrapper;
const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]');
-
const findLockedBadge = () => wrapper.find('[data-testid="locked"]');
-
const findConfidentialBadge = () => wrapper.find('[data-testid="confidential"]');
+ const findAlert = () => wrapper.find('.alert');
- const mountComponent = (props = {}, options = {}) => {
+ const mountComponent = (props = {}, options = {}, data = {}) => {
wrapper = mount(IssuableApp, {
propsData: { ...appProps, ...props },
provide: {
@@ -53,6 +53,11 @@ describe('Issuable output', () => {
HighlightBar: true,
IncidentTabs: true,
},
+ data() {
+ return {
+ ...data,
+ };
+ },
...options,
});
};
@@ -91,10 +96,8 @@ describe('Issuable output', () => {
afterEach(() => {
mock.restore();
realtimeRequestCount = 0;
-
wrapper.vm.poll.stop();
wrapper.destroy();
- wrapper = null;
});
it('should render a title/description/edited and update title/description/edited on update', () => {
@@ -115,7 +118,7 @@ describe('Issuable output', () => {
expect(formatText(editedText.text())).toMatch(/Edited[\s\S]+?by Some User/);
expect(editedText.find('.author-link').attributes('href')).toMatch(/\/some_user$/);
expect(editedText.find('time').text()).toBeTruthy();
- expect(wrapper.vm.state.lock_version).toEqual(1);
+ expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
})
.then(() => {
wrapper.vm.poll.makeRequest();
@@ -133,7 +136,9 @@ describe('Issuable output', () => {
expect(editedText.find('.author-link').attributes('href')).toMatch(/\/other_user$/);
expect(editedText.find('time').text()).toBeTruthy();
- expect(wrapper.vm.state.lock_version).toEqual(2);
+ // As the lock_version value does not differ from the server,
+ // we should not see an alert
+ expect(findAlert().exists()).toBe(false);
});
});
@@ -172,7 +177,7 @@ describe('Issuable output', () => {
${'zoomMeetingUrl'} | ${zoomMeetingUrl}
${'publishedIncidentUrl'} | ${publishedIncidentUrl}
`('sets the $prop correctly on underlying pinned links', ({ prop, value }) => {
- expect(wrapper.vm[prop]).toEqual(value);
+ expect(wrapper.vm[prop]).toBe(value);
expect(wrapper.find(`[data-testid="${prop}"]`).attributes('href')).toBe(value);
});
});
@@ -374,9 +379,9 @@ describe('Issuable output', () => {
});
})
.then(() => {
- expect(wrapper.vm.formState.lockedWarningVisible).toEqual(true);
- expect(wrapper.vm.formState.lock_version).toEqual(1);
- expect(wrapper.find('.alert').exists()).toBe(true);
+ expect(wrapper.vm.formState.lockedWarningVisible).toBe(true);
+ expect(wrapper.vm.formState.lock_version).toBe(1);
+ expect(findAlert().exists()).toBe(true);
});
});
});
@@ -530,7 +535,7 @@ describe('Issuable output', () => {
`('$title', async ({ state }) => {
wrapper.setProps({ issuableStatus: state });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findStickyHeader().text()).toContain(IssuableStatusText[state]);
});
@@ -542,7 +547,7 @@ describe('Issuable output', () => {
`('$title', async ({ isConfidential }) => {
wrapper.setProps({ isConfidential });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findConfidentialBadge().exists()).toBe(isConfidential);
});
@@ -554,7 +559,7 @@ describe('Issuable output', () => {
`('$title', async ({ isLocked }) => {
wrapper.setProps({ isLocked });
- await wrapper.vm.$nextTick();
+ await nextTick();
expect(findLockedBadge().exists()).toBe(isLocked);
});
@@ -562,9 +567,9 @@ describe('Issuable output', () => {
});
describe('Composable description component', () => {
- const findIncidentTabs = () => wrapper.find(IncidentTabs);
- const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
- const findPinnedLinks = () => wrapper.find(PinnedLinks);
+ const findIncidentTabs = () => wrapper.findComponent(IncidentTabs);
+ const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
+ const findPinnedLinks = () => wrapper.findComponent(PinnedLinks);
const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6';
describe('when using description component', () => {
diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js
index 70c04280675..cdf06ecc31f 100644
--- a/spec/frontend/issue_show/components/description_spec.js
+++ b/spec/frontend/issue_show/components/description_spec.js
@@ -5,7 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import mountComponent from 'helpers/vue_mount_component_helper';
import Description from '~/issue_show/components/description.vue';
import TaskList from '~/task_list';
-import { descriptionProps as props } from '../mock_data';
+import { descriptionProps as props } from '../mock_data/mock_data';
jest.mock('~/task_list');
diff --git a/spec/frontend/issue_show/components/edit_actions_spec.js b/spec/frontend/issue_show/components/edit_actions_spec.js
index 54707879f63..50c27cb5bda 100644
--- a/spec/frontend/issue_show/components/edit_actions_spec.js
+++ b/spec/frontend/issue_show/components/edit_actions_spec.js
@@ -1,113 +1,163 @@
-import Vue from 'vue';
-import editActions from '~/issue_show/components/edit_actions.vue';
+import { GlButton, GlModal } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IssuableEditActions from '~/issue_show/components/edit_actions.vue';
import eventHub from '~/issue_show/event_hub';
-import Store from '~/issue_show/stores';
-describe('Edit Actions components', () => {
- let vm;
+import {
+ getIssueStateQueryResponse,
+ updateIssueStateQueryResponse,
+} from '../mock_data/apollo_mock';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('Edit Actions component', () => {
+ let wrapper;
+ let fakeApollo;
+ let mockIssueStateData;
+
+ const mockResolvers = {
+ Query: {
+ issueState() {
+ return {
+ __typename: 'IssueState',
+ rawData: mockIssueStateData(),
+ };
+ },
+ },
+ };
- beforeEach((done) => {
- const Component = Vue.extend(editActions);
- const store = new Store({
- titleHtml: '',
- descriptionHtml: '',
- issuableRef: '',
- });
- store.formState.title = 'test';
+ const modalId = 'delete-issuable-modal-1';
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ const createComponent = ({ props, data } = {}) => {
+ fakeApollo = createMockApollo([], mockResolvers);
- vm = new Component({
+ wrapper = shallowMountExtended(IssuableEditActions, {
+ apolloProvider: fakeApollo,
propsData: {
+ formState: {
+ title: 'GitLab Issue',
+ },
canDestroy: true,
- formState: store.formState,
issuableType: 'issue',
+ ...props,
},
- }).$mount();
+ data() {
+ return {
+ issueState: {},
+ modalId,
+ ...data,
+ };
+ },
+ });
+ };
- Vue.nextTick(done);
- });
+ async function deleteIssuable(localWrapper) {
+ localWrapper.findComponent(GlModal).vm.$emit('primary');
+ }
- it('renders all buttons as enabled', () => {
- expect(vm.$el.querySelectorAll('.disabled').length).toBe(0);
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findEditButtons = () => wrapper.findAllComponents(GlButton);
+ const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button');
+ const findSaveButton = () => wrapper.findByTestId('issuable-save-button');
+ const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button');
- expect(vm.$el.querySelectorAll('[disabled]').length).toBe(0);
+ beforeEach(() => {
+ mockIssueStateData = jest.fn();
+ createComponent();
});
- it('does not render delete button if canUpdate is false', (done) => {
- vm.canDestroy = false;
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-danger')).toBeNull();
+ afterEach(() => {
+ wrapper.destroy();
+ });
- done();
+ it('renders all buttons as enabled', () => {
+ const buttons = findEditButtons().wrappers;
+ buttons.forEach((button) => {
+ expect(button.attributes('disabled')).toBeFalsy();
});
});
- it('disables submit button when title is blank', (done) => {
- vm.formState.title = '';
+ it('does not render the delete button if canDestroy is false', () => {
+ createComponent({ props: { canDestroy: false } });
+ expect(findDeleteButton().exists()).toBe(false);
+ });
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-confirm').getAttribute('disabled')).toBe('disabled');
+ it('disables save button when title is blank', () => {
+ createComponent({ props: { formState: { title: '', issue_type: '' } } });
- done();
- });
+ expect(findSaveButton().attributes('disabled')).toBe('true');
});
- it('should not show delete button if showDeleteButton is false', (done) => {
- vm.showDeleteButton = false;
+ it('does not render the delete button if showDeleteButton is false', () => {
+ createComponent({ props: { showDeleteButton: false } });
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-danger')).toBeNull();
- done();
- });
+ expect(findDeleteButton().exists()).toBe(false);
});
describe('updateIssuable', () => {
- it('sends update.issauble event when clicking save button', () => {
- vm.$el.querySelector('.btn-confirm').click();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
- it('disabled button after clicking save button', (done) => {
- vm.$el.querySelector('.btn-confirm').click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-confirm').getAttribute('disabled')).toBe('disabled');
+ it('sends update.issauble event when clicking save button', () => {
+ findSaveButton().vm.$emit('click', { preventDefault: jest.fn() });
- done();
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
});
describe('closeForm', () => {
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ });
+
it('emits close.form when clicking cancel', () => {
- vm.$el.querySelector('.btn-default').click();
+ findCancelButton().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
});
});
- describe('deleteIssuable', () => {
- it('sends delete.issuable event when clicking save button', () => {
- jest.spyOn(window, 'confirm').mockReturnValue(true);
- vm.$el.querySelector('.btn-danger').click();
+ describe('renders create modal with the correct information', () => {
+ it('renders correct modal id', () => {
+ expect(findModal().attributes('modalid')).toBe(modalId);
+ });
+ });
- expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true });
+ describe('deleteIssuable', () => {
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
- it('does no actions when confirm is false', (done) => {
- jest.spyOn(window, 'confirm').mockReturnValue(false);
- vm.$el.querySelector('.btn-danger').click();
+ it('does not send the `delete.issuable` event when clicking delete button', () => {
+ findDeleteButton().vm.$emit('click');
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ });
- Vue.nextTick(() => {
- expect(eventHub.$emit).not.toHaveBeenCalledWith('delete.issuable');
+ it('sends the `delete.issuable` event when clicking the delete confirm button', async () => {
+ expect(eventHub.$emit).toHaveBeenCalledTimes(0);
+ await deleteIssuable(wrapper);
+ expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true });
+ expect(eventHub.$emit).toHaveBeenCalledTimes(1);
+ });
+ });
- expect(vm.$el.querySelector('.btn-danger .fa')).toBeNull();
+ describe('with Apollo cache mock', () => {
+ it('renders the right delete button text per apollo cache type', async () => {
+ mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
+ await waitForPromises();
+ expect(findDeleteButton().text()).toBe('Delete issue');
+ });
- done();
- });
+ it('should not change the delete button text per apollo cache mutation', async () => {
+ mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse);
+ await waitForPromises();
+ expect(findDeleteButton().text()).toBe('Delete issue');
});
});
});
diff --git a/spec/frontend/issue_show/components/fields/type_spec.js b/spec/frontend/issue_show/components/fields/type_spec.js
new file mode 100644
index 00000000000..0c8af60d50d
--- /dev/null
+++ b/spec/frontend/issue_show/components/fields/type_spec.js
@@ -0,0 +1,84 @@
+import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IssueTypeField, { i18n } from '~/issue_show/components/fields/type.vue';
+import { IssuableTypes } from '~/issue_show/constants';
+import {
+ getIssueStateQueryResponse,
+ updateIssueStateQueryResponse,
+} from '../../mock_data/apollo_mock';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('Issue type field component', () => {
+ let wrapper;
+ let fakeApollo;
+ let mockIssueStateData;
+
+ const mockResolvers = {
+ Query: {
+ issueState() {
+ return {
+ __typename: 'IssueState',
+ rawData: mockIssueStateData(),
+ };
+ },
+ },
+ Mutation: {
+ updateIssueState: jest.fn().mockResolvedValue(updateIssueStateQueryResponse),
+ },
+ };
+
+ const findTypeFromGroup = () => wrapper.findComponent(GlFormGroup);
+ const findTypeFromDropDown = () => wrapper.findComponent(GlDropdown);
+ const findTypeFromDropDownItems = () => wrapper.findAllComponents(GlDropdownItem);
+
+ const createComponent = ({ data } = {}) => {
+ fakeApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMount(IssueTypeField, {
+ localVue,
+ apolloProvider: fakeApollo,
+ data() {
+ return {
+ issueState: {},
+ ...data,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockIssueStateData = jest.fn();
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a form group with the correct label', () => {
+ expect(findTypeFromGroup().attributes('label')).toBe(i18n.label);
+ });
+
+ it('renders a form select with the `issue_type` value', () => {
+ expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ });
+
+ describe('with Apollo cache mock', () => {
+ it('renders the selected issueType', async () => {
+ mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
+ await waitForPromises();
+ expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.issue);
+ });
+
+ it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
+ findTypeFromDropDownItems().at(1).vm.$emit('click', IssuableTypes.incident);
+ await wrapper.vm.$nextTick();
+ expect(findTypeFromDropDown().attributes('value')).toBe(IssuableTypes.incident);
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js
index 6d4807c4261..28498cb90ec 100644
--- a/spec/frontend/issue_show/components/form_spec.js
+++ b/spec/frontend/issue_show/components/form_spec.js
@@ -2,6 +2,7 @@ import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Autosave from '~/autosave';
import DescriptionTemplate from '~/issue_show/components/fields/description_template.vue';
+import IssueTypeField from '~/issue_show/components/fields/type.vue';
import formComponent from '~/issue_show/components/form.vue';
import LockedWarning from '~/issue_show/components/locked_warning.vue';
import eventHub from '~/issue_show/event_hub';
@@ -39,6 +40,7 @@ describe('Inline edit form component', () => {
};
const findDescriptionTemplate = () => wrapper.findComponent(DescriptionTemplate);
+ const findIssuableTypeField = () => wrapper.findComponent(IssueTypeField);
const findLockedWarning = () => wrapper.findComponent(LockedWarning);
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -68,6 +70,21 @@ describe('Inline edit form component', () => {
expect(findDescriptionTemplate().exists()).toBe(true);
});
+ it.each`
+ issuableType | value
+ ${'issue'} | ${true}
+ ${'epic'} | ${false}
+ `(
+ 'when `issue_type` is set to "$issuableType" rendering the type select will be "$value"',
+ ({ issuableType, value }) => {
+ createComponent({
+ issuableType,
+ });
+
+ expect(findIssuableTypeField().exists()).toBe(value);
+ },
+ );
+
it('hides locked warning by default', () => {
createComponent();
diff --git a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
index f46b6ba6f54..6b9f5b17e99 100644
--- a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
@@ -9,7 +9,7 @@ import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
import INVALID_URL from '~/lib/utils/invalid_url';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
-import { descriptionProps } from '../../mock_data';
+import { descriptionProps } from '../../mock_data/mock_data';
const mockAlert = {
__typename: 'AlertManagementAlert',
diff --git a/spec/frontend/issue_show/issue_spec.js b/spec/frontend/issue_show/issue_spec.js
index 9cb7059dd7f..d043693b863 100644
--- a/spec/frontend/issue_show/issue_spec.js
+++ b/spec/frontend/issue_show/issue_spec.js
@@ -5,7 +5,7 @@ import { initIssuableApp } from '~/issue_show/issue';
import * as parseData from '~/issue_show/utils/parse_data';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
-import { appProps } from './mock_data';
+import { appProps } from './mock_data/mock_data';
const mock = new MockAdapter(axios);
mock.onGet().reply(200);
diff --git a/spec/frontend/issue_show/mock_data/apollo_mock.js b/spec/frontend/issue_show/mock_data/apollo_mock.js
new file mode 100644
index 00000000000..bfd31e74393
--- /dev/null
+++ b/spec/frontend/issue_show/mock_data/apollo_mock.js
@@ -0,0 +1,9 @@
+export const getIssueStateQueryResponse = {
+ issueType: 'issue',
+ isDirty: false,
+};
+
+export const updateIssueStateQueryResponse = {
+ issueType: 'incident',
+ isDirty: true,
+};
diff --git a/spec/frontend/issue_show/mock_data.js b/spec/frontend/issue_show/mock_data/mock_data.js
index fd08c95b454..a73826954c3 100644
--- a/spec/frontend/issue_show/mock_data.js
+++ b/spec/frontend/issue_show/mock_data/mock_data.js
@@ -48,6 +48,7 @@ export const appProps = {
initialDescriptionHtml: 'test',
initialDescriptionText: 'test',
lockVersion: 1,
+ issueType: 'issue',
markdownPreviewPath: '/',
markdownDocsPath: '/',
projectNamespace: '/',
diff --git a/spec/frontend/issues_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js
index fe3d2114463..a7f3dd81517 100644
--- a/spec/frontend/issues_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issuables_list_app_spec.js
@@ -302,7 +302,6 @@ describe('Issuables list component', () => {
my_reaction_emoji: 'airplane',
scope: 'all',
state: 'opened',
- utf8: '✓',
weight: '0',
milestone: 'v3.0',
labels: 'Aquapod,Astro',
@@ -312,7 +311,7 @@ describe('Issuables list component', () => {
describe('when page is not present in params', () => {
const query =
- '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&utf8=%E2%9C%93&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13';
+ '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13';
beforeEach(() => {
setUrl(query);
@@ -356,7 +355,7 @@ describe('Issuables list component', () => {
describe('when page is present in the param', () => {
const query =
- '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&utf8=%E2%9C%93&weight=0&page=3';
+ '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&page=3';
beforeEach(() => {
setUrl(query);
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 5d83bf0142f..d78a436c618 100644
--- a/spec/frontend/issues_list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues_list/components/issues_list_app_spec.js
@@ -18,6 +18,15 @@ import {
PAGE_SIZE_MANUAL,
PARAM_DUE_DATE,
RELATIVE_POSITION_DESC,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_EPIC,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_WEIGHT,
urlSortParams,
} from '~/issues_list/constants';
import eventHub from '~/issues_list/eventhub';
@@ -39,8 +48,8 @@ describe('IssuesListApp component', () => {
endpoint: 'api/endpoint',
exportCsvPath: 'export/csv/path',
hasBlockedIssuesFeature: true,
- hasIssues: true,
hasIssueWeightsFeature: true,
+ hasProjectIssues: true,
isSignedIn: false,
issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path',
@@ -320,7 +329,7 @@ describe('IssuesListApp component', () => {
beforeEach(async () => {
global.jsdom.reconfigure({ url: `${TEST_HOST}?search=no+results` });
- wrapper = mountComponent({ provide: { hasIssues: true }, mountFn: mount });
+ wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
});
@@ -336,7 +345,7 @@ describe('IssuesListApp component', () => {
describe('when "Open" tab has no issues', () => {
beforeEach(async () => {
- wrapper = mountComponent({ provide: { hasIssues: true }, mountFn: mount });
+ wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
});
@@ -356,7 +365,7 @@ describe('IssuesListApp component', () => {
url: setUrlParams({ state: IssuableStates.Closed }, TEST_HOST),
});
- wrapper = mountComponent({ provide: { hasIssues: true }, mountFn: mount });
+ wrapper = mountComponent({ provide: { hasProjectIssues: true }, mountFn: mount });
await waitForPromises();
});
@@ -374,7 +383,7 @@ describe('IssuesListApp component', () => {
describe('when user is logged in', () => {
beforeEach(() => {
wrapper = mountComponent({
- provide: { hasIssues: false, isSignedIn: true },
+ provide: { hasProjectIssues: false, isSignedIn: true },
mountFn: mount,
});
});
@@ -413,7 +422,7 @@ describe('IssuesListApp component', () => {
describe('when user is logged out', () => {
beforeEach(() => {
wrapper = mountComponent({
- provide: { hasIssues: false, isSignedIn: false },
+ provide: { hasProjectIssues: false, isSignedIn: false },
});
});
@@ -430,6 +439,119 @@ describe('IssuesListApp component', () => {
});
});
+ describe('tokens', () => {
+ const mockCurrentUser = {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'avatar/url',
+ };
+
+ describe('when user is signed out', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: {
+ isSignedIn: false,
+ },
+ });
+ });
+
+ it('does not render My-Reaction or Confidential tokens', () => {
+ expect(findIssuableList().props('searchTokens')).not.toMatchObject([
+ { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_MY_REACTION },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
+ ]);
+ });
+ });
+
+ describe('when iterations are not available', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: {
+ projectIterationsPath: '',
+ },
+ });
+ });
+
+ it('does not render Iteration token', () => {
+ expect(findIssuableList().props('searchTokens')).not.toMatchObject([
+ { type: TOKEN_TYPE_ITERATION },
+ ]);
+ });
+ });
+
+ describe('when epics are not available', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: {
+ groupEpicsPath: '',
+ },
+ });
+ });
+
+ it('does not render Epic token', () => {
+ expect(findIssuableList().props('searchTokens')).not.toMatchObject([
+ { type: TOKEN_TYPE_EPIC },
+ ]);
+ });
+ });
+
+ describe('when weights are not available', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ provide: {
+ groupEpicsPath: '',
+ },
+ });
+ });
+
+ it('does not render Weight token', () => {
+ expect(findIssuableList().props('searchTokens')).not.toMatchObject([
+ { type: TOKEN_TYPE_WEIGHT },
+ ]);
+ });
+ });
+
+ describe('when all tokens are available', () => {
+ const originalGon = window.gon;
+
+ beforeEach(() => {
+ window.gon = {
+ ...originalGon,
+ current_user_id: mockCurrentUser.id,
+ current_user_fullname: mockCurrentUser.name,
+ current_username: mockCurrentUser.username,
+ current_user_avatar_url: mockCurrentUser.avatar_url,
+ };
+
+ wrapper = mountComponent({
+ provide: {
+ isSignedIn: true,
+ projectIterationsPath: 'project/iterations/path',
+ groupEpicsPath: 'group/epics/path',
+ hasIssueWeightsFeature: true,
+ },
+ });
+ });
+
+ it('renders all tokens', () => {
+ expect(findIssuableList().props('searchTokens')).toMatchObject([
+ { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
+ { type: TOKEN_TYPE_MILESTONE },
+ { type: TOKEN_TYPE_LABEL },
+ { type: TOKEN_TYPE_MY_REACTION },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
+ { type: TOKEN_TYPE_ITERATION },
+ { type: TOKEN_TYPE_EPIC },
+ { type: TOKEN_TYPE_WEIGHT },
+ ]);
+ });
+ });
+ });
+
describe('events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => {
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index ce2880d177a..99267fb6e31 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -21,8 +21,8 @@ export const locationSearch = [
'confidential=no',
'iteration_title=season:+%234',
'not[iteration_title]=season:+%2320',
- 'epic_id=12',
- 'not[epic_id]=34',
+ 'epic_id=gitlab-org%3A%3A%2612',
+ 'not[epic_id]=gitlab-org%3A%3A%2634',
'weight=1',
'not[weight]=3',
].join('&');
@@ -53,8 +53,8 @@ export const filteredTokens = [
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
{ type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
- { type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
- { type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
+ { type: 'epic_id', value: { data: 'gitlab-org::&12', operator: OPERATOR_IS } },
+ { type: 'epic_id', value: { data: 'gitlab-org::&34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
{ type: 'filtered-search-term', value: { data: 'find' } },
@@ -84,7 +84,7 @@ export const apiParams = {
iteration_title: 'season: #4',
'not[iteration_title]': 'season: #20',
epic_id: '12',
- 'not[epic_id]': '34',
+ 'not[epic_id]': 'gitlab-org::&34',
weight: '1',
'not[weight]': '3',
};
@@ -111,8 +111,8 @@ export const urlParams = {
confidential: 'no',
iteration_title: 'season: #4',
'not[iteration_title]': 'season: #20',
- epic_id: '12',
- 'not[epic_id]': '34',
+ epic_id: 'gitlab-org%3A%3A%2612',
+ 'not[epic_id]': 'gitlab-org::&34',
weight: '1',
'not[weight]': '3',
};
diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues_list/utils_spec.js
index 17127753972..e377c35a0aa 100644
--- a/spec/frontend/issues_list/utils_spec.js
+++ b/spec/frontend/issues_list/utils_spec.js
@@ -82,7 +82,10 @@ describe('getFilterTokens', () => {
describe('convertToParams', () => {
it('returns api params given filtered tokens', () => {
- expect(convertToParams(filteredTokens, API_PARAM)).toEqual(apiParams);
+ expect(convertToParams(filteredTokens, API_PARAM)).toEqual({
+ ...apiParams,
+ epic_id: 'gitlab-org::&12',
+ });
});
it('returns api params given filtered tokens with special values', () => {
@@ -92,7 +95,10 @@ describe('convertToParams', () => {
});
it('returns url params given filtered tokens', () => {
- expect(convertToParams(filteredTokens, URL_PARAM)).toEqual(urlParams);
+ expect(convertToParams(filteredTokens, URL_PARAM)).toEqual({
+ ...urlParams,
+ epic_id: 'gitlab-org::&12',
+ });
});
it('returns url params given filtered tokens with special values', () => {
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index 9f49cb4007a..172b6e4831c 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -73,6 +73,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
aria-label="Will be mapped to"
class="gl-icon s16"
data-testid="arrow-right-icon"
+ role="img"
>
<use
href="#arrow-right"
@@ -109,6 +110,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
aria-hidden="true"
class="gl-button-icon dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
+ role="img"
>
<use
href="#chevron-down"
@@ -135,6 +137,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
+ role="img"
>
<use
href="#search"
@@ -198,6 +201,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
aria-label="Will be mapped to"
class="gl-icon s16"
data-testid="arrow-right-icon"
+ role="img"
>
<use
href="#arrow-right"
@@ -234,6 +238,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
aria-hidden="true"
class="gl-button-icon dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
+ role="img"
>
<use
href="#chevron-down"
@@ -260,6 +265,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
+ role="img"
>
<use
href="#search"
diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js
index b56230e94fc..04b2a2da622 100644
--- a/spec/frontend/jira_import/components/jira_import_progress_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js
@@ -64,7 +64,7 @@ describe('JiraImportProgress', () => {
});
it('shows the time of import', () => {
- expect(getParagraphText()).toContain('Time of import: Apr 8, 2020 12:17pm GMT+0000');
+ expect(getParagraphText()).toContain('Time of import: Apr 8, 2020 12:17pm UTC');
});
it('shows the project key of the import', () => {
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 9d1135e26c8..482d0df4e9a 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoader, GlAlert, GlEmptyState } from '@gitlab/ui';
+import { GlSkeletonLoader, GlAlert, GlEmptyState, GlPagination } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -25,6 +25,10 @@ describe('Job table app', () => {
const findTabs = () => wrapper.findComponent(JobsTableTabs);
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ const findPrevious = () => findPagination().findAll('.page-item').at(0);
+ const findNext = () => findPagination().findAll('.page-item').at(1);
const createMockApolloProvider = (handler) => {
const requestHandlers = [[getJobsQuery, handler]];
@@ -32,8 +36,17 @@ describe('Job table app', () => {
return createMockApollo(requestHandlers);
};
- const createComponent = (handler = successHandler, mountFn = shallowMount) => {
+ const createComponent = ({
+ handler = successHandler,
+ mountFn = shallowMount,
+ data = {},
+ } = {}) => {
wrapper = mountFn(JobsTableApp, {
+ data() {
+ return {
+ ...data,
+ };
+ },
provide: {
projectPath,
},
@@ -52,6 +65,7 @@ describe('Job table app', () => {
expect(findSkeletonLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false);
+ expect(findPagination().exists()).toBe(false);
});
});
@@ -65,9 +79,10 @@ describe('Job table app', () => {
it('should display the jobs table with data', () => {
expect(findTable().exists()).toBe(true);
expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findPagination().exists()).toBe(true);
});
- it('should retfech jobs query on fetchJobsByStatus event', async () => {
+ it('should refetch jobs query on fetchJobsByStatus event', async () => {
jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
@@ -78,9 +93,72 @@ describe('Job table app', () => {
});
});
+ describe('pagination', () => {
+ it('should disable the next page button on the last page', async () => {
+ createComponent({
+ handler: successHandler,
+ mountFn: mount,
+ data: {
+ pagination: {
+ currentPage: 3,
+ },
+ jobs: {
+ pageInfo: {
+ hasPreviousPage: true,
+ startCursor: 'abc',
+ endCursor: 'bcd',
+ },
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.setData({
+ jobs: {
+ pageInfo: {
+ hasNextPage: false,
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findPrevious().exists()).toBe(true);
+ expect(findNext().exists()).toBe(true);
+ expect(findNext().classes('disabled')).toBe(true);
+ });
+
+ it('should disable the previous page button on the first page', async () => {
+ createComponent({
+ handler: successHandler,
+ mountFn: mount,
+ data: {
+ pagination: {
+ currentPage: 1,
+ },
+ jobs: {
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'abc',
+ endCursor: 'bcd',
+ },
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findPrevious().exists()).toBe(true);
+ expect(findPrevious().classes('disabled')).toBe(true);
+ expect(findNext().exists()).toBe(true);
+ });
+ });
+
describe('error state', () => {
it('should show an alert if there is an error fetching the data', async () => {
- createComponent(failedHandler);
+ createComponent({ handler: failedHandler });
await waitForPromises();
@@ -90,7 +168,7 @@ describe('Job table app', () => {
describe('empty state', () => {
it('should display empty state if there are no jobs and tab scope is null', async () => {
- createComponent(emptyHandler, mount);
+ createComponent({ handler: emptyHandler, mountFn: mount });
await waitForPromises();
@@ -99,7 +177,7 @@ describe('Job table app', () => {
});
it('should not display empty state if there are jobs and tab scope is not null', async () => {
- createComponent(successHandler, mount);
+ createComponent({ handler: successHandler, mountFn: mount });
await waitForPromises();
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 6180cd8e94d..df0ccb19cb7 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -101,13 +101,13 @@ describe('Date time utils', () => {
it('should format date properly', () => {
const formattedDate = datetimeUtility.formatDate(new Date('07/23/2016'));
- expect(formattedDate).toBe('Jul 23, 2016 12:00am GMT+0000');
+ expect(formattedDate).toBe('Jul 23, 2016 12:00am UTC');
});
it('should format ISO date properly', () => {
const formattedDate = datetimeUtility.formatDate('2016-07-23T00:00:00.559Z');
- expect(formattedDate).toBe('Jul 23, 2016 12:00am GMT+0000');
+ expect(formattedDate).toBe('Jul 23, 2016 12:00am UTC');
});
it('should throw an error if date is invalid', () => {
@@ -878,7 +878,7 @@ describe('localTimeAgo', () => {
it.each`
timeagoArg | title
${false} | ${'some time'}
- ${true} | ${'Feb 18, 2020 10:22pm GMT+0000'}
+ ${true} | ${'Feb 18, 2020 10:22pm UTC'}
`('converts $seconds seconds to $approximation', ({ timeagoArg, title }) => {
const element = document.querySelector('time');
datetimeUtility.localTimeAgo($(element), timeagoArg);
@@ -889,17 +889,6 @@ describe('localTimeAgo', () => {
});
});
-describe('dateFromParams', () => {
- it('returns the expected date object', () => {
- const expectedDate = new Date('2019-07-17T00:00:00.000Z');
- const date = datetimeUtility.dateFromParams(2019, 6, 17);
-
- expect(date.getYear()).toBe(expectedDate.getYear());
- expect(date.getMonth()).toBe(expectedDate.getMonth());
- expect(date.getDate()).toBe(expectedDate.getDate());
- });
-});
-
describe('differenceInSeconds', () => {
const startDateTime = new Date('2019-07-17T00:00:00.000Z');
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index f4483f5098b..e743678ea90 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -80,18 +80,22 @@ describe('Number Utils', () => {
describe('numberToHumanSize', () => {
it('should return bytes', () => {
expect(numberToHumanSize(654)).toEqual('654 bytes');
+ expect(numberToHumanSize(-654)).toEqual('-654 bytes');
});
it('should return KiB', () => {
expect(numberToHumanSize(1079)).toEqual('1.05 KiB');
+ expect(numberToHumanSize(-1079)).toEqual('-1.05 KiB');
});
it('should return MiB', () => {
expect(numberToHumanSize(10485764)).toEqual('10.00 MiB');
+ expect(numberToHumanSize(-10485764)).toEqual('-10.00 MiB');
});
it('should return GiB', () => {
expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB');
+ expect(numberToHumanSize(-10737418240)).toEqual('-10.00 GiB');
});
});
diff --git a/spec/frontend/lib/utils/table_utility_spec.js b/spec/frontend/lib/utils/table_utility_spec.js
new file mode 100644
index 00000000000..75b9252aa40
--- /dev/null
+++ b/spec/frontend/lib/utils/table_utility_spec.js
@@ -0,0 +1,11 @@
+import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
+import * as tableUtils from '~/lib/utils/table_utility';
+
+describe('table_utility', () => {
+ describe('thWidthClass', () => {
+ it('returns the width class including default table header classes', () => {
+ const width = 50;
+ expect(tableUtils.thWidthClass(width)).toBe(`gl-w-${width}p ${DEFAULT_TH_CLASSES}`);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index e12cd8b0e37..305d3de3c53 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -471,6 +471,7 @@ describe('URL utility', () => {
${'notaurl'} | ${false}
${'../relative_url'} | ${false}
${'<a></a>'} | ${false}
+ ${'//other-host.test'} | ${false}
`('returns $valid for $url', ({ url, valid }) => {
expect(urlUtils.isRootRelative(url)).toBe(valid);
});
@@ -650,45 +651,24 @@ describe('URL utility', () => {
});
describe('queryToObject', () => {
- it('converts search query into an object', () => {
- const searchQuery = '?one=1&two=2';
-
- expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' });
- });
-
- it('removes undefined values from the search query', () => {
- const searchQuery = '?one=1&two=2&three';
-
- expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' });
- });
-
- describe('with gatherArrays=false', () => {
- it('overwrites values with the same array-key and does not change the key', () => {
- const searchQuery = '?one[]=1&one[]=2&two=2&two=3';
-
- expect(urlUtils.queryToObject(searchQuery)).toEqual({ 'one[]': '2', two: '3' });
- });
- });
-
- describe('with gatherArrays=true', () => {
- const options = { gatherArrays: true };
- it('gathers only values with the same array-key and strips `[]` from the key', () => {
- const searchQuery = '?one[]=1&one[]=2&two=2&two=3';
-
- expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: ['1', '2'], two: '3' });
- });
-
- it('overwrites values with the same array-key name', () => {
- const searchQuery = '?one=1&one[]=2&two=2&two=3';
-
- expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: ['2'], two: '3' });
- });
-
- it('overwrites values with the same key name', () => {
- const searchQuery = '?one[]=1&one=2&two=2&two=3';
-
- expect(urlUtils.queryToObject(searchQuery, options)).toEqual({ one: '2', two: '3' });
- });
+ it.each`
+ case | query | options | result
+ ${'converts query'} | ${'?one=1&two=2'} | ${undefined} | ${{ one: '1', two: '2' }}
+ ${'converts query without ?'} | ${'one=1&two=2'} | ${undefined} | ${{ one: '1', two: '2' }}
+ ${'removes undefined values'} | ${'?one=1&two=2&three'} | ${undefined} | ${{ one: '1', two: '2' }}
+ ${'overwrites values with same key and does not change key'} | ${'?one[]=1&one[]=2&two=2&two=3'} | ${undefined} | ${{ 'one[]': '2', two: '3' }}
+ ${'gathers values with the same array-key, strips `[]` from key'} | ${'?one[]=1&one[]=2&two=2&two=3'} | ${{ gatherArrays: true }} | ${{ one: ['1', '2'], two: '3' }}
+ ${'overwrites values with the same array-key name'} | ${'?one=1&one[]=2&two=2&two=3'} | ${{ gatherArrays: true }} | ${{ one: ['2'], two: '3' }}
+ ${'overwrites values with the same key name'} | ${'?one[]=1&one=2&two=2&two=3'} | ${{ gatherArrays: true }} | ${{ one: '2', two: '3' }}
+ ${'ignores plus symbols'} | ${'?search=a+b'} | ${{ legacySpacesDecode: true }} | ${{ search: 'a+b' }}
+ ${'ignores plus symbols in keys'} | ${'?search+term=a'} | ${{ legacySpacesDecode: true }} | ${{ 'search+term': 'a' }}
+ ${'ignores plus symbols when gathering arrays'} | ${'?search[]=a+b'} | ${{ gatherArrays: true, legacySpacesDecode: true }} | ${{ search: ['a+b'] }}
+ ${'replaces plus symbols with spaces'} | ${'?search=a+b'} | ${undefined} | ${{ search: 'a b' }}
+ ${'replaces plus symbols in keys with spaces'} | ${'?search+term=a'} | ${undefined} | ${{ 'search term': 'a' }}
+ ${'replaces plus symbols when gathering arrays'} | ${'?search[]=a+b'} | ${{ gatherArrays: true }} | ${{ search: ['a b'] }}
+ ${'replaces plus symbols when gathering arrays for values with same key'} | ${'?search[]=a+b&search[]=c+d'} | ${{ gatherArrays: true }} | ${{ search: ['a b', 'c d'] }}
+ `('$case', ({ query, options, result }) => {
+ expect(urlUtils.queryToObject(query, options)).toEqual(result);
});
});
@@ -798,15 +778,29 @@ describe('URL utility', () => {
);
});
- it('handles arrays properly', () => {
+ it('adds parameters from arrays', () => {
const url = 'https://gitlab.com/test';
- expect(urlUtils.setUrlParams({ label_name: ['foo', 'bar'] }, url)).toEqual(
- 'https://gitlab.com/test?label_name=foo&label_name=bar',
+ expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url)).toEqual(
+ 'https://gitlab.com/test?labels=foo&labels=bar',
);
});
- it('handles arrays properly when railsArraySyntax=true', () => {
+ it('removes parameters from empty arrays', () => {
+ const url = 'https://gitlab.com/test?labels=foo&labels=bar';
+
+ expect(urlUtils.setUrlParams({ labels: [] }, url)).toEqual('https://gitlab.com/test');
+ });
+
+ it('removes parameters from empty arrays while keeping other parameters', () => {
+ const url = 'https://gitlab.com/test?labels=foo&labels=bar&unrelated=unrelated';
+
+ expect(urlUtils.setUrlParams({ labels: [] }, url)).toEqual(
+ 'https://gitlab.com/test?unrelated=unrelated',
+ );
+ });
+
+ it('adds parameters from arrays when railsArraySyntax=true', () => {
const url = 'https://gitlab.com/test';
expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true)).toEqual(
@@ -814,6 +808,14 @@ describe('URL utility', () => {
);
});
+ it('removes parameters from empty arrays when railsArraySyntax=true', () => {
+ const url = 'https://gitlab.com/test?labels%5B%5D=foo&labels%5B%5D=bar';
+
+ expect(urlUtils.setUrlParams({ labels: [] }, url, false, true)).toEqual(
+ 'https://gitlab.com/test',
+ );
+ });
+
it('decodes URI when decodeURI=true', () => {
const url = 'https://gitlab.com/test';
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index b40d9d7d5e2..b107708ac2c 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -12,7 +12,6 @@ import {
mockTrace,
mockEnvironmentsEndpoint,
mockDocumentationPath,
- mockManagedAppsEndpoint,
} from '../mock_data';
jest.mock('~/lib/utils/scroll_utils');
@@ -35,7 +34,7 @@ describe('EnvironmentLogs', () => {
environmentName: mockEnvName,
environmentsPath: mockEnvironmentsEndpoint,
clusterApplicationsDocumentationPath: mockDocumentationPath,
- clustersPath: mockManagedAppsEndpoint,
+ clustersPath: '/gitlab-org',
};
const updateControlBtnsMock = jest.fn();
diff --git a/spec/frontend/logs/mock_data.js b/spec/frontend/logs/mock_data.js
index 3fabab4bc59..14c8f7a2ba2 100644
--- a/spec/frontend/logs/mock_data.js
+++ b/spec/frontend/logs/mock_data.js
@@ -7,8 +7,6 @@ export const mockDocumentationPath = '/documentation.md';
export const mockLogsEndpoint = '/dummy_logs_path.json';
export const mockCursor = 'MOCK_CURSOR';
export const mockNextCursor = 'MOCK_NEXT_CURSOR';
-export const mockManagedAppName = 'kubernetes-cluster-1';
-export const mockManagedAppsEndpoint = `${mockProjectPath}/clusters.json`;
const makeMockEnvironment = (id, name, advancedQuerying) => ({
id,
@@ -25,31 +23,6 @@ export const mockEnvironments = [
makeMockEnvironment(102, 'review/a-feature', false),
];
-export const mockManagedApps = [
- {
- cluster_type: 'project_type',
- enabled: true,
- environment_scope: '*',
- name: 'kubernetes-cluster-1',
- provider_type: 'user',
- status: 'connected',
- path: '/root/autodevops-deploy/-/clusters/15',
- gitlab_managed_apps_logs_path: '/root/autodevops-deploy/-/logs?cluster_id=15',
- enable_advanced_logs_querying: true,
- },
- {
- cluster_type: 'project_type',
- enabled: true,
- environment_scope: '*',
- name: 'kubernetes-cluster-2',
- provider_type: 'user',
- status: 'connected',
- path: '/root/autodevops-deploy/-/clusters/16',
- gitlab_managed_apps_logs_path: null,
- enable_advanced_logs_querying: false,
- },
-];
-
export const mockPodName = 'production-764c58d697-aaaaa';
export const mockPods = [
mockPodName,
diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js
index d5118bbde8c..9307a3b62fb 100644
--- a/spec/frontend/logs/stores/actions_spec.js
+++ b/spec/frontend/logs/stores/actions_spec.js
@@ -11,7 +11,6 @@ import {
fetchEnvironments,
fetchLogs,
fetchMoreLogsPrepend,
- fetchManagedApps,
} from '~/logs/stores/actions';
import * as types from '~/logs/stores/mutation_types';
import logsPageState from '~/logs/stores/state';
@@ -31,8 +30,6 @@ import {
mockResponse,
mockCursor,
mockNextCursor,
- mockManagedApps,
- mockManagedAppsEndpoint,
} from '../mock_data';
jest.mock('~/flash');
@@ -219,30 +216,6 @@ describe('Logs Store actions', () => {
});
});
- describe('fetchManagedApps', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- it('should commit RECEIVE_MANAGED_APPS_DATA_SUCCESS mutation on succesful fetch', () => {
- mock.onGet(mockManagedAppsEndpoint).replyOnce(200, { clusters: mockManagedApps });
- return testAction(fetchManagedApps, mockManagedAppsEndpoint, state, [
- { type: types.RECEIVE_MANAGED_APPS_DATA_SUCCESS, payload: mockManagedApps },
- ]);
- });
-
- it('should commit RECEIVE_MANAGED_APPS_DATA_ERROR on wrong data', () => {
- mock.onGet(mockManagedAppsEndpoint).replyOnce(500);
- return testAction(
- fetchManagedApps,
- mockManagedAppsEndpoint,
- state,
- [{ type: types.RECEIVE_MANAGED_APPS_DATA_ERROR }],
- [],
- );
- });
- });
-
describe('when the backend responds succesfully', () => {
let expectedMutations;
let expectedActions;
diff --git a/spec/frontend/logs/stores/getters_spec.js b/spec/frontend/logs/stores/getters_spec.js
index bca1ce4ca92..9d213d8c01f 100644
--- a/spec/frontend/logs/stores/getters_spec.js
+++ b/spec/frontend/logs/stores/getters_spec.js
@@ -1,14 +1,7 @@
import { trace, showAdvancedFilters } from '~/logs/stores/getters';
import logsPageState from '~/logs/stores/state';
-import {
- mockLogsResult,
- mockTrace,
- mockEnvName,
- mockEnvironments,
- mockManagedApps,
- mockManagedAppName,
-} from '../mock_data';
+import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data';
describe('Logs Store getters', () => {
let state;
@@ -79,43 +72,4 @@ describe('Logs Store getters', () => {
});
});
});
-
- describe('when no managedApps are set', () => {
- beforeEach(() => {
- state.environments.current = null;
- state.environments.options = [];
- state.managedApps.current = mockManagedAppName;
- state.managedApps.options = [];
- });
-
- it('returns false', () => {
- expect(showAdvancedFilters(state)).toBe(false);
- });
- });
-
- describe('when the managedApp supports filters', () => {
- beforeEach(() => {
- state.environments.current = null;
- state.environments.options = mockEnvironments;
- state.managedApps.current = mockManagedAppName;
- state.managedApps.options = mockManagedApps;
- });
-
- it('returns true', () => {
- expect(showAdvancedFilters(state)).toBe(true);
- });
- });
-
- describe('when the managedApp does not support filters', () => {
- beforeEach(() => {
- state.environments.current = null;
- state.environments.options = mockEnvironments;
- state.managedApps.options = mockManagedApps;
- state.managedApps.current = mockManagedApps[1].name;
- });
-
- it('returns false', () => {
- expect(showAdvancedFilters(state)).toBe(false);
- });
- });
});
diff --git a/spec/frontend/logs/stores/mutations_spec.js b/spec/frontend/logs/stores/mutations_spec.js
index 111c795ba52..988197a8350 100644
--- a/spec/frontend/logs/stores/mutations_spec.js
+++ b/spec/frontend/logs/stores/mutations_spec.js
@@ -11,8 +11,6 @@ import {
mockSearch,
mockCursor,
mockNextCursor,
- mockManagedApps,
- mockManagedAppName,
} from '../mock_data';
describe('Logs Store Mutations', () => {
@@ -32,15 +30,6 @@ describe('Logs Store Mutations', () => {
it('sets the environment', () => {
mutations[types.SET_PROJECT_ENVIRONMENT](state, mockEnvName);
expect(state.environments.current).toEqual(mockEnvName);
- expect(state.managedApps.current).toBe(null);
- });
- });
-
- describe('SET_MANAGED_APP', () => {
- it('sets the managed app', () => {
- mutations[types.SET_MANAGED_APP](state, mockManagedAppName);
- expect(state.managedApps.current).toBe(mockManagedAppName);
- expect(state.environments.current).toBe(null);
});
});
@@ -265,29 +254,4 @@ describe('Logs Store Mutations', () => {
);
});
});
-
- describe('RECEIVE_MANAGED_APPS_DATA_SUCCESS', () => {
- it('receives managed apps data success', () => {
- expect(state.managedApps.options).toEqual([]);
-
- mutations[types.RECEIVE_MANAGED_APPS_DATA_SUCCESS](state, mockManagedApps);
-
- expect(state.managedApps.options.length).toEqual(1);
- expect(state.managedApps.options).toEqual([mockManagedApps[0]]);
- expect(state.managedApps.isLoading).toBe(false);
- });
- });
-
- describe('RECEIVE_MANAGED_APPS_DATA_ERROR', () => {
- it('received managed apps data error', () => {
- mutations[types.RECEIVE_MANAGED_APPS_DATA_ERROR](state);
-
- expect(state.managedApps).toEqual({
- options: [],
- isLoading: false,
- current: null,
- fetchError: true,
- });
- });
- });
});
diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index 05933e36b52..b9fdf8792fd 100644
--- a/spec/frontend/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -33,7 +33,7 @@ describe('MembersApp', () => {
wrapper = shallowMount(MembersApp, {
localVue,
- provide: {
+ propsData: {
namespace: MEMBER_TYPES.user,
},
store,
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 28614b52706..6f1a6d0c223 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -6,7 +6,7 @@ import MembersTabs from '~/members/components/members_tabs.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { pagination } from '../mock_data';
-describe('MembersApp', () => {
+describe('MembersTabs', () => {
Vue.use(Vuex);
let wrapper;
@@ -111,10 +111,10 @@ describe('MembersApp', () => {
const membersApps = wrapper.findAllComponents(MembersApp).wrappers;
- expect(membersApps[0].attributes('namespace')).toBe(MEMBER_TYPES.user);
- expect(membersApps[1].attributes('namespace')).toBe(MEMBER_TYPES.group);
- expect(membersApps[2].attributes('namespace')).toBe(MEMBER_TYPES.invite);
- expect(membersApps[3].attributes('namespace')).toBe(MEMBER_TYPES.accessRequest);
+ expect(membersApps[0].props('namespace')).toBe(MEMBER_TYPES.user);
+ expect(membersApps[1].props('namespace')).toBe(MEMBER_TYPES.group);
+ expect(membersApps[2].props('namespace')).toBe(MEMBER_TYPES.invite);
+ expect(membersApps[3].props('namespace')).toBe(MEMBER_TYPES.accessRequest);
});
});
diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index 01279581c55..313c237f51c 100644
--- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -109,6 +109,6 @@ describe('RemoveGroupLinkModal', () => {
it('modal does not show when `removeGroupLinkModalVisible` is `false`', () => {
createComponent({ removeGroupLinkModalVisible: false });
- expect(findModal().vm.$attrs.visible).toBe(false);
+ expect(findModal().props().visible).toBe(false);
});
});
diff --git a/spec/frontend/members/components/table/expires_at_spec.js b/spec/frontend/members/components/table/expires_at_spec.js
index 02fe3c6d684..2b8e6ab8f2a 100644
--- a/spec/frontend/members/components/table/expires_at_spec.js
+++ b/spec/frontend/members/components/table/expires_at_spec.js
@@ -54,7 +54,7 @@ describe('ExpiresAt', () => {
const tooltipDirective = getTooltipDirective(expiredText);
expect(tooltipDirective).not.toBeUndefined();
- expect(expiredText.attributes('title')).toBe('Mar 15, 2019 12:00am GMT+0000');
+ expect(expiredText.attributes('title')).toBe('Mar 15, 2019 12:00am UTC');
});
});
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index c8b6bead450..a4a4c620921 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -88,7 +88,7 @@ describe('RoleDropdown', () => {
});
it('renders dropdown header', () => {
- expect(getByTextInDropdownMenu('Change permissions').exists()).toBe(true);
+ expect(getByTextInDropdownMenu('Change role').exists()).toBe(true);
});
it('sets dropdown toggle and checks selected role', () => {
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index b07534ae4ed..efabe54f238 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -1,5 +1,5 @@
import { createWrapper } from '@vue/test-utils';
-import MembersApp from '~/members/components/app.vue';
+import MembersTabs from '~/members/components/members_tabs.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { initMembersApp } from '~/members/index';
import { members, pagination, dataAttribute } from './mock_data';
@@ -11,12 +11,13 @@ describe('initMembersApp', () => {
const setup = () => {
vm = initMembersApp(el, {
- namespace: MEMBER_TYPES.user,
- tableFields: ['account'],
- tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
- tableSortableFields: ['account'],
- requestFormatter: () => ({}),
- filteredSearchBar: { show: false },
+ [MEMBER_TYPES.user]: {
+ tableFields: ['account'],
+ tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
+ tableSortableFields: ['account'],
+ requestFormatter: () => ({}),
+ filteredSearchBar: { show: false },
+ },
});
wrapper = createWrapper(vm);
};
@@ -35,10 +36,10 @@ describe('initMembersApp', () => {
wrapper = null;
});
- it('renders `MembersApp`', () => {
+ it('renders `MembersTabs`', () => {
setup();
- expect(wrapper.find(MembersApp).exists()).toBe(true);
+ expect(wrapper.find(MembersTabs).exists()).toBe(true);
});
it('parses and sets `members` in Vuex store', () => {
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index d0a7c36349b..4275db5fa9f 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -1,3 +1,5 @@
+import { MEMBER_TYPES } from '~/members/constants';
+
export const member = {
requestedAt: null,
canUpdate: false,
@@ -28,6 +30,7 @@ export const member = {
usingLicense: false,
groupSso: false,
groupManagedAccount: false,
+ provisionedByThisGroup: false,
validRoles: {
Guest: 10,
Reporter: 20,
@@ -97,10 +100,12 @@ export const pagination = {
};
export const dataAttribute = JSON.stringify({
- members,
- pagination: paginationData,
+ [MEMBER_TYPES.user]: {
+ members,
+ pagination: paginationData,
+ member_path: '/groups/foo-bar/-/group_members/:id',
+ ldap_override_path: '/groups/ldap-group/-/group_members/:id/override',
+ },
source_id: 234,
can_manage_members: true,
- member_path: '/groups/foo-bar/-/group_members/:id',
- ldap_override_path: '/groups/ldap-group/-/group_members/:id/override',
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 72696979722..9740e1c2edb 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -1,4 +1,4 @@
-import { DEFAULT_SORT } from '~/members/constants';
+import { DEFAULT_SORT, MEMBER_TYPES } from '~/members/constants';
import {
generateBadges,
isGroup,
@@ -268,11 +268,13 @@ describe('Members Utils', () => {
it('correctly parses the data attribute', () => {
expect(parseDataAttributes(el)).toMatchObject({
- members,
- pagination,
+ [MEMBER_TYPES.user]: {
+ members,
+ pagination,
+ memberPath: '/groups/foo-bar/-/group_members/:id',
+ },
sourceId: 234,
canManageMembers: true,
- memberPath: '/groups/foo-bar/-/group_members/:id',
});
});
});
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
index 1f0597bac67..9bf9e8ad7cc 100644
--- a/spec/frontend/monitoring/alert_widget_spec.js
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -1,7 +1,7 @@
import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
const mockReadAlert = jest.fn();
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 98503636d33..08f9e07244f 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -7,7 +7,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
environmentstate="available"
metricsdashboardbasepath="/monitoring/monitor-project/-/environments/1/metrics"
metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
- prometheusstatus=""
>
<alerts-deprecation-warning-stub />
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index afa63bcff29..754ddd96c9b 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -208,7 +208,7 @@ describe('Time series component', () => {
});
it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (GMT+0000)');
+ expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
});
it('formats tooltip content', () => {
@@ -282,7 +282,7 @@ describe('Time series component', () => {
});
it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (GMT+0000)');
+ expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
});
it('formats tooltip sha', () => {
@@ -301,7 +301,7 @@ describe('Time series component', () => {
});
it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (GMT+0000)');
+ expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
});
it('formats tooltip sha', () => {
@@ -334,7 +334,7 @@ describe('Time series component', () => {
it('formats tooltip title and sets tooltip content', () => {
const formattedTooltipData = wrapper.vm.formatAnnotationsTooltipText(mockMarkPoint);
- expect(formattedTooltipData.title).toBe('19 Feb 2020, 10:01AM (GMT+0000)');
+ expect(formattedTooltipData.title).toBe('19 Feb 2020, 10:01AM (UTC)');
expect(formattedTooltipData.content).toBe(annotationsMetadata.tooltipData.content);
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index a72dbbd0f41..c8951dff9ed 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -778,5 +778,31 @@ describe('Dashboard Panel', () => {
expect(findRunbookLinks().at(0).attributes('href')).toBe(invalidUrl);
});
});
+
+ describe('managed alert deprecation feature flag', () => {
+ beforeEach(() => {
+ setMetricsSavedToDb([metricId]);
+ });
+
+ it('shows alerts when alerts are not deprecated', () => {
+ createWrapper(
+ { alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
+ { provide: { glFeatures: { managedAlertsDeprecation: false } } },
+ );
+
+ expect(findAlertsWidget().exists()).toBe(true);
+ expect(findMenuItemByText('Alerts').exists()).toBe(true);
+ });
+
+ it('hides alerts when alerts are deprecated', () => {
+ createWrapper(
+ { alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
+ { provide: { glFeatures: { managedAlertsDeprecation: true } } },
+ );
+
+ expect(findAlertsWidget().exists()).toBe(false);
+ expect(findMenuItemByText('Alerts').exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 0c2f85c7298..7ca1b97d849 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,9 +1,8 @@
-import { GlAlert } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import VueDraggable from 'vuedraggable';
import { TEST_HOST } from 'helpers/test_constants';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { ESC_KEY } from '~/lib/utils/keys';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -17,7 +16,6 @@ import LinksSection from '~/monitoring/components/links_section.vue';
import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
-import AlertDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import {
metricsDashboardViewModel,
metricsDashboardPanelCount,
@@ -41,7 +39,7 @@ describe('Dashboard', () => {
let mock;
const createShallowWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(Dashboard, {
+ wrapper = shallowMountExtended(Dashboard, {
propsData: { ...dashboardProps, ...props },
store,
stubs: {
@@ -53,7 +51,7 @@ describe('Dashboard', () => {
};
const createMountedWrapper = (props = {}, options = {}) => {
- wrapper = mount(Dashboard, {
+ wrapper = mountExtended(Dashboard, {
propsData: { ...dashboardProps, ...props },
store,
stubs: {
@@ -818,24 +816,28 @@ describe('Dashboard', () => {
});
});
- describe('deprecation notice', () => {
+ describe('alerts deprecation', () => {
beforeEach(() => {
setupStoreWithData(store);
});
- const findDeprecationNotice = () =>
- wrapper.find(AlertDeprecationWarning).findComponent(GlAlert);
-
- it('shows the deprecation notice when available', () => {
- createMountedWrapper({}, { provide: { hasManagedPrometheus: true } });
-
- expect(findDeprecationNotice().exists()).toBe(true);
- });
-
- it('hides the deprecation notice when not available', () => {
- createMountedWrapper();
-
- expect(findDeprecationNotice().exists()).toBe(false);
- });
+ const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
+
+ it.each`
+ managedAlertsDeprecation | hasManagedPrometheus | isVisible
+ ${false} | ${false} | ${false}
+ ${false} | ${true} | ${true}
+ ${true} | ${false} | ${false}
+ ${true} | ${true} | ${false}
+ `(
+ 'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
+ ({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
+ createMountedWrapper(
+ {},
+ { provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } } },
+ );
+ expect(findDeprecationNotice().exists()).toBe(isVisible);
+ },
+ );
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 090613b0f1e..bea263f143a 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
queryToObject,
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index b7f741c449f..f60c531e3f6 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import statusCodes from '~/lib/utils/http_status';
@@ -257,9 +257,9 @@ describe('Monitoring store actions', () => {
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
- expect(createFlash).toHaveBeenCalledWith(
- expect.stringContaining(mockDashboardsErrorResponse.message),
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringContaining(mockDashboardsErrorResponse.message),
+ });
done();
})
.catch(done.fail);
@@ -1148,9 +1148,9 @@ describe('Monitoring store actions', () => {
return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- expect.stringContaining('error getting options for variable "label1"'),
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringContaining('error getting options for variable "label1"'),
+ });
},
);
});
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js
new file mode 100644
index 00000000000..7221ea2c5cd
--- /dev/null
+++ b/spec/frontend/nav/components/responsive_app_spec.js
@@ -0,0 +1,173 @@
+import { shallowMount } from '@vue/test-utils';
+import ResponsiveApp from '~/nav/components/responsive_app.vue';
+import ResponsiveHeader from '~/nav/components/responsive_header.vue';
+import ResponsiveHome from '~/nav/components/responsive_home.vue';
+import TopNavContainerView from '~/nav/components/top_nav_container_view.vue';
+import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '~/nav/event_hub';
+import { resetMenuItemsActive } from '~/nav/utils/reset_menu_items_active';
+import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
+import { TEST_NAV_DATA } from '../mock_data';
+
+const HTML_HEADER_CONTENT = '<div class="header-content"></div>';
+const HTML_MENU_EXPANDED = '<div class="menu-expanded"></div>';
+const HTML_HEADER_WITH_MENU_EXPANDED =
+ '<div></div><div class="header-content menu-expanded"></div>';
+
+describe('~/nav/components/responsive_app.vue', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(ResponsiveApp, {
+ propsData: {
+ navData: TEST_NAV_DATA,
+ },
+ stubs: {
+ KeepAliveSlots,
+ },
+ });
+ };
+ const triggerResponsiveToggle = () => eventHub.$emit(EVENT_RESPONSIVE_TOGGLE);
+
+ const findHome = () => wrapper.findComponent(ResponsiveHome);
+ const findMobileOverlay = () => wrapper.find('[data-testid="mobile-overlay"]');
+ const findSubviewHeader = () => wrapper.findComponent(ResponsiveHeader);
+ const findSubviewContainer = () => wrapper.findComponent(TopNavContainerView);
+ const hasBodyResponsiveOpen = () => document.body.classList.contains('top-nav-responsive-open');
+ const hasMobileOverlayVisible = () => findMobileOverlay().classes('mobile-nav-open');
+
+ beforeEach(() => {
+ document.body.innerHTML = '';
+ // Add test class to reset state + assert that we're adding classes correctly
+ document.body.className = 'test-class';
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows home by default', () => {
+ expect(findHome().isVisible()).toBe(true);
+ expect(findHome().props()).toEqual({
+ navData: resetMenuItemsActive(TEST_NAV_DATA),
+ });
+ });
+
+ it.each`
+ bodyHtml | expectation
+ ${''} | ${false}
+ ${HTML_HEADER_CONTENT} | ${false}
+ ${HTML_MENU_EXPANDED} | ${false}
+ ${HTML_HEADER_WITH_MENU_EXPANDED} | ${true}
+ `(
+ 'with responsive toggle event and html set to $bodyHtml, responsive open = $expectation',
+ ({ bodyHtml, expectation }) => {
+ document.body.innerHTML = bodyHtml;
+
+ triggerResponsiveToggle();
+
+ expect(hasBodyResponsiveOpen()).toBe(expectation);
+ },
+ );
+
+ it.each`
+ events | expectation
+ ${[]} | ${false}
+ ${['bv::dropdown::show']} | ${true}
+ ${['bv::dropdown::show', 'bv::dropdown::hide']} | ${false}
+ `(
+ 'with root events $events, movile overlay visible = $expectation',
+ async ({ events, expectation }) => {
+ // `await...reduce(async` is like doing an `forEach(async (...))` excpet it works
+ await events.reduce(async (acc, evt) => {
+ await acc;
+
+ wrapper.vm.$root.$emit(evt);
+
+ await wrapper.vm.$nextTick();
+ }, Promise.resolve());
+
+ expect(hasMobileOverlayVisible()).toBe(expectation);
+ },
+ );
+ });
+
+ describe('with menu expanded in body', () => {
+ beforeEach(() => {
+ document.body.innerHTML = HTML_HEADER_WITH_MENU_EXPANDED;
+ createComponent();
+ });
+
+ it('sets the body responsive open', () => {
+ expect(hasBodyResponsiveOpen()).toBe(true);
+ });
+ });
+
+ const projectsContainerProps = {
+ containerClass: 'gl-px-3',
+ frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.namespace,
+ frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.vuexModule,
+ linksPrimary: TEST_NAV_DATA.views.projects.linksPrimary,
+ linksSecondary: TEST_NAV_DATA.views.projects.linksSecondary,
+ };
+ const groupsContainerProps = {
+ containerClass: 'gl-px-3',
+ frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_GROUPS.namespace,
+ frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_GROUPS.vuexModule,
+ linksPrimary: TEST_NAV_DATA.views.groups.linksPrimary,
+ linksSecondary: TEST_NAV_DATA.views.groups.linksSecondary,
+ };
+
+ describe.each`
+ view | header | containerProps
+ ${'projects'} | ${'Projects'} | ${projectsContainerProps}
+ ${'groups'} | ${'Groups'} | ${groupsContainerProps}
+ `('when menu item with $view is clicked', ({ view, header, containerProps }) => {
+ beforeEach(async () => {
+ createComponent();
+
+ findHome().vm.$emit('menu-item-click', { view });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('shows header', () => {
+ expect(findSubviewHeader().text()).toBe(header);
+ });
+
+ it('shows container subview', () => {
+ expect(findSubviewContainer().props()).toEqual(containerProps);
+ });
+
+ it('hides home', () => {
+ expect(findHome().isVisible()).toBe(false);
+ });
+
+ describe('when header back button is clicked', () => {
+ beforeEach(() => {
+ findSubviewHeader().vm.$emit('menu-item-click', { view: 'home' });
+ });
+
+ it('shows home', () => {
+ expect(findHome().isVisible()).toBe(true);
+ });
+ });
+ });
+
+ describe('when destroyed', () => {
+ beforeEach(() => {
+ createComponent();
+ wrapper.destroy();
+ });
+
+ it('responsive toggle event does nothing', () => {
+ triggerResponsiveToggle();
+
+ expect(hasBodyResponsiveOpen()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js
new file mode 100644
index 00000000000..937c44727c7
--- /dev/null
+++ b/spec/frontend/nav/components/responsive_header_spec.js
@@ -0,0 +1,67 @@
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import ResponsiveHeader from '~/nav/components/responsive_header.vue';
+import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
+
+const TEST_SLOT_CONTENT = 'Test slot content';
+
+describe('~/nav/components/top_nav_menu_sections.vue', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(ResponsiveHeader, {
+ slots: {
+ default: TEST_SLOT_CONTENT,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ const findMenuItem = () => wrapper.findComponent(TopNavMenuItem);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders slot', () => {
+ expect(wrapper.text()).toBe(TEST_SLOT_CONTENT);
+ });
+
+ it('renders back button', () => {
+ const button = findMenuItem();
+
+ const tooltip = getBinding(button.element, 'gl-tooltip').value.title;
+
+ expect(tooltip).toBe('Go back');
+ expect(button.props()).toEqual({
+ menuItem: {
+ id: 'home',
+ view: 'home',
+ icon: 'angle-left',
+ },
+ iconOnly: true,
+ });
+ });
+
+ it('emits nothing', () => {
+ expect(wrapper.emitted()).toEqual({});
+ });
+
+ describe('when back button is clicked', () => {
+ beforeEach(() => {
+ findMenuItem().vm.$emit('click');
+ });
+
+ it('emits menu-item-click', () => {
+ expect(wrapper.emitted()).toEqual({
+ 'menu-item-click': [[{ id: 'home', view: 'home', icon: 'angle-left' }]],
+ });
+ });
+ });
+});
diff --git a/spec/frontend/nav/components/responsive_home_spec.js b/spec/frontend/nav/components/responsive_home_spec.js
new file mode 100644
index 00000000000..8f198d92747
--- /dev/null
+++ b/spec/frontend/nav/components/responsive_home_spec.js
@@ -0,0 +1,137 @@
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import ResponsiveHome from '~/nav/components/responsive_home.vue';
+import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
+import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
+import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
+import { TEST_NAV_DATA } from '../mock_data';
+
+const TEST_SEARCH_MENU_ITEM = {
+ id: 'search',
+ title: 'search',
+ icon: 'search',
+ href: '/search',
+};
+
+const TEST_NEW_DROPDOWN_VIEW_MODEL = {
+ title: 'new',
+ menu_sections: [],
+};
+
+describe('~/nav/components/responsive_home.vue', () => {
+ let wrapper;
+ let menuItemClickListener;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ResponsiveHome, {
+ propsData: {
+ navData: TEST_NAV_DATA,
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ listeners: {
+ 'menu-item-click': menuItemClickListener,
+ },
+ });
+ };
+
+ const findSearchMenuItem = () => wrapper.findComponent(TopNavMenuItem);
+ const findNewDropdown = () => wrapper.findComponent(TopNavNewDropdown);
+ const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
+
+ beforeEach(() => {
+ menuItemClickListener = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ desc | fn
+ ${'does not show search menu item'} | ${findSearchMenuItem}
+ ${'does not show new dropdown'} | ${findNewDropdown}
+ `('$desc', ({ fn }) => {
+ expect(fn().exists()).toBe(false);
+ });
+
+ it('shows menu sections', () => {
+ expect(findMenuSections().props('sections')).toEqual([
+ { id: 'primary', menuItems: TEST_NAV_DATA.primary },
+ { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
+ ]);
+ });
+
+ it('emits when menu sections emits', () => {
+ expect(menuItemClickListener).not.toHaveBeenCalled();
+
+ findMenuSections().vm.$emit('menu-item-click', TEST_NAV_DATA.primary[0]);
+
+ expect(menuItemClickListener).toHaveBeenCalledWith(TEST_NAV_DATA.primary[0]);
+ });
+ });
+
+ describe('without secondary', () => {
+ beforeEach(() => {
+ createComponent({ navData: { ...TEST_NAV_DATA, secondary: null } });
+ });
+
+ it('shows menu sections', () => {
+ expect(findMenuSections().props('sections')).toEqual([
+ { id: 'primary', menuItems: TEST_NAV_DATA.primary },
+ ]);
+ });
+ });
+
+ describe('with search view', () => {
+ beforeEach(() => {
+ createComponent({
+ navData: {
+ ...TEST_NAV_DATA,
+ views: { search: TEST_SEARCH_MENU_ITEM },
+ },
+ });
+ });
+
+ it('shows search menu item', () => {
+ expect(findSearchMenuItem().props()).toEqual({
+ menuItem: TEST_SEARCH_MENU_ITEM,
+ iconOnly: true,
+ });
+ });
+
+ it('shows tooltip for search', () => {
+ const tooltip = getBinding(findSearchMenuItem().element, 'gl-tooltip');
+ expect(tooltip.value).toEqual({ title: TEST_SEARCH_MENU_ITEM.title });
+ });
+ });
+
+ describe('with new view', () => {
+ beforeEach(() => {
+ createComponent({
+ navData: {
+ ...TEST_NAV_DATA,
+ views: { new: TEST_NEW_DROPDOWN_VIEW_MODEL },
+ },
+ });
+ });
+
+ it('shows new dropdown', () => {
+ expect(findNewDropdown().props()).toEqual({
+ viewModel: TEST_NEW_DROPDOWN_VIEW_MODEL,
+ });
+ });
+
+ it('shows tooltip for new dropdown', () => {
+ const tooltip = getBinding(findNewDropdown().element, 'gl-tooltip');
+ expect(tooltip.value).toEqual({ title: TEST_NEW_DROPDOWN_VIEW_MODEL.title });
+ });
+ });
+});
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index 06700ce748e..1d6ea99155b 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -1,5 +1,5 @@
-import { GlNavItemDropdown, GlTooltip } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlNavItemDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import TopNavApp from '~/nav/components/top_nav_app.vue';
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
import { TEST_NAV_DATA } from '../mock_data';
@@ -7,8 +7,8 @@ import { TEST_NAV_DATA } from '../mock_data';
describe('~/nav/components/top_nav_app.vue', () => {
let wrapper;
- const createComponent = (mountFn = shallowMount) => {
- wrapper = mountFn(TopNavApp, {
+ const createComponent = () => {
+ wrapper = shallowMount(TopNavApp, {
propsData: {
navData: TEST_NAV_DATA,
},
@@ -17,7 +17,6 @@ describe('~/nav/components/top_nav_app.vue', () => {
const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown);
const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
- const findTooltip = () => wrapper.findComponent(GlTooltip);
afterEach(() => {
wrapper.destroy();
@@ -31,7 +30,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
it('renders nav item dropdown', () => {
expect(findNavItemDropdown().attributes('href')).toBeUndefined();
expect(findNavItemDropdown().attributes()).toMatchObject({
- icon: 'dot-grid',
+ icon: 'hamburger',
text: TEST_NAV_DATA.activeTitle,
'no-flip': '',
});
@@ -44,25 +43,5 @@ describe('~/nav/components/top_nav_app.vue', () => {
views: TEST_NAV_DATA.views,
});
});
-
- it('renders tooltip', () => {
- expect(findTooltip().attributes()).toMatchObject({
- 'boundary-padding': '0',
- placement: 'right',
- title: TopNavApp.TOOLTIP,
- });
- });
- });
-
- describe('when full mounted', () => {
- beforeEach(() => {
- createComponent(mount);
- });
-
- it('has dropdown toggle as tooltip target', () => {
- const targetFn = findTooltip().props('target');
-
- expect(targetFn()).toBe(wrapper.find('.js-top-nav-dropdown-toggle').element);
- });
});
});
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 b08d75f36ce..06d2179b859 100644
--- a/spec/frontend/nav/components/top_nav_container_view_spec.js
+++ b/spec/frontend/nav/components/top_nav_container_view_spec.js
@@ -4,7 +4,7 @@ import FrequentItemsApp from '~/frequent_items/components/app.vue';
import { FREQUENT_ITEMS_PROJECTS } from '~/frequent_items/constants';
import eventHub from '~/frequent_items/event_hub';
import TopNavContainerView from '~/nav/components/top_nav_container_view.vue';
-import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
+import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
import { TEST_NAV_DATA } from '../mock_data';
@@ -13,39 +13,39 @@ const DEFAULT_PROPS = {
frequentItemsVuexModule: FREQUENT_ITEMS_PROJECTS.vuexModule,
linksPrimary: TEST_NAV_DATA.primary,
linksSecondary: TEST_NAV_DATA.secondary,
+ containerClass: 'test-frequent-items-container-class',
};
const TEST_OTHER_PROPS = {
namespace: 'projects',
- currentUserName: '',
- currentItem: {},
+ currentUserName: 'test-user',
+ currentItem: { id: 'test' },
};
describe('~/nav/components/top_nav_container_view.vue', () => {
let wrapper;
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, options = {}) => {
wrapper = shallowMount(TopNavContainerView, {
propsData: {
...DEFAULT_PROPS,
...TEST_OTHER_PROPS,
...props,
},
+ ...options,
});
};
- const findMenuItems = (parent = wrapper) => parent.findAll(TopNavMenuItem);
- const findMenuItemsModel = (parent = wrapper) =>
- findMenuItems(parent).wrappers.map((x) => x.props());
- const findMenuItemGroups = () => wrapper.findAll('[data-testid="menu-item-group"]');
- const findMenuItemGroupsModel = () => findMenuItemGroups().wrappers.map(findMenuItemsModel);
+ const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
const findFrequentItemsApp = () => {
const parent = wrapper.findComponent(VuexModuleProvider);
return {
vuexModule: parent.props('vuexModule'),
props: parent.findComponent(FrequentItemsApp).props(),
+ attributes: parent.findComponent(FrequentItemsApp).attributes(),
};
};
+ const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]');
afterEach(() => {
wrapper.destroy();
@@ -67,34 +67,40 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
);
describe('default', () => {
+ const EXTRA_ATTRS = { 'data-test-attribute': 'foo' };
+
beforeEach(() => {
- createComponent();
+ createComponent({}, { attrs: EXTRA_ATTRS });
+ });
+
+ it('does not inherit extra attrs', () => {
+ expect(wrapper.attributes()).toEqual({
+ class: expect.any(String),
+ });
});
it('renders frequent items app', () => {
expect(findFrequentItemsApp()).toEqual({
vuexModule: DEFAULT_PROPS.frequentItemsVuexModule,
- props: TEST_OTHER_PROPS,
+ props: expect.objectContaining(TEST_OTHER_PROPS),
+ attributes: expect.objectContaining(EXTRA_ATTRS),
});
});
- it('renders menu item groups', () => {
- expect(findMenuItemGroupsModel()).toEqual([
- TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })),
- TEST_NAV_DATA.secondary.map((menuItem) => ({ menuItem })),
- ]);
- });
-
- it('only the first group does not have margin top', () => {
- expect(findMenuItemGroups().wrappers.map((x) => x.classes('gl-mt-3'))).toEqual([false, true]);
+ it('renders given container class', () => {
+ expect(findFrequentItemsContainer().classes(DEFAULT_PROPS.containerClass)).toBe(true);
});
- it('only the first menu item does not have margin top', () => {
- const actual = findMenuItems(findMenuItemGroups().at(1)).wrappers.map((x) =>
- x.classes('gl-mt-1'),
- );
+ it('renders menu sections', () => {
+ const sections = [
+ { id: 'primary', menuItems: TEST_NAV_DATA.primary },
+ { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
+ ];
- expect(actual).toEqual([false, ...TEST_NAV_DATA.secondary.slice(1).fill(true)]);
+ expect(findMenuSections().props()).toEqual({
+ sections,
+ withTopBorder: true,
+ });
});
});
@@ -106,8 +112,8 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
});
it('renders one menu item group', () => {
- expect(findMenuItemGroupsModel()).toEqual([
- TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })),
+ expect(findMenuSections().props('sections')).toEqual([
+ { id: 'primary', menuItems: TEST_NAV_DATA.primary },
]);
});
});
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 d9bba22238a..70df05a2781 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -1,67 +1,62 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
+import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
+import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
import { TEST_NAV_DATA } from '../mock_data';
-const SECONDARY_GROUP_CLASSES = TopNavDropdownMenu.SECONDARY_GROUP_CLASS.split(' ');
-
describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
let wrapper;
- const createComponent = (props = {}) => {
- wrapper = shallowMount(TopNavDropdownMenu, {
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(TopNavDropdownMenu, {
propsData: {
primary: TEST_NAV_DATA.primary,
secondary: TEST_NAV_DATA.secondary,
views: TEST_NAV_DATA.views,
...props,
},
+ stubs: {
+ // Stub the keep-alive-slots so we don't render frequent items which uses a store
+ KeepAliveSlots: true,
+ },
});
};
- const findMenuItems = (parent = wrapper) => parent.findAll('[data-testid="menu-item"]');
- const findMenuItemsModel = (parent = wrapper) =>
- findMenuItems(parent).wrappers.map((x) => ({
- menuItem: x.props('menuItem'),
- isActive: x.classes('active'),
- }));
- const findMenuItemGroups = () => wrapper.findAll('[data-testid="menu-item-group"]');
- const findMenuItemGroupsModel = () =>
- findMenuItemGroups().wrappers.map((x) => ({
- classes: x.classes(),
- items: findMenuItemsModel(x),
- }));
+ const findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
+ const findMenuSections = () => wrapper.find(TopNavMenuSections);
const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');
- const createItemsGroupModelExpectation = ({
- primary = TEST_NAV_DATA.primary,
- secondary = TEST_NAV_DATA.secondary,
- activeIndex = -1,
- } = {}) => [
- {
- classes: [],
- items: primary.map((menuItem, index) => ({ isActive: index === activeIndex, menuItem })),
- },
- {
- classes: SECONDARY_GROUP_CLASSES,
- items: secondary.map((menuItem) => ({ isActive: false, menuItem })),
- },
- ];
+ const withActiveIndex = (menuItems, activeIndex) =>
+ menuItems.map((x, idx) => ({
+ ...x,
+ active: idx === activeIndex,
+ }));
afterEach(() => {
wrapper.destroy();
});
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+ });
+
describe('default', () => {
beforeEach(() => {
createComponent();
});
- it('renders menu item groups', () => {
- expect(findMenuItemGroupsModel()).toEqual(createItemsGroupModelExpectation());
+ it('renders menu sections', () => {
+ expect(findMenuSections().props()).toEqual({
+ sections: [
+ { id: 'primary', menuItems: TEST_NAV_DATA.primary },
+ { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
+ ],
+ withTopBorder: false,
+ });
});
it('has full width menu sidebar', () => {
@@ -74,36 +69,25 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
expect(subview.isVisible()).toBe(false);
expect(subview.props()).toEqual({ slotKey: '' });
});
-
- it('the first menu item in a group does not render margin top', () => {
- const actual = findMenuItems(findMenuItemGroups().at(0)).wrappers.map((x) =>
- x.classes('gl-mt-1'),
- );
-
- expect(actual).toEqual([false, ...TEST_NAV_DATA.primary.slice(1).fill(true)]);
- });
});
describe('with pre-initialized active view', () => {
- const primaryWithActive = [
- TEST_NAV_DATA.primary[0],
- {
- ...TEST_NAV_DATA.primary[1],
- active: true,
- },
- ...TEST_NAV_DATA.primary.slice(2),
- ];
-
beforeEach(() => {
- createComponent({
- primary: primaryWithActive,
- });
+ // We opt for a small integration test, to make sure the event is handled correctly
+ // as it would in prod.
+ createComponent(
+ {
+ primary: withActiveIndex(TEST_NAV_DATA.primary, 1),
+ },
+ mount,
+ );
});
- it('renders menu item groups', () => {
- expect(findMenuItemGroupsModel()).toEqual(
- createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 1 }),
- );
+ it('renders menu sections', () => {
+ expect(findMenuSections().props('sections')).toStrictEqual([
+ { id: 'primary', menuItems: withActiveIndex(TEST_NAV_DATA.primary, 1) },
+ { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
+ ]);
});
it('does not have full width menu sidebar', () => {
@@ -114,11 +98,11 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
const subview = findMenuSubview();
expect(subview.isVisible()).toBe(true);
- expect(subview.props('slotKey')).toBe(primaryWithActive[1].view);
+ expect(subview.props('slotKey')).toBe(TEST_NAV_DATA.primary[1].view);
});
it('does not change view if non-view menu item is clicked', async () => {
- const secondaryLink = findMenuItems().at(primaryWithActive.length);
+ const secondaryLink = findMenuItems().at(TEST_NAV_DATA.primary.length);
// Ensure this doesn't have a view
expect(secondaryLink.props('menuItem').view).toBeUndefined();
@@ -127,10 +111,10 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
await nextTick();
- expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[1].view);
+ expect(findMenuSubview().props('slotKey')).toBe(TEST_NAV_DATA.primary[1].view);
});
- describe('when other view menu item is clicked', () => {
+ describe('when menu item is clicked', () => {
let primaryLink;
beforeEach(async () => {
@@ -144,13 +128,20 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
});
it('changes active view', () => {
- expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[0].view);
+ expect(findMenuSubview().props('slotKey')).toBe(TEST_NAV_DATA.primary[0].view);
});
it('changes active status on menu item', () => {
- expect(findMenuItemGroupsModel()).toStrictEqual(
- createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 0 }),
- );
+ expect(findMenuSections().props('sections')).toStrictEqual([
+ {
+ id: 'primary',
+ menuItems: withActiveIndex(TEST_NAV_DATA.primary, 0),
+ },
+ {
+ id: 'secondary',
+ menuItems: withActiveIndex(TEST_NAV_DATA.secondary, -1),
+ },
+ ]);
});
});
});
diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js
index 579af13d08a..fd2b4d3b056 100644
--- a/spec/frontend/nav/components/top_nav_menu_item_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js
@@ -7,6 +7,7 @@ const TEST_MENU_ITEM = {
icon: 'search',
href: '/pretty/good/burger',
view: 'burger-view',
+ data: { qa_selector: 'not-a-real-selector', method: 'post', testFoo: 'test' },
};
describe('~/nav/components/top_nav_menu_item.vue', () => {
@@ -29,7 +30,10 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
const findButtonIcons = () =>
findButton()
.findAllComponents(GlIcon)
- .wrappers.map((x) => x.props('name'));
+ .wrappers.map((x) => ({
+ name: x.props('name'),
+ classes: x.classes(),
+ }));
beforeEach(() => {
listener = jest.fn();
@@ -47,6 +51,16 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect(button.text()).toBe(TEST_MENU_ITEM.title);
});
+ it('renders button data attributes', () => {
+ const button = findButton();
+
+ expect(button.attributes()).toMatchObject({
+ 'data-qa-selector': TEST_MENU_ITEM.data.qa_selector,
+ 'data-method': TEST_MENU_ITEM.data.method,
+ 'data-test-foo': TEST_MENU_ITEM.data.testFoo,
+ });
+ });
+
it('passes listeners to button', () => {
expect(listener).not.toHaveBeenCalled();
@@ -54,11 +68,42 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect(listener).toHaveBeenCalledWith('TEST');
});
+
+ it('renders expected icons', () => {
+ expect(findButtonIcons()).toEqual([
+ {
+ name: TEST_MENU_ITEM.icon,
+ classes: ['gl-mr-2!'],
+ },
+ {
+ name: 'chevron-right',
+ classes: ['gl-ml-auto'],
+ },
+ ]);
+ });
+ });
+
+ describe('with icon-only', () => {
+ beforeEach(() => {
+ createComponent({ iconOnly: true });
+ });
+
+ it('does not render title or view icon', () => {
+ expect(wrapper.text()).toBe('');
+ });
+
+ it('only renders menuItem icon', () => {
+ expect(findButtonIcons()).toEqual([
+ {
+ name: TEST_MENU_ITEM.icon,
+ classes: [],
+ },
+ ]);
+ });
});
describe.each`
desc | menuItem | expectedIcons
- ${'default'} | ${TEST_MENU_ITEM} | ${[TEST_MENU_ITEM.icon, 'chevron-right']}
${'with no icon'} | ${{ ...TEST_MENU_ITEM, icon: null }} | ${['chevron-right']}
${'with no view'} | ${{ ...TEST_MENU_ITEM, view: null }} | ${[TEST_MENU_ITEM.icon]}
${'with no icon or view'} | ${{ ...TEST_MENU_ITEM, view: null, icon: null }} | ${[]}
@@ -68,7 +113,32 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
});
it(`renders expected icons ${JSON.stringify(expectedIcons)}`, () => {
- expect(findButtonIcons()).toEqual(expectedIcons);
+ expect(findButtonIcons().map((x) => x.name)).toEqual(expectedIcons);
+ });
+ });
+
+ describe.each`
+ desc | active | cssClass | expectedClasses
+ ${'default'} | ${false} | ${''} | ${[]}
+ ${'with css class'} | ${false} | ${'test-css-class testing-123'} | ${['test-css-class', 'testing-123']}
+ ${'with css class & active'} | ${true} | ${'test-css-class'} | ${['test-css-class', ...TopNavMenuItem.ACTIVE_CLASS.split(' ')]}
+ `('$desc', ({ active, cssClass, expectedClasses }) => {
+ beforeEach(() => {
+ createComponent({
+ menuItem: {
+ ...TEST_MENU_ITEM,
+ active,
+ css_class: cssClass,
+ },
+ });
+ });
+
+ it('renders expected classes', () => {
+ expect(wrapper.classes()).toStrictEqual([
+ 'top-nav-menu-item',
+ 'gl-display-block',
+ ...expectedClasses,
+ ]);
});
});
});
diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
new file mode 100644
index 00000000000..d56542fe572
--- /dev/null
+++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
@@ -0,0 +1,107 @@
+import { shallowMount } from '@vue/test-utils';
+import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
+
+const TEST_SECTIONS = [
+ {
+ id: 'primary',
+ menuItems: [{ id: 'test', href: '/test/href' }, { id: 'foo' }, { id: 'bar' }],
+ },
+ {
+ id: 'secondary',
+ menuItems: [{ id: 'lorem' }, { id: 'ipsum' }],
+ },
+];
+
+describe('~/nav/components/top_nav_menu_sections.vue', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(TopNavMenuSections, {
+ propsData: {
+ sections: TEST_SECTIONS,
+ ...props,
+ },
+ });
+ };
+
+ const findMenuItemModels = (parent) =>
+ parent.findAll('[data-testid="menu-item"]').wrappers.map((x) => ({
+ menuItem: x.props('menuItem'),
+ classes: x.classes(),
+ }));
+ const findSectionModels = () =>
+ wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({
+ classes: x.classes(),
+ menuItems: findMenuItemModels(x),
+ }));
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders sections with menu items', () => {
+ expect(findSectionModels()).toEqual([
+ {
+ classes: [],
+ menuItems: [
+ {
+ menuItem: TEST_SECTIONS[0].menuItems[0],
+ classes: ['gl-w-full'],
+ },
+ ...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({
+ menuItem,
+ classes: ['gl-w-full', 'gl-mt-1'],
+ })),
+ ],
+ },
+ {
+ classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
+ menuItems: [
+ {
+ menuItem: TEST_SECTIONS[1].menuItems[0],
+ classes: ['gl-w-full'],
+ },
+ ...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({
+ menuItem,
+ classes: ['gl-w-full', 'gl-mt-1'],
+ })),
+ ],
+ },
+ ]);
+ });
+
+ it('when clicked menu item with href, does nothing', () => {
+ const menuItem = wrapper.findAll('[data-testid="menu-item"]').at(0);
+
+ menuItem.vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({});
+ });
+
+ it('when clicked menu item without href, emits "menu-item-click"', () => {
+ const menuItem = wrapper.findAll('[data-testid="menu-item"]').at(1);
+
+ menuItem.vm.$emit('click');
+
+ expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[1]]]);
+ });
+ });
+
+ describe('with withTopBorder=true', () => {
+ beforeEach(() => {
+ createComponent({ withTopBorder: true });
+ });
+
+ 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'],
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
new file mode 100644
index 00000000000..18210658b89
--- /dev/null
+++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
@@ -0,0 +1,122 @@
+import { GlDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
+
+const TEST_VIEW_MODEL = {
+ title: 'Dropdown',
+ menu_sections: [
+ {
+ title: 'Section 1',
+ menu_items: [
+ { id: 'foo-1', title: 'Foo 1', href: '/foo/1' },
+ { id: 'foo-2', title: 'Foo 2', href: '/foo/2' },
+ { id: 'foo-3', title: 'Foo 3', href: '/foo/3' },
+ ],
+ },
+ {
+ title: 'Section 2',
+ menu_items: [
+ { id: 'bar-1', title: 'Bar 1', href: '/bar/1' },
+ { id: 'bar-2', title: 'Bar 2', href: '/bar/2' },
+ ],
+ },
+ ],
+};
+
+describe('~/nav/components/top_nav_menu_sections.vue', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(TopNavNewDropdown, {
+ propsData: {
+ viewModel: TEST_VIEW_MODEL,
+ ...props,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownContents = () =>
+ findDropdown()
+ .findAll('[data-testid]')
+ .wrappers.map((child) => {
+ const type = child.attributes('data-testid');
+
+ if (type === 'divider') {
+ return { type };
+ } else if (type === 'header') {
+ return { type, text: child.text() };
+ }
+
+ return {
+ type,
+ text: child.text(),
+ href: child.attributes('href'),
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders dropdown parent', () => {
+ expect(findDropdown().props()).toMatchObject({
+ text: TEST_VIEW_MODEL.title,
+ textSrOnly: true,
+ icon: 'plus',
+ });
+ });
+
+ it('renders dropdown content', () => {
+ expect(findDropdownContents()).toEqual([
+ {
+ type: 'header',
+ text: TEST_VIEW_MODEL.menu_sections[0].title,
+ },
+ ...TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({
+ type: 'item',
+ href,
+ text: title,
+ })),
+ {
+ type: 'divider',
+ },
+ {
+ type: 'header',
+ text: TEST_VIEW_MODEL.menu_sections[1].title,
+ },
+ ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({
+ type: 'item',
+ href,
+ text: title,
+ })),
+ ]);
+ });
+ });
+
+ describe('with only 1 section', () => {
+ beforeEach(() => {
+ createComponent({
+ viewModel: {
+ ...TEST_VIEW_MODEL,
+ menu_sections: TEST_VIEW_MODEL.menu_sections.slice(0, 1),
+ },
+ });
+ });
+
+ it('renders dropdown content without headers and dividers', () => {
+ expect(findDropdownContents()).toEqual(
+ TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({
+ type: 'item',
+ href,
+ text: title,
+ })),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/nav/mock_data.js b/spec/frontend/nav/mock_data.js
index 2987d8deb16..c2ad86a4605 100644
--- a/spec/frontend/nav/mock_data.js
+++ b/spec/frontend/nav/mock_data.js
@@ -25,11 +25,15 @@ export const TEST_NAV_DATA = {
namespace: 'projects',
currentUserName: '',
currentItem: {},
+ linksPrimary: [{ id: 'project-link', href: '/path/to/projects', title: 'Project Link' }],
+ linksSecondary: [],
},
groups: {
namespace: 'groups',
currentUserName: '',
currentItem: {},
+ linksPrimary: [],
+ linksSecondary: [{ id: 'group-link', href: '/path/to/groups', title: 'Group Link' }],
},
},
};
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index b140eea9439..537622b7918 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -328,20 +328,45 @@ describe('issue_comment_form component', () => {
mountComponent({ mountFunction: mount });
});
- it('should save note when cmd+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSave');
+ describe('when no draft exists', () => {
+ it('should save note when cmd+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
- findTextArea().trigger('keydown.enter', { metaKey: true });
+ findTextArea().trigger('keydown.enter', { metaKey: true });
- expect(wrapper.vm.handleSave).toHaveBeenCalled();
+ expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ });
+
+ it('should save note when ctrl+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
+
+ findTextArea().trigger('keydown.enter', { ctrlKey: true });
+
+ expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ });
});
- it('should save note when ctrl+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSave');
+ describe('when a draft exists', () => {
+ beforeEach(() => {
+ store.registerModule('batchComments', batchComments());
+ store.state.batchComments.drafts = [{ note: 'A' }];
+ });
+
+ it('should save note draft when cmd+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSaveDraft');
+
+ findTextArea().trigger('keydown.enter', { metaKey: true });
+
+ expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ });
+
+ it('should save note draft when ctrl+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSaveDraft');
- findTextArea().trigger('keydown.enter', { ctrlKey: true });
+ findTextArea().trigger('keydown.enter', { ctrlKey: true });
- expect(wrapper.vm.handleSave).toHaveBeenCalled();
+ expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ });
});
});
});
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index c6a7d7ead98..925dbcc09ec 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -20,7 +20,7 @@ const createUnallowedNote = () =>
describe('DiscussionActions', () => {
let wrapper;
- const createComponentFactory = (shallow = true) => (props) => {
+ const createComponentFactory = (shallow = true) => (props, options) => {
const store = createStore();
const mountFn = shallow ? shallowMount : mount;
@@ -34,6 +34,7 @@ describe('DiscussionActions', () => {
shouldShowJumpToNextDiscussion: true,
...props,
},
+ ...options,
});
};
@@ -90,17 +91,17 @@ describe('DiscussionActions', () => {
describe('events handling', () => {
const createComponent = createComponentFactory(false);
- beforeEach(() => {
- createComponent();
- });
-
it('emits showReplyForm event when clicking on reply placeholder', () => {
+ createComponent({}, { attachTo: document.body });
+
jest.spyOn(wrapper.vm, '$emit');
wrapper.find(ReplyPlaceholder).find('textarea').trigger('focus');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm');
});
it('emits resolve event when clicking on resolve button', () => {
+ createComponent();
+
jest.spyOn(wrapper.vm, '$emit');
wrapper.find(ResolveDiscussionButton).find('button').trigger('click');
expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve');
diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index 2a4cd0df0c7..3932f818c4e 100644
--- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
@@ -6,31 +6,34 @@ const placeholderText = 'Test Button Text';
describe('ReplyPlaceholder', () => {
let wrapper;
- const findTextarea = () => wrapper.find({ ref: 'textarea' });
-
- beforeEach(() => {
+ const createComponent = ({ options = {} } = {}) => {
wrapper = shallowMount(ReplyPlaceholder, {
propsData: {
placeholderText,
},
+ ...options,
});
- });
+ };
+
+ const findTextarea = () => wrapper.find({ ref: 'textarea' });
afterEach(() => {
wrapper.destroy();
});
- it('emits focus event on button click', () => {
- findTextarea().trigger('focus');
+ it('emits focus event on button click', async () => {
+ createComponent({ options: { attachTo: document.body } });
+
+ await findTextarea().trigger('focus');
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.emitted()).toEqual({
- focus: [[]],
- });
+ expect(wrapper.emitted()).toEqual({
+ focus: [[]],
});
});
it('should render reply button', () => {
+ createComponent();
+
expect(findTextarea().attributes('placeholder')).toEqual(placeholderText);
});
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 735bc2b70dd..a364a524e7b 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -56,6 +56,18 @@ describe('noteable_discussion component', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(true);
});
+ it('should hide actions when diff refs do not exists', async () => {
+ const discussion = { ...discussionMock };
+ discussion.diff_file = { ...mockDiffFile, diff_refs: null };
+ discussion.diff_discussion = true;
+ discussion.expanded = false;
+
+ wrapper.setProps({ discussion });
+ await nextTick();
+
+ expect(wrapper.vm.canShowReplyActions).toBe(false);
+ });
+
describe('actions', () => {
it('should toggle reply form', async () => {
await nextTick();
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 9b7456d54bc..7eef2017dfb 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -25,7 +25,19 @@ import {
} from '../mock_data';
const TEST_ERROR_MESSAGE = 'Test error message';
-jest.mock('~/flash');
+const mockFlashClose = jest.fn();
+jest.mock('~/flash', () => {
+ const flash = jest.fn().mockImplementation(() => {
+ return {
+ close: mockFlashClose,
+ };
+ });
+
+ return {
+ createFlash: flash,
+ deprecatedCreateFlash: flash,
+ };
+});
describe('Actions Notes Store', () => {
let commit;
@@ -254,42 +266,144 @@ describe('Actions Notes Store', () => {
});
describe('poll', () => {
- beforeEach((done) => {
- axiosMock
- .onGet(notesDataMock.notesPath)
- .reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' });
+ const pollInterval = 6000;
+ 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);
+ const advanceAndRAF = async (time) => {
+ if (time) {
+ jest.advanceTimersByTime(time);
+ }
+
+ return new Promise((resolve) => requestAnimationFrame(resolve));
+ };
+ const advanceXMoreIntervals = async (number) => {
+ const timeoutLength = pollInterval * number;
+ return advanceAndRAF(timeoutLength);
+ };
+ const startPolling = async () => {
+ await store.dispatch('poll');
+ await advanceAndRAF(2);
+ };
+ const cleanUp = async () => {
+ jest.clearAllTimers();
+
+ return store.dispatch('stopPolling');
+ };
+
+ beforeEach((done) => {
store.dispatch('setNotesData', notesDataMock).then(done).catch(done.fail);
});
- it('calls service with last fetched state', (done) => {
- store
- .dispatch('poll')
- .then(() => {
- jest.advanceTimersByTime(2);
- })
- .then(() => new Promise((resolve) => requestAnimationFrame(resolve)))
- .then(() => {
- expect(store.state.lastFetchedAt).toBe('123456');
-
- jest.advanceTimersByTime(1500);
- })
- .then(
- () =>
- new Promise((resolve) => {
- requestAnimationFrame(resolve);
- }),
- )
- .then(() => {
- const expectedGetRequests = 2;
- expect(axiosMock.history.get.length).toBe(expectedGetRequests);
- expect(axiosMock.history.get[expectedGetRequests - 1].headers).toMatchObject({
- 'X-Last-Fetched-At': '123456',
- });
- })
- .then(() => store.dispatch('stopPolling'))
- .then(done)
- .catch(done.fail);
+ afterEach(() => {
+ return cleanUp();
+ });
+
+ it('calls service with last fetched state', async () => {
+ successMock();
+
+ await startPolling();
+
+ expect(store.state.lastFetchedAt).toBe('123456');
+
+ await advanceXMoreIntervals(1);
+
+ expect(axiosMock.history.get).toHaveLength(2);
+ expect(axiosMock.history.get[1].headers).toMatchObject({
+ 'X-Last-Fetched-At': '123456',
+ });
+ });
+
+ describe('polling side effects', () => {
+ it('retries twice', async () => {
+ failureMock();
+
+ await startPolling();
+
+ // This is the first request, not a retry
+ expect(axiosMock.history.get).toHaveLength(1);
+
+ await advanceXMoreIntervals(1);
+
+ // Retry #1
+ expect(axiosMock.history.get).toHaveLength(2);
+
+ await advanceXMoreIntervals(1);
+
+ // Retry #2
+ expect(axiosMock.history.get).toHaveLength(3);
+
+ await advanceXMoreIntervals(10);
+
+ // There are no more retries
+ expect(axiosMock.history.get).toHaveLength(3);
+ });
+
+ it('shows the error display on the second failure', async () => {
+ failureMock();
+
+ await startPolling();
+
+ expect(axiosMock.history.get).toHaveLength(1);
+ expect(Flash).not.toHaveBeenCalled();
+
+ await advanceXMoreIntervals(1);
+
+ expect(axiosMock.history.get).toHaveLength(2);
+ expect(Flash).toHaveBeenCalled();
+ expect(Flash).toHaveBeenCalledTimes(1);
+ });
+
+ it('resets the failure counter on success', async () => {
+ // We can't get access to the actual counter in the polling closure.
+ // So we can infer that it's reset by ensuring that the error is only
+ // shown when we cause two failures in a row - no successes between
+
+ axiosMock
+ .onGet(notesDataMock.notesPath)
+ .replyOnce(500) // cause one error
+ .onGet(notesDataMock.notesPath)
+ .replyOnce(200, pollResponse, pollHeaders) // then a success
+ .onGet(notesDataMock.notesPath)
+ .reply(500); // and then more errors
+
+ await startPolling(); // Failure #1
+ await advanceXMoreIntervals(1); // Success #1
+ await advanceXMoreIntervals(1); // Failure #2
+
+ // That was the first failure AFTER a success, so we should NOT see the error displayed
+ expect(Flash).not.toHaveBeenCalled();
+
+ // Now we'll allow another failure
+ await advanceXMoreIntervals(1); // Failure #3
+
+ // Since this is the second failure in a row, the error should happen
+ expect(Flash).toHaveBeenCalled();
+ expect(Flash).toHaveBeenCalledTimes(1);
+ });
+
+ it('hides the error display if it exists on success', async () => {
+ jest.mock();
+ failureMock();
+
+ await startPolling();
+ await advanceXMoreIntervals(2);
+
+ // After two errors, the error should be displayed
+ expect(Flash).toHaveBeenCalled();
+ expect(Flash).toHaveBeenCalledTimes(1);
+
+ axiosMock.reset();
+ successMock();
+
+ await advanceXMoreIntervals(1);
+
+ expect(mockFlashClose).toHaveBeenCalled();
+ expect(mockFlashClose).toHaveBeenCalledTimes(1);
+ });
});
});
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 272e9b71f67..5eecfd395e2 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { timezones } from '~/monitoring/format_date';
@@ -56,7 +56,7 @@ describe('operation settings external dashboard component', () => {
it('renders header text', () => {
mountComponent();
- expect(wrapper.find('.js-section-header').text()).toBe('Metrics dashboard');
+ expect(wrapper.find('.js-section-header').text()).toBe('Metrics');
});
describe('expand/collapse button', () => {
@@ -77,13 +77,13 @@ describe('operation settings external dashboard component', () => {
});
it('renders descriptive text', () => {
- expect(subHeader.text()).toContain('Manage Metrics Dashboard settings.');
+ expect(subHeader.text()).toContain('Manage metrics dashboard settings.');
});
it('renders help page link', () => {
const link = subHeader.find(GlLink);
- expect(link.text()).toBe('Learn more');
+ expect(link.text()).toBe('Learn more.');
expect(link.attributes().href).toBe(helpPage);
});
});
@@ -203,10 +203,10 @@ describe('operation settings external dashboard component', () => {
.$nextTick()
.then(jest.runAllTicks)
.then(() =>
- expect(createFlash).toHaveBeenCalledWith(
- `There was an error saving your changes. ${message}`,
- 'alert',
- ),
+ expect(createFlash).toHaveBeenCalledWith({
+ message: `There was an error saving your changes. ${message}`,
+ type: 'alert',
+ }),
);
});
});
diff --git a/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap
new file mode 100644
index 00000000000..881d441e116
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/file_sha_spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FileSha renders 1`] = `
+<div
+ class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all gl-py-2 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
+>
+ <!---->
+
+ <span>
+ <div
+ class="gl-px-4"
+ >
+
+ bar:
+ foo
+
+ <gl-button-stub
+ aria-label="Copy this value"
+ buttontextclasses=""
+ category="tertiary"
+ data-clipboard-text="foo"
+ icon="copy-to-clipboard"
+ size="small"
+ title="Copy SHA"
+ variant="default"
+ />
+ </div>
+ </span>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js
index 11dad7ba34d..3132ec61942 100644
--- a/spec/frontend/packages/details/components/app_spec.js
+++ b/spec/frontend/packages/details/components/app_spec.js
@@ -1,5 +1,6 @@
-import { GlEmptyState, GlModal } from '@gitlab/ui';
+import { GlEmptyState } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
@@ -34,6 +35,7 @@ describe('PackagesApp', () => {
let store;
const fetchPackageVersions = jest.fn();
const deletePackage = jest.fn();
+ const deletePackageFile = jest.fn();
const defaultProjectName = 'bar';
const { location } = window;
@@ -59,6 +61,7 @@ describe('PackagesApp', () => {
actions: {
deletePackage,
fetchPackageVersions,
+ deletePackageFile,
},
getters,
});
@@ -82,8 +85,8 @@ describe('PackagesApp', () => {
const packageTitle = () => wrapper.find(PackageTitle);
const emptyState = () => wrapper.find(GlEmptyState);
const deleteButton = () => wrapper.find('.js-delete-button');
- const deleteModal = () => wrapper.find(GlModal);
- const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' });
+ const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' });
+ const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' });
const versionsTab = () => wrapper.find('.js-versions-tab > a');
const packagesLoader = () => wrapper.find(PackagesListLoader);
const packagesVersionRows = () => wrapper.findAll(PackageListRow);
@@ -107,10 +110,12 @@ describe('PackagesApp', () => {
window.location = location;
});
- it('renders the app and displays the package title', () => {
+ it('renders the app and displays the package title', async () => {
createComponent();
- expect(packageTitle()).toExist();
+ await nextTick();
+
+ expect(packageTitle().exists()).toBe(true);
});
it('renders an empty state component when no an invalid package is passed as a prop', () => {
@@ -118,7 +123,7 @@ describe('PackagesApp', () => {
packageEntity: {},
});
- expect(emptyState()).toExist();
+ expect(emptyState().exists()).toBe(true);
});
it('package history has the right props', () => {
@@ -152,7 +157,16 @@ describe('PackagesApp', () => {
});
it('shows the delete confirmation modal when delete is clicked', () => {
- expect(deleteModal()).toExist();
+ expect(findDeleteModal().exists()).toBe(true);
+ });
+ });
+
+ describe('deleting package files', () => {
+ it('shows the delete confirmation modal when delete is clicked', () => {
+ createComponent();
+ findPackageFiles().vm.$emit('delete-file', mavenFiles[0]);
+
+ expect(findDeleteFileModal().exists()).toBe(true);
});
});
@@ -228,13 +242,7 @@ describe('PackagesApp', () => {
});
describe('tracking and delete', () => {
- const doDelete = async () => {
- deleteButton().trigger('click');
- await wrapper.vm.$nextTick();
- modalDeleteButton().trigger('click');
- };
-
- describe('delete', () => {
+ describe('delete package', () => {
const originalReferrer = document.referrer;
const setReferrer = (value = defaultProjectName) => {
Object.defineProperty(document, 'referrer', {
@@ -250,9 +258,9 @@ describe('PackagesApp', () => {
});
});
- it('calls the proper vuex action', async () => {
+ it('calls the proper vuex action', () => {
createComponent({ packageEntity: npmPackage });
- await doDelete();
+ findDeleteModal().vm.$emit('primary');
expect(deletePackage).toHaveBeenCalled();
});
@@ -260,7 +268,7 @@ describe('PackagesApp', () => {
setReferrer();
deletePackage.mockResolvedValue();
createComponent({ packageEntity: npmPackage });
- await doDelete();
+ findDeleteModal().vm.$emit('primary');
await deletePackage();
expect(window.location.replace).toHaveBeenCalledWith(
'project_url?showSuccessDeleteAlert=true',
@@ -271,7 +279,7 @@ describe('PackagesApp', () => {
setReferrer('baz');
deletePackage.mockResolvedValue();
createComponent({ packageEntity: npmPackage });
- await doDelete();
+ findDeleteModal().vm.$emit('primary');
await deletePackage();
expect(window.location.replace).toHaveBeenCalledWith(
'group_url?showSuccessDeleteAlert=true',
@@ -279,6 +287,17 @@ describe('PackagesApp', () => {
});
});
+ describe('delete file', () => {
+ it('calls the proper vuex action', () => {
+ createComponent({ packageEntity: npmPackage });
+
+ findPackageFiles().vm.$emit('delete-file', mavenFiles[0]);
+ findDeleteFileModal().vm.$emit('primary');
+
+ expect(deletePackageFile).toHaveBeenCalled();
+ });
+ });
+
describe('tracking', () => {
let eventSpy;
let utilSpy;
@@ -295,9 +314,9 @@ describe('PackagesApp', () => {
expect(utilSpy).toHaveBeenCalledWith('conan');
});
- it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, async () => {
+ it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => {
createComponent({ packageEntity: npmPackage });
- await doDelete();
+ findDeleteModal().vm.$emit('primary');
expect(eventSpy).toHaveBeenCalledWith(
category,
TrackingActions.DELETE_PACKAGE,
@@ -305,6 +324,56 @@ describe('PackagesApp', () => {
);
});
+ it(`canceling a package deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE}`, () => {
+ createComponent({ packageEntity: npmPackage });
+
+ findDeleteModal().vm.$emit('canceled');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.CANCEL_DELETE_PACKAGE,
+ expect.any(Object),
+ );
+ });
+
+ it(`request a file deletion tracks ${TrackingActions.REQUEST_DELETE_PACKAGE_FILE}`, () => {
+ createComponent({ packageEntity: npmPackage });
+
+ findPackageFiles().vm.$emit('delete-file', mavenFiles[0]);
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.REQUEST_DELETE_PACKAGE_FILE,
+ expect.any(Object),
+ );
+ });
+
+ it(`confirming a file deletion tracks ${TrackingActions.DELETE_PACKAGE_FILE}`, () => {
+ createComponent({ packageEntity: npmPackage });
+
+ findPackageFiles().vm.$emit('delete-file', npmPackage);
+ findDeleteFileModal().vm.$emit('primary');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.REQUEST_DELETE_PACKAGE_FILE,
+ expect.any(Object),
+ );
+ });
+
+ it(`canceling a file deletion tracks ${TrackingActions.CANCEL_DELETE_PACKAGE_FILE}`, () => {
+ createComponent({ packageEntity: npmPackage });
+
+ findPackageFiles().vm.$emit('delete-file', npmPackage);
+ findDeleteFileModal().vm.$emit('canceled');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.CANCEL_DELETE_PACKAGE_FILE,
+ expect.any(Object),
+ );
+ });
+
it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
createComponent({ packageEntity: conanPackage });
diff --git a/spec/frontend/packages/details/components/file_sha_spec.js b/spec/frontend/packages/details/components/file_sha_spec.js
new file mode 100644
index 00000000000..7bfcf78baab
--- /dev/null
+++ b/spec/frontend/packages/details/components/file_sha_spec.js
@@ -0,0 +1,33 @@
+import { shallowMount } from '@vue/test-utils';
+
+import FileSha from '~/packages/details/components/file_sha.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+describe('FileSha', () => {
+ let wrapper;
+
+ const defaultProps = { sha: 'foo', title: 'bar' };
+
+ function createComponent() {
+ wrapper = shallowMount(FileSha, {
+ propsData: {
+ ...defaultProps,
+ },
+ stubs: {
+ ClipboardButton,
+ DetailsRow,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/packages/details/components/installations_commands_spec.js b/spec/frontend/packages/details/components/installations_commands_spec.js
index 065bf503585..164f9f69741 100644
--- a/spec/frontend/packages/details/components/installations_commands_spec.js
+++ b/spec/frontend/packages/details/components/installations_commands_spec.js
@@ -7,6 +7,7 @@ import MavenInstallation from '~/packages/details/components/maven_installation.
import NpmInstallation from '~/packages/details/components/npm_installation.vue';
import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
+import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue';
import {
conanPackage,
@@ -15,6 +16,7 @@ import {
nugetPackage,
pypiPackage,
composerPackage,
+ terraformModule,
} from '../../mock_data';
describe('InstallationCommands', () => {
@@ -32,6 +34,7 @@ describe('InstallationCommands', () => {
const nugetInstallation = () => wrapper.find(NugetInstallation);
const pypiInstallation = () => wrapper.find(PypiInstallation);
const composerInstallation = () => wrapper.find(ComposerInstallation);
+ const terraformInstallation = () => wrapper.findComponent(TerraformInstallation);
afterEach(() => {
wrapper.destroy();
@@ -46,6 +49,7 @@ describe('InstallationCommands', () => {
${nugetPackage} | ${nugetInstallation}
${pypiPackage} | ${pypiInstallation}
${composerPackage} | ${composerInstallation}
+ ${terraformModule} | ${terraformInstallation}
`('renders', ({ packageEntity, selector }) => {
it(`${packageEntity.package_type} instructions exist`, () => {
createComponent({ packageEntity });
diff --git a/spec/frontend/packages/details/components/package_files_spec.js b/spec/frontend/packages/details/components/package_files_spec.js
index bcf1b6d56f0..e8e5a24d3a3 100644
--- a/spec/frontend/packages/details/components/package_files_spec.js
+++ b/spec/frontend/packages/details/components/package_files_spec.js
@@ -1,4 +1,6 @@
+import { GlDropdown, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue/';
import stubChildren from 'helpers/stub_children';
import component from '~/packages/details/components/package_files.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -12,16 +14,21 @@ describe('Package Files', () => {
const findAllRows = () => wrapper.findAll('[data-testid="file-row"');
const findFirstRow = () => findAllRows().at(0);
const findSecondRow = () => findAllRows().at(1);
- const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"');
- const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"');
- const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"');
+ const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]');
+ const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]');
+ const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]');
const findFirstRowFileIcon = () => findFirstRow().find(FileIcon);
const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip);
+ const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown);
+ const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]');
+ const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
+ const findFirstRowShaComponent = (id) => wrapper.find(`[data-testid="${id}"]`);
- const createComponent = (packageFiles = npmFiles) => {
+ const createComponent = ({ packageFiles = npmFiles, canDelete = true } = {}) => {
wrapper = mount(component, {
propsData: {
packageFiles,
+ canDelete,
},
stubs: {
...stubChildren(component),
@@ -43,7 +50,7 @@ describe('Package Files', () => {
});
it('renders multiple files for a package that contains more than one file', () => {
- createComponent(mavenFiles);
+ createComponent({ packageFiles: mavenFiles });
expect(findAllRows()).toHaveLength(2);
});
@@ -123,7 +130,7 @@ describe('Package Files', () => {
});
describe('when package file has no pipeline associated', () => {
it('does not exist', () => {
- createComponent(mavenFiles);
+ createComponent({ packageFiles: mavenFiles });
expect(findFirstRowCommitLink().exists()).toBe(false);
});
@@ -131,11 +138,122 @@ describe('Package Files', () => {
describe('when only one file lacks an associated pipeline', () => {
it('renders the commit when it exists and not otherwise', () => {
- createComponent([npmFiles[0], mavenFiles[0]]);
+ createComponent({ packageFiles: [npmFiles[0], mavenFiles[0]] });
expect(findFirstRowCommitLink().exists()).toBe(true);
expect(findSecondRowCommitLink().exists()).toBe(false);
});
});
+
+ describe('action menu', () => {
+ describe('when the user can delete', () => {
+ it('exists', () => {
+ createComponent();
+
+ expect(findFirstActionMenu().exists()).toBe(true);
+ });
+
+ describe('menu items', () => {
+ describe('delete file', () => {
+ it('exists', () => {
+ createComponent();
+
+ expect(findActionMenuDelete().exists()).toBe(true);
+ });
+
+ it('emits a delete event when clicked', () => {
+ createComponent();
+
+ findActionMenuDelete().vm.$emit('click');
+
+ const [[{ id }]] = wrapper.emitted('delete-file');
+ expect(id).toBe(npmFiles[0].id);
+ });
+ });
+ });
+ });
+
+ describe('when the user can not delete', () => {
+ const canDelete = false;
+
+ it('does not exist', () => {
+ createComponent({ canDelete });
+
+ expect(findFirstActionMenu().exists()).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('additional details', () => {
+ describe('details toggle button', () => {
+ it('exists', () => {
+ createComponent();
+
+ expect(findFirstToggleDetailsButton().exists()).toBe(true);
+ });
+
+ it('is hidden when no details is present', () => {
+ const [{ ...noShaFile }] = npmFiles;
+ noShaFile.file_sha256 = null;
+ noShaFile.file_md5 = null;
+ noShaFile.file_sha1 = null;
+ createComponent({ packageFiles: [noShaFile] });
+
+ expect(findFirstToggleDetailsButton().exists()).toBe(false);
+ });
+
+ it('toggles the details row', async () => {
+ createComponent();
+
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down');
+
+ findFirstToggleDetailsButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findFirstRowShaComponent('sha-256').exists()).toBe(true);
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-up');
+
+ findFirstToggleDetailsButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findFirstRowShaComponent('sha-256').exists()).toBe(false);
+ expect(findFirstToggleDetailsButton().props('icon')).toBe('angle-down');
+ });
+ });
+
+ describe('file shas', () => {
+ const showShaFiles = () => {
+ findFirstToggleDetailsButton().vm.$emit('click');
+ return nextTick();
+ };
+
+ it.each`
+ selector | title | sha
+ ${'sha-256'} | ${'SHA-256'} | ${'file_sha256'}
+ ${'md5'} | ${'MD5'} | ${'file_md5'}
+ ${'sha-1'} | ${'SHA-1'} | ${'file_sha1'}
+ `('has a $title row', async ({ selector, title, sha }) => {
+ createComponent();
+
+ await showShaFiles();
+
+ expect(findFirstRowShaComponent(selector).props()).toMatchObject({
+ title,
+ sha,
+ });
+ });
+
+ it('does not display a row when the data is missing', async () => {
+ const [{ ...missingMd5 }] = npmFiles;
+ missingMd5.file_md5 = null;
+
+ createComponent({ packageFiles: [missingMd5] });
+
+ await showShaFiles();
+
+ expect(findFirstRowShaComponent('md5').exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/packages/details/store/actions_spec.js b/spec/frontend/packages/details/store/actions_spec.js
index d11ee548b72..b16e50debc4 100644
--- a/spec/frontend/packages/details/store/actions_spec.js
+++ b/spec/frontend/packages/details/store/actions_spec.js
@@ -1,10 +1,18 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
-import { fetchPackageVersions, deletePackage } from '~/packages/details/store/actions';
+import {
+ fetchPackageVersions,
+ deletePackage,
+ deletePackageFile,
+} from '~/packages/details/store/actions';
import * as types from '~/packages/details/store/mutation_types';
-import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants';
+import {
+ DELETE_PACKAGE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+} from '~/packages/shared/constants';
import { npmPackage as packageEntity } from '../../mock_data';
jest.mock('~/flash.js');
@@ -74,7 +82,10 @@ describe('Actions Package details store', () => {
packageEntity.project_id,
packageEntity.id,
);
- expect(createFlash).toHaveBeenCalledWith(FETCH_PACKAGE_VERSIONS_ERROR);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: FETCH_PACKAGE_VERSIONS_ERROR,
+ type: 'warning',
+ });
done();
},
);
@@ -96,7 +107,48 @@ describe('Actions Package details store', () => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
testAction(deletePackage, undefined, { packageEntity }, [], [], () => {
- expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_ERROR_MESSAGE,
+ type: 'warning',
+ });
+ done();
+ });
+ });
+ });
+
+ describe('deletePackageFile', () => {
+ const fileId = 'a_file_id';
+
+ it('should call Api.deleteProjectPackageFile and commit the right data', (done) => {
+ const packageFiles = [{ id: 'foo' }, { id: fileId }];
+ Api.deleteProjectPackageFile = jest.fn().mockResolvedValue();
+ testAction(
+ deletePackageFile,
+ fileId,
+ { packageEntity, packageFiles },
+ [{ type: types.UPDATE_PACKAGE_FILES, payload: [{ id: 'foo' }] }],
+ [],
+ () => {
+ expect(Api.deleteProjectPackageFile).toHaveBeenCalledWith(
+ packageEntity.project_id,
+ packageEntity.id,
+ fileId,
+ );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
+ type: 'success',
+ });
+ done();
+ },
+ );
+ });
+ it('should create flash on API error', (done) => {
+ Api.deleteProjectPackageFile = jest.fn().mockRejectedValue();
+ testAction(deletePackageFile, fileId, { packageEntity }, [], [], () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE,
+ type: 'warning',
+ });
done();
});
});
diff --git a/spec/frontend/packages/details/store/mutations_spec.js b/spec/frontend/packages/details/store/mutations_spec.js
index 6bc5fb7241f..296ed02d786 100644
--- a/spec/frontend/packages/details/store/mutations_spec.js
+++ b/spec/frontend/packages/details/store/mutations_spec.js
@@ -28,4 +28,13 @@ describe('Mutations package details Store', () => {
expect(mockState.packageEntity.versions).toEqual(fakeVersions);
});
});
+ describe('UPDATE_PACKAGE_FILES', () => {
+ it('should update the packageFiles', () => {
+ const files = [1, 2, 3];
+
+ mutations[types.UPDATE_PACKAGE_FILES](mockState, files);
+
+ expect(mockState.packageFiles).toEqual(files);
+ });
+ });
});
diff --git a/spec/frontend/packages/list/stores/actions_spec.js b/spec/frontend/packages/list/stores/actions_spec.js
index 52966c1be5e..adccb7436e1 100644
--- a/spec/frontend/packages/list/stores/actions_spec.js
+++ b/spec/frontend/packages/list/stores/actions_spec.js
@@ -2,7 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { MISSING_DELETE_PATH_ERROR } from '~/packages/list/constants';
import * as actions from '~/packages/list/stores/actions';
import * as types from '~/packages/list/stores/mutation_types';
@@ -241,7 +241,9 @@ describe('Actions Package list store', () => {
`('should reject and createFlash when $property is missing', ({ actionPayload }, done) => {
testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch((e) => {
expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR));
- expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_PACKAGE_ERROR_MESSAGE,
+ });
done();
});
});
diff --git a/spec/frontend/packages/mock_data.js b/spec/frontend/packages/mock_data.js
index 06009daba54..33b47cca68b 100644
--- a/spec/frontend/packages/mock_data.js
+++ b/spec/frontend/packages/mock_data.js
@@ -79,6 +79,9 @@ export const npmFiles = [
pipelines: [
{ id: 1, project: { commit_url: 'http://foo.bar' }, git_commit_message: 'foo bar baz?' },
],
+ file_sha256: 'file_sha256',
+ file_md5: 'file_md5',
+ file_sha1: 'file_sha1',
},
];
@@ -175,6 +178,20 @@ export const composerPackage = {
version: '1.0.0',
};
+export const terraformModule = {
+ created_at: '2015-12-10',
+ id: 2,
+ name: 'Test/system-22',
+ package_type: 'terraform_module',
+ project_path: 'foo/bar/baz',
+ projectPathName: 'foo/bar/baz',
+ project_id: 1,
+ updated_at: '2015-12-10',
+ version: '0.1',
+ versions: [],
+ _links,
+};
+
export const mockTags = [
{
name: 'foo-1',
diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
index f4e617ecafe..b576f1b2553 100644
--- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
@@ -11,7 +11,7 @@ exports[`packages_list_row renders 1`] = `
<!---->
<div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
+ 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"
@@ -42,7 +42,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-fill-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"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/__snapshots__/terraform_installation_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/__snapshots__/terraform_installation_spec.js.snap
new file mode 100644
index 00000000000..427160b45e3
--- /dev/null
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/__snapshots__/terraform_installation_spec.js.snap
@@ -0,0 +1,44 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TerraformInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Provision instructions
+ </h3>
+
+ <code-instruction-stub
+ copytext="Copy Terraform Command"
+ instruction="module \\"Test/system-22\\" {
+ source = \\"foo/Test/system-22\\"
+ version = \\"0.1\\"
+}"
+ label="Copy and paste into your Terraform configuration, insert the variables, and run Terraform init:"
+ multiline="true"
+ trackingaction=""
+ trackinglabel=""
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <code-instruction-stub
+ copytext="Copy Terraform Setup Command"
+ instruction="credentials \\"gitlab.com\\" {
+ token = \\"<TOKEN>\\"
+}"
+ label="To authorize access to the Terraform registry:"
+ multiline="true"
+ trackingaction=""
+ trackinglabel=""
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the Terraform registry, %{linkStart}see our documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details_title_spec.js
new file mode 100644
index 00000000000..87e0059344c
--- /dev/null
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details_title_spec.js
@@ -0,0 +1,93 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { terraformModule, mavenFiles, npmPackage } from 'jest/packages/mock_data';
+import component from '~/packages_and_registries/infrastructure_registry/components/details_title.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('PackageTitle', () => {
+ let wrapper;
+ let store;
+
+ function createComponent({ packageFiles = mavenFiles, packageEntity = terraformModule } = {}) {
+ store = new Vuex.Store({
+ state: {
+ packageEntity,
+ packageFiles,
+ },
+ getters: {
+ packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline,
+ },
+ });
+
+ wrapper = shallowMount(component, {
+ localVue,
+ store,
+ stubs: {
+ TitleArea,
+ },
+ });
+ return wrapper.vm.$nextTick();
+ }
+
+ const findTitleArea = () => wrapper.findComponent(TitleArea);
+ const packageSize = () => wrapper.find('[data-testid="package-size"]');
+ const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
+ const packageRef = () => wrapper.find('[data-testid="package-ref"]');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('module title', () => {
+ it('is correctly bound', async () => {
+ await createComponent();
+
+ expect(findTitleArea().props('title')).toBe(terraformModule.name);
+ });
+ });
+
+ describe('calculates the package size', () => {
+ it('correctly calculates the size', async () => {
+ await createComponent();
+
+ expect(packageSize().props('text')).toBe('300 bytes');
+ });
+ });
+
+ describe('package ref', () => {
+ it('does not display the ref if missing', async () => {
+ await createComponent();
+
+ expect(packageRef().exists()).toBe(false);
+ });
+
+ it('correctly shows the package ref if there is one', async () => {
+ await createComponent({ packageEntity: npmPackage });
+ expect(packageRef().props()).toMatchObject({
+ text: npmPackage.pipeline.ref,
+ icon: 'branch',
+ });
+ });
+ });
+
+ describe('pipeline project', () => {
+ it('does not display the project if missing', async () => {
+ await createComponent();
+
+ expect(pipelineProject().exists()).toBe(false);
+ });
+
+ it('correctly shows the pipeline project if there is one', async () => {
+ await createComponent({ packageEntity: npmPackage });
+
+ expect(pipelineProject().props()).toMatchObject({
+ text: npmPackage.pipeline.project.name,
+ icon: 'review-list',
+ link: npmPackage.pipeline.project.web_url,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/terraform_installation_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/terraform_installation_spec.js
new file mode 100644
index 00000000000..7a129794d54
--- /dev/null
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/terraform_installation_spec.js
@@ -0,0 +1,61 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { terraformModule as packageEntity } from 'jest/packages/mock_data';
+import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/components/terraform_installation.vue';
+import CodeInstructions from '~/vue_shared/components/registry/code_instruction.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('TerraformInstallation', () => {
+ let wrapper;
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ projectPath: 'foo',
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAllComponents(CodeInstructions);
+
+ function createComponent() {
+ wrapper = shallowMount(TerraformInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct command', () => {
+ expect(findCodeInstructions().at(0).props('instruction')).toMatchInlineSnapshot(`
+ "module \\"Test/system-22\\" {
+ source = \\"foo/Test/system-22\\"
+ version = \\"0.1\\"
+ }"
+ `);
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct command', () => {
+ expect(findCodeInstructions().at(1).props('instruction')).toMatchInlineSnapshot(`
+ "credentials \\"gitlab.com\\" {
+ token = \\"<TOKEN>\\"
+ }"
+ `);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index a725941f7f6..8266f9bee89 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -10,6 +10,8 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/packages_and_registries/settings/project/constants';
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
+import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import {
expirationPolicyPayload,
@@ -28,15 +30,19 @@ describe('Registry Settings App', () => {
isAdmin: false,
adminSettingsPath: 'settingsPath',
enableHistoricEntries: false,
+ helpPagePath: 'helpPagePath',
+ showCleanupPolicyOnAlert: false,
};
const findSettingsComponent = () => wrapper.find(SettingsForm);
const findAlert = () => wrapper.find(GlAlert);
+ const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
const mountComponent = (provide = defaultProvidedValues, config) => {
wrapper = shallowMount(component, {
stubs: {
GlSprintf,
+ SettingsBlock,
},
mocks: {
$toast: {
@@ -66,6 +72,26 @@ describe('Registry Settings App', () => {
wrapper.destroy();
});
+ describe('cleanup is on alert', () => {
+ it('exist when showCleanupPolicyOnAlert is true and has the correct props', () => {
+ mountComponent({
+ ...defaultProvidedValues,
+ showCleanupPolicyOnAlert: true,
+ });
+
+ expect(findCleanupAlert().exists()).toBe(true);
+ expect(findCleanupAlert().props()).toMatchObject({
+ projectPath: 'path',
+ });
+ });
+
+ it('is hidden when showCleanupPolicyOnAlert is false', async () => {
+ mountComponent();
+
+ expect(findCleanupAlert().exists()).toBe(false);
+ });
+ });
+
describe('isEdited status', () => {
it.each`
description | apiResponse | workingCopy | result
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap
new file mode 100644
index 00000000000..2cded2ead2e
--- /dev/null
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/cleanup_policy_enabled_alert_spec.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CleanupPolicyEnabledAlert renders 1`] = `
+<gl-alert-stub
+ class="gl-mt-2"
+ dismissible="true"
+ dismisslabel="Dismiss"
+ primarybuttonlink=""
+ primarybuttontext=""
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ title=""
+ variant="info"
+>
+ <gl-sprintf-stub
+ message="Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}"
+ />
+</gl-alert-stub>
+`;
diff --git a/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js b/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js
new file mode 100644
index 00000000000..269e087f5ac
--- /dev/null
+++ b/spec/frontend/packages_and_registries/shared/components/cleanup_policy_enabled_alert_spec.js
@@ -0,0 +1,49 @@
+import { GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import component from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+
+describe('CleanupPolicyEnabledAlert', () => {
+ let wrapper;
+
+ const defaultProps = {
+ projectPath: 'foo',
+ cleanupPoliciesSettingsPath: 'label-bar',
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const mountComponent = (props) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ LocalStorageSync,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders', () => {
+ mountComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('when dismissed is not visible', async () => {
+ mountComponent();
+
+ expect(findAlert().exists()).toBe(true);
+ findAlert().vm.$emit('dismiss');
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
index d22e0474e06..4280a78c202 100644
--- a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import * as flash from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import PromoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
@@ -103,7 +103,7 @@ describe('Promote milestone modal', () => {
wrapper.findComponent(GlModal).vm.$emit('primary');
await waitForPromises();
- expect(flash.deprecatedCreateFlash).toHaveBeenCalledWith(dummyError);
+ expect(createFlash).toHaveBeenCalledWith({ message: dummyError });
});
});
});
diff --git a/spec/frontend/pages/projects/forks/new/components/app_spec.js b/spec/frontend/pages/projects/forks/new/components/app_spec.js
index e1820606704..a7b4b9c42bd 100644
--- a/spec/frontend/pages/projects/forks/new/components/app_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/app_spec.js
@@ -13,6 +13,7 @@ describe('App component', () => {
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
+ restrictedVisibilityLevels: [],
};
const createComponent = (props = {}) => {
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 6d853120232..c80ccfa8256 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -1,4 +1,5 @@
-import { GlFormInputGroup, GlFormInput, GlForm } from '@gitlab/ui';
+import { GlFormInputGroup, GlFormInput, GlForm, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
+import { getByRole, getAllByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -15,6 +16,13 @@ describe('ForkForm component', () => {
let wrapper;
let axiosMock;
+ const PROJECT_VISIBILITY_TYPE = {
+ private:
+ 'Private Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.',
+ internal: 'Internal The project can be accessed by any logged in user.',
+ public: 'Public The project can be accessed without any authentication.',
+ };
+
const GON_GITLAB_URL = 'https://gitlab.com';
const GON_API_VERSION = 'v7';
@@ -37,6 +45,7 @@ describe('ForkForm component', () => {
projectPath: 'project-name',
projectDescription: 'some project description',
projectVisibility: 'private',
+ restrictedVisibilityLevels: [],
};
const mockGetRequest = (data = {}, statusCode = httpStatus.OK) => {
@@ -61,6 +70,8 @@ describe('ForkForm component', () => {
stubs: {
GlFormInputGroup,
GlFormInput,
+ GlFormRadioGroup,
+ GlFormRadio,
},
});
};
@@ -81,6 +92,7 @@ describe('ForkForm component', () => {
axiosMock.restore();
});
+ const findFormSelectOptions = () => wrapper.find('select[name="namespace"]').findAll('option');
const findPrivateRadio = () => wrapper.find('[data-testid="radio-private"]');
const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]');
const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]');
@@ -203,24 +215,145 @@ describe('ForkForm component', () => {
});
describe('visibility level', () => {
+ it('displays the correct description', () => {
+ mockGetRequest();
+ createComponent();
+
+ const formRadios = wrapper.findAll(GlFormRadio);
+
+ Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibilityType, index) => {
+ expect(formRadios.at(index).text()).toBe(PROJECT_VISIBILITY_TYPE[visibilityType]);
+ });
+ });
+
+ it('displays all 3 visibility levels', () => {
+ mockGetRequest();
+ createComponent();
+
+ expect(wrapper.findAll(GlFormRadio)).toHaveLength(3);
+ });
+
+ describe('when the namespace is changed', () => {
+ const namespaces = [
+ {
+ visibility: 'private',
+ },
+ {
+ visibility: 'internal',
+ },
+ {
+ visibility: 'public',
+ },
+ ];
+
+ beforeEach(() => {
+ mockGetRequest();
+ });
+
+ it('resets the visibility to default "private"', async () => {
+ createFullComponent({ projectVisibility: 'public' }, { namespaces });
+
+ expect(wrapper.vm.form.fields.visibility.value).toBe('public');
+ await findFormSelectOptions().at(1).setSelected();
+
+ await wrapper.vm.$nextTick();
+
+ expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true);
+ });
+
+ it('sets the visibility to be null when restrictedVisibilityLevels is set', async () => {
+ createFullComponent({ restrictedVisibilityLevels: [10] }, { namespaces });
+
+ await findFormSelectOptions().at(1).setSelected();
+
+ await wrapper.vm.$nextTick();
+
+ const container = getByRole(wrapper.element, 'radiogroup', { name: /visibility/i });
+ const visibilityRadios = getAllByRole(container, 'radio');
+ expect(visibilityRadios.filter((e) => e.checked)).toHaveLength(0);
+ });
+ });
+
+ it.each`
+ project | restrictedVisibilityLevels
+ ${'private'} | ${[]}
+ ${'internal'} | ${[]}
+ ${'public'} | ${[]}
+ ${'private'} | ${[0]}
+ ${'private'} | ${[10]}
+ ${'private'} | ${[20]}
+ ${'private'} | ${[0, 10]}
+ ${'private'} | ${[0, 20]}
+ ${'private'} | ${[10, 20]}
+ ${'private'} | ${[0, 10, 20]}
+ ${'internal'} | ${[0]}
+ ${'internal'} | ${[10]}
+ ${'internal'} | ${[20]}
+ ${'internal'} | ${[0, 10]}
+ ${'internal'} | ${[0, 20]}
+ ${'internal'} | ${[10, 20]}
+ ${'internal'} | ${[0, 10, 20]}
+ ${'public'} | ${[0]}
+ ${'public'} | ${[10]}
+ ${'public'} | ${[0, 10]}
+ ${'public'} | ${[0, 20]}
+ ${'public'} | ${[10, 20]}
+ ${'public'} | ${[0, 10, 20]}
+ `('checks the correct radio button', async ({ project, restrictedVisibilityLevels }) => {
+ mockGetRequest();
+ createFullComponent({
+ projectVisibility: project,
+ restrictedVisibilityLevels,
+ });
+
+ if (restrictedVisibilityLevels.length === 0) {
+ expect(wrapper.find('[name="visibility"]:checked').attributes('value')).toBe(project);
+ } else {
+ expect(wrapper.find('[name="visibility"]:checked').exists()).toBe(false);
+ }
+ });
+
it.each`
- project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled
- ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
- ${'private'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'}
- ${'private'} | ${'public'} | ${undefined} | ${'true'} | ${'true'}
- ${'internal'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
- ${'internal'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'}
- ${'internal'} | ${'public'} | ${undefined} | ${undefined} | ${'true'}
- ${'public'} | ${'private'} | ${undefined} | ${'true'} | ${'true'}
- ${'public'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'}
- ${'public'} | ${'public'} | ${undefined} | ${undefined} | ${undefined}
+ project | namespace | privateIsDisabled | internalIsDisabled | publicIsDisabled | restrictedVisibilityLevels
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'private'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'private'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'internal'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'} | ${[]}
+ ${'internal'} | ${'public'} | ${undefined} | ${undefined} | ${'true'} | ${[]}
+ ${'public'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[]}
+ ${'public'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'} | ${[]}
+ ${'public'} | ${'public'} | ${undefined} | ${undefined} | ${undefined} | ${[]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[0]}
+ ${'internal'} | ${'internal'} | ${'true'} | ${undefined} | ${'true'} | ${[0]}
+ ${'public'} | ${'public'} | ${'true'} | ${undefined} | ${undefined} | ${[0]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[10]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[10]}
+ ${'public'} | ${'public'} | ${undefined} | ${'true'} | ${undefined} | ${[10]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[20]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${undefined} | ${'true'} | ${[20]}
+ ${'public'} | ${'public'} | ${undefined} | ${undefined} | ${'true'} | ${[20]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[10, 20]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[10, 20]}
+ ${'public'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[10, 20]}
+ ${'private'} | ${'private'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]}
+ ${'internal'} | ${'internal'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]}
+ ${'public'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]}
`(
'sets appropriate radio button disabled state',
- async ({ project, namespace, privateIsDisabled, internalIsDisabled, publicIsDisabled }) => {
+ async ({
+ project,
+ namespace,
+ privateIsDisabled,
+ internalIsDisabled,
+ publicIsDisabled,
+ restrictedVisibilityLevels,
+ }) => {
mockGetRequest();
createComponent(
{
projectVisibility: project,
+ restrictedVisibilityLevels,
},
{
form: { fields: { namespace: { value: { visibility: namespace } } } },
@@ -235,7 +368,7 @@ describe('ForkForm component', () => {
});
describe('onSubmit', () => {
- beforeEach(() => {
+ const setupComponent = (fields = {}) => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
mockGetRequest();
@@ -245,9 +378,14 @@ describe('ForkForm component', () => {
namespaces: MOCK_NAMESPACES_RESPONSE,
form: {
state: true,
+ ...fields,
},
},
);
+ };
+
+ beforeEach(() => {
+ setupComponent();
});
const selectedMockNamespaceIndex = 1;
@@ -279,6 +417,22 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
});
+
+ it('does not make POST request if no visbility is checked', async () => {
+ jest.spyOn(axios, 'post');
+
+ setupComponent({
+ fields: {
+ visibility: {
+ value: null,
+ },
+ },
+ });
+
+ await submitForm();
+
+ expect(axios.post).not.toHaveBeenCalled();
+ });
});
describe('with valid form', () => {
@@ -330,7 +484,7 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
- message: dummyError,
+ message: 'An error occurred while forking the project. Please try again.',
});
});
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
index e7ac837a4c8..9f8dbf3d542 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index c4c48ea7517..4ba9120d196 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -66,7 +66,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<gl-area-chart-stub
annotations=""
data="[object Object]"
- formattooltiptext="function () { [native code] }"
+ formattooltiptext="[Function]"
height="200"
includelegendavgmax="true"
legendaveragetext="Avg"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
index 350669433f0..59b42de2485 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_a_spec.js.snap
@@ -92,6 +92,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
aria-hidden="true"
class="gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
@@ -114,6 +115,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
aria-hidden="true"
class="gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
index c9d8ab4566c..091edc7505c 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_b_spec.js.snap
@@ -81,6 +81,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
aria-hidden="true"
class="gl-text-green-500 gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
@@ -142,6 +143,7 @@ exports[`Learn GitLab Design B renders correctly 1`] = `
aria-hidden="true"
class="gl-text-green-500 gl-icon s16"
data-testid="completed-icon"
+ role="img"
>
<use
href="#check-circle-filled"
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 27cd0fe34bf..de0d70a07d7 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
@@ -27,6 +28,7 @@ describe('Interval Pattern Input Component', () => {
const findAllLabels = () => wrapper.findAll('label');
const findSelectedRadio = () =>
wrapper.findAll('input[type="radio"]').wrappers.find((x) => x.element.checked);
+ const findIcon = () => wrapper.findComponent(GlIcon);
const findSelectedRadioKey = () => findSelectedRadio()?.attributes('data-testid');
const selectEveryDayRadio = () => findEveryDayRadio().trigger('click');
const selectEveryWeekRadio = () => findEveryWeekRadio().trigger('click');
@@ -40,6 +42,11 @@ describe('Interval Pattern Input Component', () => {
wrapper = mount(IntervalPatternInput, {
propsData: { ...props },
+ provide: {
+ glFeatures: {
+ ciDailyLimitForPipelineSchedules: true,
+ },
+ },
data() {
return {
randomHour: data?.hour || mockHour,
@@ -202,4 +209,24 @@ describe('Interval Pattern Input Component', () => {
expect(findSelectedRadioKey()).toBe(customKey);
});
});
+
+ describe('Custom cron syntax quota info', () => {
+ it('the info message includes 5 minutes', () => {
+ createWrapper({ dailyLimit: '288' });
+
+ expect(findIcon().attributes('title')).toContain('5 minutes');
+ });
+
+ it('the info message includes 60 minutes', () => {
+ createWrapper({ dailyLimit: '24' });
+
+ expect(findIcon().attributes('title')).toContain('60 minutes');
+ });
+
+ it('the info message icon is not shown when there is no daily limit', () => {
+ createWrapper();
+
+ expect(findIcon().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
new file mode 100644
index 00000000000..2c8eb8e459f
--- /dev/null
+++ b/spec/frontend/pages/shared/nav/sidebar_tracking_spec.js
@@ -0,0 +1,160 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import { initSidebarTracking } from '~/pages/shared/nav/sidebar_tracking';
+
+describe('~/pages/shared/nav/sidebar_tracking.js', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <aside class="nav-sidebar">
+ <div class="nav-sidebar-inner-scroll">
+ <ul class="sidebar-top-level-items">
+ <li data-track-label="project_information_menu" class="home">
+ <a aria-label="Project information" class="shortcuts-project-information has-sub-items" href="">
+ <span class="nav-icon-container">
+ <svg class="s16" data-testid="project-icon">
+ <use xlink:href="/assets/icons-1b2dadc4c3d49797908ba67b8f10da5d63dd15d859bde28d66fb60bbb97a4dd5.svg#project"></use>
+ </svg>
+ </span>
+ <span class="nav-item-name">Project information</span>
+ </a>
+ <ul class="sidebar-sub-level-items">
+ <li class="fly-out-top-item">
+ <a aria-label="Project information" href="#">
+ <strong class="fly-out-top-item-name">Project information</strong>
+ </a>
+ </li>
+ <li class="divider fly-out-top-item"></li>
+ <li data-track-label="activity" class="">
+ <a aria-label="Activity" class="shortcuts-project-activity" href=#">
+ <span>Activity</span>
+ </a>
+ </li>
+ <li data-track-label="labels" class="">
+ <a aria-label="Labels" href="#">
+ <span>Labels</span>
+ </a>
+ </li>
+ <li data-track-label="members" class="">
+ <a aria-label="Members" href="#">
+ <span>Members</span>
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </aside>
+ `);
+
+ initSidebarTracking();
+ });
+
+ describe('sidebar is not collapsed', () => {
+ describe('menu is not expanded', () => {
+ it('sets the proper data tracking attributes when clicking on menu', () => {
+ const menu = document.querySelector('li[data-track-label="project_information_menu"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('is-over', 'is-showing-fly-out');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+
+ it('sets the proper data tracking attributes when clicking on submenu', () => {
+ const menu = document.querySelector('li[data-track-label="activity"]');
+ const menuLink = menu.querySelector('a');
+ const submenuList = document.querySelector('ul.sidebar-sub-level-items');
+
+ submenuList.classList.add('fly-out-list');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu_item',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+ });
+
+ describe('menu is expanded', () => {
+ it('sets the proper data tracking attributes when clicking on menu', () => {
+ const menu = document.querySelector('li[data-track-label="project_information_menu"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('active');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Expanded',
+ }),
+ });
+ });
+
+ it('sets the proper data tracking attributes when clicking on submenu', () => {
+ const menu = document.querySelector('li[data-track-label="activity"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('active');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu_item',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Expanded',
+ menu_display: 'Expanded',
+ }),
+ });
+ });
+ });
+ });
+
+ describe('sidebar is collapsed', () => {
+ beforeEach(() => {
+ document.querySelector('aside.nav-sidebar').classList.add('js-sidebar-collapsed');
+ });
+
+ it('sets the proper data tracking attributes when clicking on menu', () => {
+ const menu = document.querySelector('li[data-track-label="project_information_menu"]');
+ const menuLink = menu.querySelector('a');
+
+ menu.classList.add('is-over', 'is-showing-fly-out');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Collapsed',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+
+ it('sets the proper data tracking attributes when clicking on submenu', () => {
+ const menu = document.querySelector('li[data-track-label="activity"]');
+ const menuLink = menu.querySelector('a');
+ const submenuList = document.querySelector('ul.sidebar-sub-level-items');
+
+ submenuList.classList.add('fly-out-list');
+ menuLink.click();
+
+ expect(menu.dataset).toMatchObject({
+ trackAction: 'click_menu_item',
+ trackExtra: JSON.stringify({
+ sidebar_display: 'Collapsed',
+ menu_display: 'Fly out',
+ }),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 1cac8ef8ee2..f36d6262b5f 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -2,15 +2,23 @@ import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
+import {
+ WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ CONTENT_EDITOR_LOADED_ACTION,
+ SAVED_USING_CONTENT_EDITOR_ACTION,
+} from '~/pages/shared/wikis/constants';
+
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('WikiForm', () => {
let wrapper;
let mock;
+ let trackingSpy;
const findForm = () => wrapper.find('form');
const findTitle = () => wrapper.find('#wiki_title');
@@ -19,9 +27,11 @@ describe('WikiForm', () => {
const findMessage = () => wrapper.find('#wiki_message');
const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByRole('link', { name: 'Cancel' });
- const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use new editor' });
+ const findUseNewEditorButton = () => wrapper.findByRole('button', { name: 'Use the new editor' });
+ const findDismissContentEditorAlertButton = () =>
+ wrapper.findByRole('button', { name: 'Try this later' });
const findSwitchToOldEditorButton = () =>
- wrapper.findByRole('button', { name: 'Switch to old editor' });
+ wrapper.findByRole('button', { name: 'Switch me back to the classic editor.' });
const findTitleHelpLink = () => wrapper.findByRole('link', { name: 'More Information.' });
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
@@ -55,15 +65,12 @@ describe('WikiForm', () => {
persisted: true,
title: 'My page',
- content: 'My page content',
+ content: ' My page content ',
format: 'markdown',
path: '/project/path/-/wikis/home',
};
- function createWrapper(
- persisted = false,
- { pageInfo, glFeatures } = { glFeatures: { wikiContentEditor: false } },
- ) {
+ function createWrapper(persisted = false, { pageInfo } = {}) {
wrapper = extendedWrapper(
mount(
WikiForm,
@@ -79,7 +86,6 @@ describe('WikiForm', () => {
...(persisted ? pageInfoPersisted : pageInfoNew),
...pageInfo,
},
- glFeatures,
},
},
{ attachToDocument: true },
@@ -88,6 +94,7 @@ describe('WikiForm', () => {
}
beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
mock = new MockAdapter(axios);
});
@@ -124,6 +131,12 @@ describe('WikiForm', () => {
expect(findMessage().element.value).toBe('Update My page');
});
+ it('does not trim page content by default', () => {
+ createWrapper(true);
+
+ expect(findContent().element.value).toBe(' My page content ');
+ });
+
it.each`
value | text
${'markdown'} | ${'[Link Title](page-slug)'}
@@ -178,10 +191,10 @@ describe('WikiForm', () => {
describe('when wiki content is updated', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper(true);
const input = findContent();
- input.setValue('Lorem ipsum dolar sit!');
+ input.setValue(' Lorem ipsum dolar sit! ');
input.element.dispatchEvent(new Event('input'));
return wrapper.vm.$nextTick();
@@ -193,13 +206,25 @@ describe('WikiForm', () => {
expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
- it('when form submitted, unsets before unload warning', async () => {
- triggerFormSubmit();
+ describe('form submit', () => {
+ beforeEach(async () => {
+ triggerFormSubmit();
- await wrapper.vm.$nextTick();
+ await wrapper.vm.$nextTick();
+ });
- const e = dispatchBeforeUnload();
- expect(e.preventDefault).not.toHaveBeenCalled();
+ it('when form submitted, unsets before unload warning', async () => {
+ const e = dispatchBeforeUnload();
+ expect(e.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger tracking event', async () => {
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+
+ it('does not trim page content', () => {
+ expect(findContent().element.value).toBe(' Lorem ipsum dolar sit! ');
+ });
});
});
@@ -251,9 +276,9 @@ describe('WikiForm', () => {
);
});
- describe('when feature flag wikiContentEditor is enabled', () => {
+ describe('wiki content editor', () => {
beforeEach(() => {
- createWrapper(true, { glFeatures: { wikiContentEditor: true } });
+ createWrapper(true);
});
it.each`
@@ -261,7 +286,7 @@ describe('WikiForm', () => {
${'markdown'} | ${true}
${'rdoc'} | ${false}
`(
- 'switch to new editor button exists: $buttonExists if format is $format',
+ 'gl-alert containing "use new editor" button exists: $buttonExists if format is $format',
async ({ format, buttonExists }) => {
setFormat(format);
@@ -271,6 +296,12 @@ describe('WikiForm', () => {
},
);
+ it('gl-alert containing "use new editor" button is dismissed on clicking dismiss button', async () => {
+ await findDismissContentEditorAlertButton().trigger('click');
+
+ expect(findUseNewEditorButton().exists()).toBe(false);
+ });
+
const assertOldEditorIsVisible = () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
@@ -284,7 +315,7 @@ describe('WikiForm', () => {
);
};
- it('shows old editor by default', assertOldEditorIsVisible);
+ it('shows classic editor by default', assertOldEditorIsVisible);
describe('switch format to rdoc', () => {
beforeEach(async () => {
@@ -293,7 +324,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
});
- it('continues to show the old editor', assertOldEditorIsVisible);
+ it('continues to show the classic editor', assertOldEditorIsVisible);
describe('switch format back to markdown', () => {
beforeEach(async () => {
@@ -303,7 +334,7 @@ describe('WikiForm', () => {
});
it(
- 'still shows the old editor and does not automatically switch to the content editor ',
+ 'still shows the classic editor and does not automatically switch to the content editor ',
assertOldEditorIsVisible,
);
});
@@ -328,12 +359,12 @@ describe('WikiForm', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});
- describe('clicking "switch to old editor"', () => {
+ describe('clicking "switch to classic editor"', () => {
beforeEach(() => {
return findSwitchToOldEditorButton().trigger('click');
});
- it('switches to old editor directly without showing a modal', () => {
+ it('switches to classic editor directly without showing a modal', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
});
@@ -351,11 +382,12 @@ describe('WikiForm', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+ it('shows a tip to send feedback', () => {
+ expect(wrapper.text()).toContain('Tell us your experiences with the new Markdown editor');
+ });
+
it('shows warnings that the rich text editor is in beta and may not work properly', () => {
expect(wrapper.text()).toContain(
- "Switching will discard any changes you've made in the new editor.",
- );
- expect(wrapper.text()).toContain(
"This editor is in beta and may not display the page's contents properly.",
);
});
@@ -368,6 +400,15 @@ describe('WikiForm', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(true);
});
+ it('sends tracking event when editor loads', async () => {
+ // wait for content editor to load
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ });
+ });
+
it('disables the format dropdown', () => {
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
@@ -400,9 +441,19 @@ describe('WikiForm', () => {
});
});
+ it('triggers tracking event on form submit', async () => {
+ triggerFormSubmit();
+
+ await wrapper.vm.$nextTick();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
+ label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
+ });
+ });
+
it('updates content from content editor on form submit', async () => {
// old value
- expect(findContent().element.value).toBe('My page content');
+ expect(findContent().element.value).toBe(' My page content ');
// wait for content editor to load
await waitForPromises();
@@ -414,7 +465,7 @@ describe('WikiForm', () => {
expect(findContent().element.value).toBe('hello **world**');
});
- describe('clicking "switch to old editor"', () => {
+ describe('clicking "switch to classic editor"', () => {
let modal;
beforeEach(async () => {
@@ -428,7 +479,7 @@ describe('WikiForm', () => {
expect(modal.vm.show).toHaveBeenCalled();
});
- describe('confirming "switch to old editor" in the modal', () => {
+ describe('confirming "switch to classic editor" in the modal', () => {
beforeEach(async () => {
wrapper.vm.contentEditor.tiptapEditor.commands.setContent(
'<p>hello __world__ from content editor</p>',
@@ -440,7 +491,7 @@ describe('WikiForm', () => {
await wrapper.vm.$nextTick();
});
- it('switches to old editor', () => {
+ it('switches to classic editor', () => {
expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true);
});
@@ -451,8 +502,8 @@ describe('WikiForm', () => {
);
});
- it('the old editor retains its old value and does not use the content from the content editor', () => {
- expect(findContent().element.value).toBe('My page content');
+ it('the classic editor retains its old value and does not use the content from the content editor', () => {
+ expect(findContent().element.value).toBe(' My page content ');
});
});
});
diff --git a/spec/frontend/pages/users/activity_calendar_spec.js b/spec/frontend/pages/users/activity_calendar_spec.js
new file mode 100644
index 00000000000..b33e92e14b2
--- /dev/null
+++ b/spec/frontend/pages/users/activity_calendar_spec.js
@@ -0,0 +1,16 @@
+import { getLevelFromContributions } from '~/pages/users/activity_calendar';
+
+describe('getLevelFromContributions', () => {
+ it.each([
+ [0, 0],
+ [1, 1],
+ [9, 1],
+ [10, 2],
+ [19, 2],
+ [20, 3],
+ [30, 4],
+ [99, 4],
+ ])('.getLevelFromContributions(%i, %i)', (count, expected) => {
+ expect(getLevelFromContributions(count)).toBe(expected);
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
index 9e677425807..39081e07e52 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -118,7 +118,8 @@ describe('Pipeline Editor | Commit section', () => {
});
it('calls the mutation with the CREATE action', () => {
- expect(mockMutate).toHaveBeenCalledTimes(1);
+ // the extra calls are for updating client queries (currentBranch and lastCommitBranch)
+ expect(mockMutate).toHaveBeenCalledTimes(3);
expect(mockMutate).toHaveBeenCalledWith({
mutation: commitCreate,
update: expect.any(Function),
@@ -138,7 +139,7 @@ describe('Pipeline Editor | Commit section', () => {
});
it('calls the mutation with the UPDATE action', () => {
- expect(mockMutate).toHaveBeenCalledTimes(1);
+ expect(mockMutate).toHaveBeenCalledTimes(3);
expect(mockMutate).toHaveBeenCalledWith({
mutation: commitCreate,
update: expect.any(Function),
@@ -158,7 +159,7 @@ describe('Pipeline Editor | Commit section', () => {
});
it('calls the mutation with the current branch', () => {
- expect(mockMutate).toHaveBeenCalledTimes(1);
+ expect(mockMutate).toHaveBeenCalledTimes(3);
expect(mockMutate).toHaveBeenCalledWith({
mutation: commitCreate,
update: expect.any(Function),
@@ -181,7 +182,7 @@ describe('Pipeline Editor | Commit section', () => {
it('a second commit submits the latest sha, keeping the form updated', async () => {
await submitCommit();
- expect(mockMutate).toHaveBeenCalledTimes(2);
+ expect(mockMutate).toHaveBeenCalledTimes(6);
expect(mockMutate).toHaveBeenCalledWith({
mutation: commitCreate,
update: expect.any(Function),
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index 8a4f07c4d88..e435c0dcc08 100644
--- a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -1,13 +1,11 @@
import { getByRole } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
-import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue';
describe('First pipeline card', () => {
let wrapper;
const defaultProvide = {
- ciExamplesHelpPagePath: '/pipelines/examples',
runnerHelpPagePath: '/help/runners',
};
@@ -20,9 +18,9 @@ describe('First pipeline card', () => {
};
const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href;
- const findPipelinesLink = () => getLinkByName(/examples and templates/i);
const findRunnersLink = () => getLinkByName(/make sure your instance has runners available/i);
- const findVisualReference = () => wrapper.findComponent(PipelineVisualReference);
+ const findInstructionsList = () => wrapper.find('ol');
+ const findAllInstructions = () => findInstructionsList().findAll('li');
beforeEach(() => {
createComponent();
@@ -37,11 +35,11 @@ describe('First pipeline card', () => {
});
it('renders the content', () => {
- expect(findVisualReference().exists()).toBe(true);
+ expect(findInstructionsList().exists()).toBe(true);
+ expect(findAllInstructions()).toHaveLength(3);
});
- it('renders the links', () => {
+ it('renders the link', () => {
expect(findRunnersLink()).toContain(defaultProvide.runnerHelpPagePath);
- expect(findPipelinesLink()).toContain(defaultProvide.ciExamplesHelpPagePath);
});
});
diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js b/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js
deleted file mode 100644
index e4834544484..00000000000
--- a/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue';
-import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue';
-
-describe('Demo job pill', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMount(PipelineVisualReference);
- };
-
- const findAllDemoJobPills = () => wrapper.findAllComponents(DemoJobPill);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders all stage names', () => {
- expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.build);
- expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.test);
- expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.deploy);
- });
-
- it('renders all job pills', () => {
- expect(findAllDemoJobPills()).toHaveLength(4);
- });
-});
diff --git a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
index 7a5b01fb04a..6f9245e39aa 100644
--- a/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/editor/text_editor_spec.js
@@ -9,6 +9,7 @@ import {
mockCommitSha,
mockProjectPath,
mockProjectNamespace,
+ mockDefaultBranch,
} from '../../mock_data';
describe('Pipeline Editor | Text editor component', () => {
@@ -32,12 +33,14 @@ describe('Pipeline Editor | Text editor component', () => {
},
};
- const createComponent = (opts = {}, mountFn = shallowMount) => {
+ const createComponent = (glFeatures = {}, mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
provide: {
projectPath: mockProjectPath,
projectNamespace: mockProjectNamespace,
ciConfigPath: mockCiConfigPath,
+ defaultBranch: mockDefaultBranch,
+ glFeatures,
},
attrs: {
value: mockCiYml,
@@ -54,7 +57,6 @@ describe('Pipeline Editor | Text editor component', () => {
stubs: {
EditorLite: MockEditorLite,
},
- ...opts,
});
};
@@ -66,7 +68,6 @@ describe('Pipeline Editor | Text editor component', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
mockUse.mockClear();
mockRegisterCiSchema.mockClear();
@@ -100,25 +101,37 @@ describe('Pipeline Editor | Text editor component', () => {
});
});
- describe('register CI schema', () => {
- beforeEach(async () => {
- createComponent();
-
- // Since the editor will have already mounted, the event will have fired.
- // To ensure we properly test this, we clear the mock and re-remit the event.
- mockRegisterCiSchema.mockClear();
- mockUse.mockClear();
+ describe('CI schema', () => {
+ describe('when `schema_linting` feature flag is on', () => {
+ beforeEach(() => {
+ createComponent({ schemaLinting: true });
+ // Since the editor will have already mounted, the event will have fired.
+ // To ensure we properly test this, we clear the mock and re-remit the event.
+ mockRegisterCiSchema.mockClear();
+ mockUse.mockClear();
+ findEditor().vm.$emit(EDITOR_READY_EVENT);
+ });
- findEditor().vm.$emit(EDITOR_READY_EVENT);
+ it('configures editor with syntax highlight', () => {
+ expect(mockUse).toHaveBeenCalledTimes(1);
+ expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
+ expect(mockRegisterCiSchema).toHaveBeenCalledWith({
+ projectNamespace: mockProjectNamespace,
+ projectPath: mockProjectPath,
+ ref: mockCommitSha,
+ });
+ });
});
- it('configures editor with syntax highlight', async () => {
- expect(mockUse).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledWith({
- projectNamespace: mockProjectNamespace,
- projectPath: mockProjectPath,
- ref: mockCommitSha,
+ describe('when `schema_linting` feature flag is off', () => {
+ beforeEach(() => {
+ createComponent();
+ findEditor().vm.$emit(EDITOR_READY_EVENT);
+ });
+
+ it('does not call the register CI schema function', () => {
+ expect(mockUse).not.toHaveBeenCalled();
+ expect(mockRegisterCiSchema).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
index d6763a7de41..e731ad8695e 100644
--- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -11,7 +11,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
-import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql';
+import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.graphql';
import {
mockBranchPaginationLimit,
mockDefaultBranch,
@@ -22,6 +22,7 @@ import {
mockTotalBranches,
mockTotalBranchResults,
mockTotalSearchResults,
+ mockNewBranch,
} from '../../mock_data';
const localVue = createLocalVue();
@@ -31,9 +32,12 @@ describe('Pipeline editor branch switcher', () => {
let wrapper;
let mockApollo;
let mockAvailableBranchQuery;
+ let mockCurrentBranchQuery;
+ let mockLastCommitBranchQuery;
const createComponent = (
- { isQueryLoading, mountFn, options } = {
+ { currentBranch, isQueryLoading, mountFn, options } = {
+ currentBranch: mockDefaultBranch,
isQueryLoading: false,
mountFn: shallowMount,
options: {},
@@ -58,8 +62,8 @@ describe('Pipeline editor branch switcher', () => {
},
data() {
return {
- branches: ['main'],
- currentBranch: mockDefaultBranch,
+ availableBranches: ['main'],
+ currentBranch,
};
},
...options,
@@ -67,8 +71,18 @@ describe('Pipeline editor branch switcher', () => {
};
const createComponentWithApollo = (mountFn = shallowMount) => {
- const handlers = [[getAvailableBranches, mockAvailableBranchQuery]];
- mockApollo = createMockApollo(handlers);
+ const handlers = [[getAvailableBranchesQuery, mockAvailableBranchQuery]];
+ const resolvers = {
+ Query: {
+ currentBranch() {
+ return mockCurrentBranchQuery();
+ },
+ lastCommitBranch() {
+ return mockLastCommitBranchQuery();
+ },
+ },
+ };
+ mockApollo = createMockApollo(handlers, resolvers);
createComponent({
mountFn,
@@ -76,11 +90,6 @@ describe('Pipeline editor branch switcher', () => {
localVue,
apolloProvider: mockApollo,
mocks: {},
- data() {
- return {
- currentBranch: mockDefaultBranch,
- };
- },
},
});
};
@@ -90,15 +99,40 @@ describe('Pipeline editor branch switcher', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll);
+ const defaultBranchInDropdown = () => findDropdownItems().at(0);
+
+ const setMockResolvedValues = ({ availableBranches, currentBranch, lastCommitBranch }) => {
+ if (availableBranches) {
+ mockAvailableBranchQuery.mockResolvedValue(availableBranches);
+ }
+
+ if (currentBranch) {
+ mockCurrentBranchQuery.mockResolvedValue(currentBranch);
+ }
+
+ mockLastCommitBranchQuery.mockResolvedValue(lastCommitBranch || '');
+ };
beforeEach(() => {
mockAvailableBranchQuery = jest.fn();
+ mockCurrentBranchQuery = jest.fn();
+ mockLastCommitBranchQuery = jest.fn();
});
afterEach(() => {
wrapper.destroy();
});
+ const testErrorHandling = () => {
+ expect(wrapper.emitted('showError')).toBeDefined();
+ expect(wrapper.emitted('showError')[0]).toEqual([
+ {
+ reasons: [wrapper.vm.$options.i18n.fetchError],
+ type: DEFAULT_FAILURE,
+ },
+ ]);
+ };
+
describe('when querying for the first time', () => {
beforeEach(() => {
createComponentWithApollo();
@@ -111,7 +145,10 @@ describe('Pipeline editor branch switcher', () => {
describe('after querying', () => {
beforeEach(async () => {
- mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
+ setMockResolvedValues({
+ availableBranches: mockProjectBranches,
+ currentBranch: mockDefaultBranch,
+ });
createComponentWithApollo(mount);
await waitForPromises();
});
@@ -126,10 +163,8 @@ describe('Pipeline editor branch switcher', () => {
});
it('renders current branch with a check mark', () => {
- const defaultBranchInDropdown = findDropdownItems().at(0);
-
- expect(defaultBranchInDropdown.text()).toBe(mockDefaultBranch);
- expect(defaultBranchInDropdown.props('isChecked')).toBe(true);
+ expect(defaultBranchInDropdown().text()).toBe(mockDefaultBranch);
+ expect(defaultBranchInDropdown().props('isChecked')).toBe(true);
});
it('does not render check mark for other branches', () => {
@@ -142,7 +177,10 @@ describe('Pipeline editor branch switcher', () => {
describe('on fetch error', () => {
beforeEach(async () => {
- mockAvailableBranchQuery.mockResolvedValue(new Error());
+ setMockResolvedValues({
+ availableBranches: new Error(),
+ currentBranch: mockDefaultBranch,
+ });
createComponentWithApollo();
await waitForPromises();
});
@@ -152,20 +190,17 @@ describe('Pipeline editor branch switcher', () => {
});
it('shows an error message', () => {
- expect(wrapper.emitted('showError')).toBeDefined();
- expect(wrapper.emitted('showError')[0]).toEqual([
- {
- reasons: [wrapper.vm.$options.i18n.fetchError],
- type: DEFAULT_FAILURE,
- },
- ]);
+ testErrorHandling();
});
});
describe('when switching branches', () => {
beforeEach(async () => {
jest.spyOn(window.history, 'pushState').mockImplementation(() => {});
- mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
+ setMockResolvedValues({
+ availableBranches: mockProjectBranches,
+ currentBranch: mockDefaultBranch,
+ });
createComponentWithApollo(mount);
await waitForPromises();
});
@@ -212,14 +247,32 @@ describe('Pipeline editor branch switcher', () => {
describe('when searching', () => {
beforeEach(async () => {
- mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
+ setMockResolvedValues({
+ availableBranches: mockProjectBranches,
+ currentBranch: mockDefaultBranch,
+ });
createComponentWithApollo(mount);
await waitForPromises();
+ });
+
+ afterEach(() => {
+ mockAvailableBranchQuery.mockClear();
+ });
+
+ it('shows error message on fetch error', async () => {
+ mockAvailableBranchQuery.mockResolvedValue(new Error());
+
+ findSearchBox().vm.$emit('input', 'te');
+ await waitForPromises();
- mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
+ testErrorHandling();
});
describe('with a search term', () => {
+ beforeEach(async () => {
+ mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
+ });
+
it('calls query with correct variables', async () => {
findSearchBox().vm.$emit('input', 'te');
await waitForPromises();
@@ -253,6 +306,7 @@ describe('Pipeline editor branch switcher', () => {
describe('without a search term', () => {
beforeEach(async () => {
+ mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
findSearchBox().vm.$emit('input', 'te');
await waitForPromises();
@@ -296,7 +350,10 @@ describe('Pipeline editor branch switcher', () => {
describe('when scrolling to the bottom of the list', () => {
beforeEach(async () => {
- mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
+ setMockResolvedValues({
+ availableBranches: mockProjectBranches,
+ currentBranch: mockDefaultBranch,
+ });
createComponentWithApollo();
await waitForPromises();
});
@@ -326,6 +383,15 @@ describe('Pipeline editor branch switcher', () => {
searchPattern: '*',
});
});
+
+ it('shows error message on fetch error', async () => {
+ mockAvailableBranchQuery.mockResolvedValue(new Error());
+
+ findInfiniteScroll().vm.$emit('bottomReached');
+ await waitForPromises();
+
+ testErrorHandling();
+ });
});
describe('when search term exists', () => {
@@ -343,4 +409,35 @@ describe('Pipeline editor branch switcher', () => {
});
});
});
+
+ describe('when committing a new branch', () => {
+ const createNewBranch = async () => {
+ setMockResolvedValues({
+ currentBranch: mockNewBranch,
+ lastCommitBranch: mockNewBranch,
+ });
+ await wrapper.vm.$apollo.queries.currentBranch.refetch();
+ await wrapper.vm.$apollo.queries.lastCommitBranch.refetch();
+ };
+
+ beforeEach(async () => {
+ setMockResolvedValues({
+ availableBranches: mockProjectBranches,
+ currentBranch: mockDefaultBranch,
+ });
+ createComponentWithApollo(mount);
+ await waitForPromises();
+ await createNewBranch();
+ });
+
+ it('sets new branch as current branch', () => {
+ expect(defaultBranchInDropdown().text()).toBe(mockNewBranch);
+ expect(defaultBranchInDropdown().props('isChecked')).toBe(true);
+ });
+
+ it('adds new branch to branch switcher', () => {
+ expect(defaultBranchInDropdown().text()).toBe(mockNewBranch);
+ expect(findDropdownItems()).toHaveLength(mockTotalBranchResults + 1);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index e08fce3ceb9..cadcdf6ae2e 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -5,6 +5,7 @@ export const mockProjectNamespace = 'user1';
export const mockProjectPath = 'project1';
export const mockProjectFullPath = `${mockProjectNamespace}/${mockProjectPath}`;
export const mockDefaultBranch = 'main';
+export const mockNewBranch = 'new-branch';
export const mockNewMergeRequestPath = '/-/merge_requests/new';
export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
diff --git a/spec/frontend/pipelines/components/dag/mock_data.js b/spec/frontend/pipelines/components/dag/mock_data.js
index e7e93804195..f27e7cf3d6b 100644
--- a/spec/frontend/pipelines/components/dag/mock_data.js
+++ b/spec/frontend/pipelines/components/dag/mock_data.js
@@ -398,6 +398,8 @@ export const multiNote = {
},
};
+export const missingJob = 'missing_job';
+
/*
It is important that the base include parallel jobs
as well as non-parallel jobs with spaces in the name to prevent
@@ -657,4 +659,16 @@ export const mockParsedGraphQLNodes = [
],
__typename: 'CiGroup',
},
+ {
+ category: 'production',
+ name: 'production_e',
+ size: 1,
+ jobs: [
+ {
+ name: 'production_e',
+ needs: [missingJob],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
];
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 4914a9a1ced..bb7e27b5ec2 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
+import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
import {
IID_FAILURE,
LAYER_VIEW,
@@ -17,7 +18,6 @@ import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
-import getUserCallouts from '~/pipelines/graphql/queries/get_user_callouts.query.graphql';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
const defaultProvide = {
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 96f2cd1e371..c7d95526a0c 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -14,6 +14,7 @@ describe('Linked pipeline', () => {
let wrapper;
const findButton = () => wrapper.find(GlButton);
+ const findDownstreamPipelineTitle = () => wrapper.find('[data-testid="downstream-title"]');
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
@@ -119,6 +120,11 @@ describe('Linked pipeline', () => {
expect(findPipelineLabel().exists()).toBe(true);
});
+ it('should have the name of the trigger job on the card when it is a child pipeline', () => {
+ createWrapper(downstreamProps);
+ expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.source_job.name);
+ });
+
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
createWrapper(upstreamProps);
expect(findPipelineLabel().exists()).toBe(true);
diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
index c67b91ae190..16c28791514 100644
--- a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
+++ b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
@@ -21,3 +21,10 @@ exports[`Links Inner component with one need matches snapshot and has expected p
<path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
</svg> </div>"
`;
+
+exports[`Links Inner component with same stage needs matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118L32,118C62,118,62,128,92,128\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index bb1f0965469..8f39c8c2405 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -10,6 +10,7 @@ import {
pipelineData,
pipelineDataWithNoNeeds,
rootRect,
+ sameStageNeeds,
} from '../pipeline_graph/mock_data';
describe('Links Inner component', () => {
@@ -40,7 +41,7 @@ describe('Links Inner component', () => {
// We create fixture so that each job has an empty div that represent
// the JobPill in the DOM. Each `JobPill` would have different coordinates,
- // so we increment their coordinates on each iteration to simulat different positions.
+ // so we increment their coordinates on each iteration to simulate different positions.
const setFixtures = ({ stages }) => {
const jobs = createJobsHash(stages);
const arrayOfJobs = Object.keys(jobs);
@@ -81,7 +82,6 @@ describe('Links Inner component', () => {
afterEach(() => {
jest.restoreAllMocks();
wrapper.destroy();
- wrapper = null;
});
describe('basic SVG creation', () => {
@@ -160,6 +160,25 @@ describe('Links Inner component', () => {
});
});
+ describe('with same stage needs', () => {
+ beforeEach(() => {
+ setFixtures(sameStageNeeds);
+ createComponent({ pipelineData: sameStageNeeds.stages });
+ });
+
+ it('renders the correct number of links', () => {
+ expect(findAllLinksPath()).toHaveLength(2);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
describe('with a large number of needs', () => {
beforeEach(() => {
setFixtures(largePipelineData);
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 57d846c53c8..31f0e72c279 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -7,7 +7,9 @@ import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.
import {
mockCancelledPipelineHeader,
mockFailedPipelineHeader,
+ mockFailedPipelineNoPermissions,
mockRunningPipelineHeader,
+ mockRunningPipelineNoPermissions,
mockSuccessfulPipelineHeader,
} from './mock_data';
@@ -168,5 +170,19 @@ describe('Pipeline details header', () => {
});
});
});
+
+ describe('Permissions', () => {
+ it('should not display the cancel action if user does not have permission', () => {
+ wrapper = createComponent(mockRunningPipelineNoPermissions);
+
+ expect(findCancelButton().exists()).toBe(false);
+ });
+
+ it('should not display the retry action if user does not have permission', () => {
+ wrapper = createComponent(mockFailedPipelineNoPermissions);
+
+ expect(findRetryButton().exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 16f15b20824..7e3c3727c9d 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -10,6 +10,7 @@ export const mockPipelineHeader = {
id: 123,
userPermissions: {
destroyPipeline: true,
+ updatePipeline: true,
},
createdAt: threeWeeksAgo.toISOString(),
user: {
@@ -34,6 +35,31 @@ export const mockFailedPipelineHeader = {
},
};
+export const mockFailedPipelineNoPermissions = {
+ id: 123,
+ userPermissions: {
+ destroyPipeline: false,
+ updatePipeline: false,
+ },
+ createdAt: threeWeeksAgo.toISOString(),
+ user: {
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatarUrl: 'link',
+ },
+ status: PIPELINE_RUNNING,
+ retryable: true,
+ cancelable: false,
+ detailedStatus: {
+ group: 'running',
+ icon: 'status_running',
+ label: 'running',
+ text: 'running',
+ detailsPath: 'path',
+ },
+};
+
export const mockRunningPipelineHeader = {
...mockPipelineHeader,
status: PIPELINE_RUNNING,
@@ -48,6 +74,31 @@ export const mockRunningPipelineHeader = {
},
};
+export const mockRunningPipelineNoPermissions = {
+ id: 123,
+ userPermissions: {
+ destroyPipeline: false,
+ updatePipeline: false,
+ },
+ createdAt: threeWeeksAgo.toISOString(),
+ user: {
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatarUrl: 'link',
+ },
+ status: PIPELINE_RUNNING,
+ retryable: false,
+ cancelable: true,
+ detailedStatus: {
+ group: 'running',
+ icon: 'status_running',
+ label: 'running',
+ text: 'running',
+ detailsPath: 'path',
+ },
+};
+
export const mockCancelledPipelineHeader = {
...mockPipelineHeader,
status: PIPELINE_CANCELED,
diff --git a/spec/frontend/pipelines/notification/pipeline_notification_spec.js b/spec/frontend/pipelines/notification/pipeline_notification_spec.js
deleted file mode 100644
index 79aa337ba9d..00000000000
--- a/spec/frontend/pipelines/notification/pipeline_notification_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { GlBanner } from '@gitlab/ui';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import PipelineNotification from '~/pipelines/components/notification/pipeline_notification.vue';
-import getUserCallouts from '~/pipelines/graphql/queries/get_user_callouts.query.graphql';
-
-describe('Pipeline notification', () => {
- const localVue = createLocalVue();
-
- let wrapper;
- const dagDocPath = 'my/dag/path';
-
- const createWrapper = (apolloProvider) => {
- return shallowMount(PipelineNotification, {
- localVue,
- provide: {
- dagDocPath,
- },
- apolloProvider,
- });
- };
-
- const createWrapperWithApollo = async ({ callouts = [], isLoading = false } = {}) => {
- localVue.use(VueApollo);
-
- const mappedCallouts = callouts.map((callout) => {
- return { featureName: callout, __typename: 'UserCallout' };
- });
-
- const mockCalloutsResponse = {
- data: {
- currentUser: {
- id: 45,
- __typename: 'User',
- callouts: {
- id: 5,
- __typename: 'UserCalloutConnection',
- nodes: mappedCallouts,
- },
- },
- },
- };
- const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse);
- const requestHandlers = [[getUserCallouts, getUserCalloutsHandler]];
-
- const apolloWrapper = createWrapper(createMockApollo(requestHandlers));
- if (!isLoading) {
- await nextTick();
- }
-
- return apolloWrapper;
- };
-
- const findBanner = () => wrapper.findComponent(GlBanner);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('shows the banner if the user has never seen it', async () => {
- wrapper = await createWrapperWithApollo({ callouts: ['random'] });
-
- expect(findBanner().exists()).toBe(true);
- });
-
- it('does not show the banner while the user callout query is loading', async () => {
- wrapper = await createWrapperWithApollo({ callouts: ['random'], isLoading: true });
-
- expect(findBanner().exists()).toBe(false);
- });
-
- it('does not show the banner if the user has previously dismissed it', async () => {
- wrapper = await createWrapperWithApollo({ callouts: ['pipeline_needs_banner'.toUpperCase()] });
-
- expect(findBanner().exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/pipelines/parsing_utils_spec.js b/spec/frontend/pipelines/parsing_utils_spec.js
index 96748ae9e5c..074009ae056 100644
--- a/spec/frontend/pipelines/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/parsing_utils_spec.js
@@ -10,7 +10,7 @@ import {
getMaxNodes,
} from '~/pipelines/components/parsing_utils';
-import { mockParsedGraphQLNodes } from './components/dag/mock_data';
+import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
describe('DAG visualization parsing utilities', () => {
@@ -24,6 +24,12 @@ describe('DAG visualization parsing utilities', () => {
expect(unfilteredLinks[0]).toHaveProperty('target', 'test_a');
expect(unfilteredLinks[0]).toHaveProperty('value', 10);
});
+
+ it('does not generate a link for non-existing jobs', () => {
+ const sources = unfilteredLinks.map(({ source }) => source);
+
+ expect(sources.includes(missingJob)).toBe(false);
+ });
});
describe('filterByAncestors', () => {
@@ -88,7 +94,7 @@ describe('DAG visualization parsing utilities', () => {
These lengths are determined by the mock data.
If the data changes, the numbers may also change.
*/
- expect(parsed.nodes).toHaveLength(21);
+ expect(parsed.nodes).toHaveLength(mockParsedGraphQLNodes.length);
expect(cleanedNodes).toHaveLength(12);
});
});
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
index a79917bfd48..db77e0a0573 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -162,6 +162,38 @@ export const parallelNeedData = {
],
};
+export const sameStageNeeds = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build', name: 'build_1' }],
+ },
+ ],
+ },
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_2',
+ jobs: [{ script: 'yarn test', stage: 'build', needs: ['build_1'] }],
+ },
+ ],
+ },
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_3',
+ jobs: [{ script: 'yarn test', stage: 'build', needs: ['build_2'] }],
+ },
+ ],
+ },
+ ],
+};
+
export const largePipelineData = {
stages: [
{
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
index 070d3bf7dac..5816bc06fe3 100644
--- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
@@ -111,6 +111,28 @@ describe('utils functions', () => {
});
});
+ it('removes needs which are not in the data', () => {
+ const inexistantJobName = 'job5';
+ const jobsWithNeeds = {
+ [jobName1]: job1,
+ [jobName2]: job2,
+ [jobName3]: job3,
+ [jobName4]: {
+ name: jobName4,
+ script: 'echo deploy',
+ stage: 'deploy',
+ needs: [inexistantJobName],
+ },
+ };
+
+ expect(generateJobNeedsDict(jobsWithNeeds)).toEqual({
+ [jobName1]: [],
+ [jobName2]: [],
+ [jobName3]: [jobName1, jobName2],
+ [jobName4]: [],
+ });
+ });
+
it('handles parallel jobs by adding the group name as a need', () => {
const size = 3;
const jobOptimize1 = 'optimize_1';
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index f9b59c5dc48..874ecbccf82 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -7,8 +7,8 @@ import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import { getExperimentVariant } from '~/experimentation/utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { getExperimentData, getExperimentVariant } from '~/experimentation/utils';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
@@ -23,6 +23,7 @@ import { stageReply, users, mockSearch, branches } from './mock_data';
jest.mock('~/flash');
jest.mock('~/experimentation/utils', () => ({
...jest.requireActual('~/experimentation/utils'),
+ getExperimentData: jest.fn().mockReturnValue(false),
getExperimentVariant: jest.fn().mockReturnValue('control'),
}));
@@ -48,6 +49,7 @@ describe('Pipelines', () => {
resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: `${mockProjectPath}/pipelines/new`,
codeQualityPagePath: `${mockProjectPath}/-/new/master?commit_message=Add+.gitlab-ci.yml+and+create+a+code+quality+job&file_name=.gitlab-ci.yml&template=Code-Quality`,
+ ciRunnerSettingsPath: `${mockProjectPath}/-/settings/ci_cd#js-runners-settings`,
};
const noPermissions = {
@@ -349,7 +351,7 @@ describe('Pipelines', () => {
it('displays a warning message if raw text search is used', () => {
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ expect(createFlash).toHaveBeenCalledWith({ message: RAW_TEXT_WARNING, type: 'warning' });
});
it('should update browser bar', () => {
@@ -563,6 +565,7 @@ describe('Pipelines', () => {
describe('when the code_quality_walkthrough experiment is active', () => {
beforeAll(() => {
+ getExperimentData.mockImplementation((name) => name === 'code_quality_walkthrough');
getExperimentVariant.mockReturnValue('candidate');
});
@@ -574,6 +577,29 @@ describe('Pipelines', () => {
});
});
+ describe('when the ci_runner_templates experiment is active', () => {
+ beforeAll(() => {
+ getExperimentData.mockImplementation((name) => name === 'ci_runner_templates');
+ getExperimentVariant.mockReturnValue('candidate');
+ });
+
+ it('renders two buttons', () => {
+ expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
+ expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
+ 'Install GitLab Runners',
+ );
+ expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
+ paths.ciRunnerSettingsPath,
+ );
+ expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
+ 'Learn about Runners',
+ );
+ expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
+ '/help/ci/quick_start/index.md',
+ );
+ });
+ });
+
it('does not render filtered search', () => {
expect(findFilteredSearch().exists()).toBe(false);
});
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index 6258b08dfbb..e931ddb8496 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index a3d7b63373c..42adefcd0bb 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -2,7 +2,7 @@ import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import UpdateUsername from '~/profile/account/components/update_username.vue';
@@ -146,7 +146,9 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
- expect(createFlash).toBeCalledWith('Invalid username');
+ expect(createFlash).toBeCalledWith({
+ message: 'Invalid username',
+ });
});
it("shows a fallback error message if the error response doesn't have a `message` property", async () => {
@@ -156,9 +158,9 @@ describe('UpdateUsername component', () => {
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
- expect(createFlash).toBeCalledWith(
- 'An error occurred while updating your username, please try again.',
- );
+ expect(createFlash).toBeCalledWith({
+ message: 'An error occurred while updating your username, please try again.',
+ });
});
});
});
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 9688cb47799..0c8089430d0 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -159,12 +159,7 @@ describe('CommitFormModal', () => {
});
it('Changes the target_project_id input value', async () => {
- createComponent(
- shallowMount,
- {},
- { glFeatures: { pickIntoProject: true } },
- { isCherryPick: true },
- );
+ createComponent(shallowMount, {}, {}, { isCherryPick: true });
findProjectsDropdown().vm.$emit('selectProject', '_changed_project_value_');
await wrapper.vm.$nextTick();
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index e2c993b8395..fdb12640b26 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import actions from '~/projects/commits/store/actions';
import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
@@ -39,7 +39,9 @@ describe('Project commits actions', () => {
actions.receiveAuthorsError(mockDispatchContext);
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith('An error occurred fetching the project authors.');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'An error occurred fetching the project authors.',
+ });
});
});
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 40e31e24a14..b41b5028736 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
@@ -69,7 +69,7 @@ describe('ProtectedBranchEdit', () => {
expect(mock.history.patch).toHaveLength(1);
expect(toggle).not.toBeDisabled();
- expect(flash).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
}));
});
@@ -81,7 +81,7 @@ describe('ProtectedBranchEdit', () => {
it('flashes error', () =>
axios.waitForAll().then(() => {
- expect(flash).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
}));
});
});
diff --git a/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
index 4be4fce1abf..f80e2ce6ecc 100644
--- a/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -26,6 +26,7 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
aria-hidden="true"
class="gl-icon s8"
data-testid="angle-right-icon"
+ role="img"
>
<use
href="#angle-right"
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index dc9063bde2c..c8fcb3116cd 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -313,10 +313,10 @@ describe('tags list row', () => {
});
describe.each`
- name | finderFunction | text | icon | clipboard
- ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 01:29 GMT+0000 on 2020-11-03'} | ${'clock'} | ${false}
- ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true}
- ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true}
+ name | finderFunction | text | icon | clipboard
+ ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 01:29 UTC on 2020-11-03'} | ${'clock'} | ${false}
+ ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true}
+ ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true}
`('$name details row', ({ finderFunction, text, icon, clipboard }) => {
it(`has ${text} as text`, async () => {
mountComponent();
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index 48acc06792d..b58a53f0af2 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
+import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
@@ -43,21 +44,22 @@ describe('List Page', () => {
let wrapper;
let apolloProvider;
- const findDeleteModal = () => wrapper.find(GlModal);
- const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findCliCommands = () => wrapper.find(CliCommands);
- const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
- const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
- const findRegistryHeader = () => wrapper.find(RegistryHeader);
+ const findCliCommands = () => wrapper.findComponent(CliCommands);
+ const findProjectEmptyState = () => wrapper.findComponent(ProjectEmptyState);
+ const findGroupEmptyState = () => wrapper.findComponent(GroupEmptyState);
+ const findRegistryHeader = () => wrapper.findComponent(RegistryHeader);
- const findDeleteAlert = () => wrapper.find(GlAlert);
- const findImageList = () => wrapper.find(ImageList);
- const findRegistrySearch = () => wrapper.find(RegistrySearch);
+ const findDeleteAlert = () => wrapper.findComponent(GlAlert);
+ const findImageList = () => wrapper.findComponent(ImageList);
+ const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
- const findDeleteImage = () => wrapper.find(DeleteImage);
+ const findDeleteImage = () => wrapper.findComponent(DeleteImage);
+ const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
const waitForApolloRequestRender = async () => {
jest.runOnlyPendingTimers();
@@ -560,4 +562,33 @@ describe('List Page', () => {
},
);
});
+
+ describe('cleanup is on alert', () => {
+ it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => {
+ mountComponent({
+ config: {
+ showCleanupPolicyOnAlert: true,
+ projectPath: 'foo',
+ isGroupPage: false,
+ cleanupPoliciesSettingsPath: 'bar',
+ },
+ });
+
+ await waitForApolloRequestRender();
+
+ expect(findCleanupAlert().exists()).toBe(true);
+ expect(findCleanupAlert().props()).toMatchObject({
+ projectPath: 'foo',
+ cleanupPoliciesSettingsPath: 'bar',
+ });
+ });
+
+ it('is hidden when showCleanupPolicyOnAlert is false', async () => {
+ mountComponent();
+
+ await waitForApolloRequestRender();
+
+ expect(findCleanupAlert().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/related_merge_requests/store/actions_spec.js b/spec/frontend/related_merge_requests/store/actions_spec.js
index a14096388e6..3bd07c34b6f 100644
--- a/spec/frontend/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/related_merge_requests/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/related_merge_requests/store/actions';
import * as types from '~/related_merge_requests/store/mutation_types';
@@ -100,7 +100,9 @@ describe('RelatedMergeRequest store actions', () => {
[{ type: 'requestData' }, { type: 'receiveDataError' }],
() => {
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expect.stringMatching('Something went wrong'),
+ });
done();
},
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index cad593b76ea..e0a1343c39c 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -5,6 +5,58 @@ Object {
"data": Array [
Object {
"_links": Object {
+ "closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.2&scope=all&state=closed",
+ "closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=closed",
+ "editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.2/edit",
+ "mergedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=merged",
+ "openedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.2&scope=all&state=opened",
+ "openedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.2&scope=all&state=opened",
+ "self": "http://localhost/releases-namespace/releases-project/-/releases/v1.2",
+ "selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.2",
+ },
+ "assets": Object {
+ "count": 4,
+ "links": Array [],
+ "sources": Array [
+ Object {
+ "format": "zip",
+ "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.zip",
+ },
+ Object {
+ "format": "tar.gz",
+ "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.gz",
+ },
+ Object {
+ "format": "tar.bz2",
+ "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar.bz2",
+ },
+ Object {
+ "format": "tar",
+ "url": "http://localhost/releases-namespace/releases-project/-/archive/v1.2/releases-project-v1.2.tar",
+ },
+ ],
+ },
+ "author": Object {
+ "avatarUrl": "https://www.gravatar.com/avatar/16f8e2050ce10180ca571c2eb19cfce2?s=80&d=identicon",
+ "username": "administrator",
+ "webUrl": "http://localhost/administrator",
+ },
+ "commit": Object {
+ "shortId": "b83d6e39",
+ "title": "Merge branch 'branch-merged' into 'master'",
+ },
+ "commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
+ "descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>",
+ "evidences": Array [],
+ "milestones": Array [],
+ "name": "The second release",
+ "releasedAt": "2019-01-10T00:00:00Z",
+ "tagName": "v1.2",
+ "tagPath": "/releases-namespace/releases-project/-/tags/v1.2",
+ "upcomingRelease": true,
+ },
+ Object {
+ "_links": Object {
"closedIssuesUrl": "http://localhost/releases-namespace/releases-project/-/issues?release_tag=v1.1&scope=all&state=closed",
"closedMergeRequestsUrl": "http://localhost/releases-namespace/releases-project/-/merge_requests?release_tag=v1.1&scope=all&state=closed",
"editUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/edit",
@@ -121,10 +173,10 @@ Object {
},
],
"paginationInfo": Object {
- "endCursor": "eyJpZCI6IjEifQ",
+ "endCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTgtMTItMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMSJ9",
"hasNextPage": false,
"hasPreviousPage": false,
- "startCursor": "eyJpZCI6IjEifQ",
+ "startCursor": "eyJyZWxlYXNlZF9hdCI6IjIwMTktMDEtMTAgMDA6MDA6MDAuMDAwMDAwMDAwIFVUQyIsImlkIjoiMiJ9",
},
}
`;
diff --git a/spec/frontend/releases/components/app_index_apollo_client_spec.js b/spec/frontend/releases/components/app_index_apollo_client_spec.js
new file mode 100644
index 00000000000..002d8939058
--- /dev/null
+++ b/spec/frontend/releases/components/app_index_apollo_client_spec.js
@@ -0,0 +1,394 @@
+import { cloneDeep } from 'lodash';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createFlash from '~/flash';
+import { historyPushState } from '~/lib/utils/common_utils';
+import ReleasesIndexApolloClientApp from '~/releases/components/app_index_apollo_client.vue';
+import ReleaseBlock from '~/releases/components/release_block.vue';
+import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
+import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
+import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
+import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
+import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
+import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+let mockQueryParams;
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ getParameterByName: jest
+ .fn()
+ .mockImplementation((parameterName) => mockQueryParams[parameterName]),
+ historyPushState: jest.fn(),
+}));
+
+describe('app_index_apollo_client.vue', () => {
+ const originalAllReleasesQueryResponse = getJSONFixture(
+ 'graphql/releases/graphql/queries/all_releases.query.graphql.json',
+ );
+ const projectPath = 'project/path';
+ const newReleasePath = 'path/to/new/release/page';
+ const before = 'beforeCursor';
+ const after = 'afterCursor';
+
+ let wrapper;
+ let allReleases;
+ let singleRelease;
+ let noReleases;
+ let queryMock;
+
+ const createComponent = ({
+ singleResponse = Promise.resolve(singleRelease),
+ fullResponse = Promise.resolve(allReleases),
+ } = {}) => {
+ const apolloProvider = createMockApollo([
+ [
+ allReleasesQuery,
+ queryMock.mockImplementation((vars) => {
+ return vars.first === 1 ? singleResponse : fullResponse;
+ }),
+ ],
+ ]);
+
+ wrapper = shallowMountExtended(ReleasesIndexApolloClientApp, {
+ apolloProvider,
+ provide: {
+ newReleasePath,
+ projectPath,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockQueryParams = {};
+
+ allReleases = cloneDeep(originalAllReleasesQueryResponse);
+
+ singleRelease = cloneDeep(originalAllReleasesQueryResponse);
+ singleRelease.data.project.releases.nodes.splice(
+ 1,
+ singleRelease.data.project.releases.nodes.length,
+ );
+
+ noReleases = cloneDeep(originalAllReleasesQueryResponse);
+ noReleases.data.project.releases.nodes = [];
+
+ queryMock = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ // Finders
+ const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
+ const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
+ const findNewReleaseButton = () =>
+ wrapper.findByText(ReleasesIndexApolloClientApp.i18n.newRelease);
+ const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
+ const findPagination = () => wrapper.findComponent(ReleasesPaginationApolloClient);
+ const findSort = () => wrapper.findComponent(ReleasesSortApolloClient);
+
+ // Tests
+ describe('component states', () => {
+ // These need to be defined as functions, since `singleRelease` and
+ // `allReleases` are generated in a `beforeEach`, and therefore
+ // aren't available at test definition time.
+ const getInProgressResponse = () => new Promise(() => {});
+ const getErrorResponse = () => Promise.reject(new Error('Oops!'));
+ const getSingleRequestLoadedResponse = () => Promise.resolve(singleRelease);
+ const getFullRequestLoadedResponse = () => Promise.resolve(allReleases);
+ const getLoadedEmptyResponse = () => Promise.resolve(noReleases);
+
+ const toDescription = (bool) => (bool ? 'does' : 'does not');
+
+ describe.each`
+ description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
+ ${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false}
+ ${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'both requests loaded with no results'} | ${getLoadedEmptyResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
+ ${'single request loading, full request loaded'} | ${getInProgressResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'single request loading, full request failed'} | ${getInProgressResponse} | ${getErrorResponse} | ${true} | ${false} | ${true} | ${0} | ${false}
+ ${'single request loaded, full request loading'} | ${getSingleRequestLoadedResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${1} | ${false}
+ ${'single request loaded, full request failed'} | ${getSingleRequestLoadedResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${1} | ${false}
+ ${'single request failed, full request loading'} | ${getErrorResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'single request failed, full request loaded'} | ${getErrorResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
+ ${'single request loaded with no results, full request loading'} | ${getLoadedEmptyResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
+ ${'single request loading, full request loadied with no results'} | ${getInProgressResponse} | ${getLoadedEmptyResponse} | ${false} | ${true} | ${false} | ${0} | ${false}
+ `(
+ '$description',
+ ({
+ singleResponseFn,
+ fullResponseFn,
+ loadingIndicator,
+ emptyState,
+ flashMessage,
+ releaseCount,
+ pagination,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ singleResponse: singleResponseFn(),
+ fullResponse: fullResponseFn(),
+ });
+ });
+
+ it(`${toDescription(loadingIndicator)} render a loading indicator`, () => {
+ expect(findLoadingIndicator().exists()).toBe(loadingIndicator);
+ });
+
+ it(`${toDescription(emptyState)} render an empty state`, () => {
+ expect(findEmptyState().exists()).toBe(emptyState);
+ });
+
+ it(`${toDescription(flashMessage)} show a flash message`, () => {
+ if (flashMessage) {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: ReleasesIndexApolloClientApp.i18n.errorMessage,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ } else {
+ expect(createFlash).not.toHaveBeenCalled();
+ }
+ });
+
+ it(`renders ${releaseCount} release(s)`, () => {
+ expect(findAllReleaseBlocks()).toHaveLength(releaseCount);
+ });
+
+ it(`${toDescription(pagination)} render the pagination controls`, () => {
+ expect(findPagination().exists()).toBe(pagination);
+ });
+
+ it('does render the "New release" button', () => {
+ expect(findNewReleaseButton().exists()).toBe(true);
+ });
+
+ it('does render the sort controls', () => {
+ expect(findSort().exists()).toBe(true);
+ });
+ },
+ );
+ });
+
+ describe('URL parameters', () => {
+ describe('when the URL contains no query parameters', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+
+ describe('when the URL contains a "before" query parameter', () => {
+ beforeEach(() => {
+ mockQueryParams = { before };
+ createComponent();
+ });
+
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(1);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ before,
+ last: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+
+ describe('when the URL contains an "after" query parameter', () => {
+ beforeEach(() => {
+ mockQueryParams = { after };
+ createComponent();
+ });
+
+ it('makes a request with the correct GraphQL query parameters', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+
+ describe('when the URL contains both "before" and "after" query parameters', () => {
+ beforeEach(() => {
+ mockQueryParams = { before, after };
+ createComponent();
+ });
+
+ it('ignores the "before" parameter and behaves as if only the "after" parameter was provided', () => {
+ expect(queryMock).toHaveBeenCalledTimes(2);
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: 1,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+
+ expect(queryMock).toHaveBeenCalledWith({
+ after,
+ first: PAGE_SIZE,
+ fullPath: projectPath,
+ sort: DEFAULT_SORT,
+ });
+ });
+ });
+ });
+
+ describe('New release button', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the new release button with the correct href', () => {
+ expect(findNewReleaseButton().attributes().href).toBe(newReleasePath);
+ });
+ });
+
+ describe('pagination', () => {
+ beforeEach(() => {
+ mockQueryParams = { before };
+ createComponent();
+ });
+
+ it('requeries the GraphQL endpoint when a pagination button is clicked', async () => {
+ expect(queryMock.mock.calls).toEqual([[expect.objectContaining({ before })]]);
+
+ mockQueryParams = { after };
+ findPagination().vm.$emit('next', after);
+
+ await wrapper.vm.$nextTick();
+
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ before })],
+ [expect.objectContaining({ after })],
+ [expect.objectContaining({ after })],
+ ]);
+ });
+ });
+
+ describe('sorting', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it(`sorts by ${DEFAULT_SORT} by default`, () => {
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ ]);
+ });
+
+ it('requeries the GraphQL endpoint and updates the URL when the sort is changed', async () => {
+ findSort().vm.$emit('input', CREATED_ASC);
+
+ await wrapper.vm.$nextTick();
+
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: CREATED_ASC })],
+ [expect.objectContaining({ sort: CREATED_ASC })],
+ ]);
+
+ // URL manipulation is tested in more detail in the `describe` block below
+ expect(historyPushState).toHaveBeenCalled();
+ });
+
+ it('does not requery the GraphQL endpoint or update the URL if the sort is updated to the same value', async () => {
+ findSort().vm.$emit('input', DEFAULT_SORT);
+
+ await wrapper.vm.$nextTick();
+
+ expect(queryMock.mock.calls).toEqual([
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ [expect.objectContaining({ sort: DEFAULT_SORT })],
+ ]);
+
+ expect(historyPushState).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('sorting + pagination interaction', () => {
+ const nonPaginationQueryParam = 'nonPaginationQueryParam';
+
+ beforeEach(() => {
+ historyPushState.mockImplementation((newUrl) => {
+ mockQueryParams = Object.fromEntries(new URL(newUrl).searchParams);
+ });
+ });
+
+ describe.each`
+ queryParamsBefore | paramName | paramInitialValue
+ ${{ before, nonPaginationQueryParam }} | ${'before'} | ${before}
+ ${{ after, nonPaginationQueryParam }} | ${'after'} | ${after}
+ `(
+ 'when the URL contains a "$paramName" pagination cursor',
+ ({ queryParamsBefore, paramName, paramInitialValue }) => {
+ beforeEach(async () => {
+ mockQueryParams = queryParamsBefore;
+ createComponent();
+
+ findSort().vm.$emit('input', CREATED_ASC);
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it(`resets the page's "${paramName}" pagination cursor when the sort is changed`, () => {
+ const firstRequestVariables = queryMock.mock.calls[0][0];
+ // Might be request #2 or #3, depending on the pagination direction
+ const mostRecentRequestVariables =
+ queryMock.mock.calls[queryMock.mock.calls.length - 1][0];
+
+ expect(firstRequestVariables[paramName]).toBe(paramInitialValue);
+ expect(mostRecentRequestVariables[paramName]).toBeUndefined();
+ });
+
+ it(`updates the URL to not include the "${paramName}" URL query parameter`, () => {
+ expect(historyPushState).toHaveBeenCalledTimes(1);
+
+ const updatedUrlQueryParams = Object.fromEntries(
+ new URL(historyPushState.mock.calls[0][0]).searchParams,
+ );
+
+ expect(updatedUrlQueryParams[paramName]).toBeUndefined();
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/releases/components/releases_empty_state_spec.js b/spec/frontend/releases/components/releases_empty_state_spec.js
new file mode 100644
index 00000000000..495e6d863f7
--- /dev/null
+++ b/spec/frontend/releases/components/releases_empty_state_spec.js
@@ -0,0 +1,56 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
+
+describe('releases_empty_state.vue', () => {
+ const documentationPath = 'path/to/releases/documentation';
+ const illustrationPath = 'path/to/releases/empty/state/illustration';
+
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ReleasesEmptyState, {
+ provide: {
+ documentationPath,
+ illustrationPath,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ 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',
+ }),
+ );
+ });
+});
diff --git a/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js b/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js
new file mode 100644
index 00000000000..a538afd5d38
--- /dev/null
+++ b/spec/frontend/releases/components/releases_pagination_apollo_client_spec.js
@@ -0,0 +1,126 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { historyPushState } from '~/lib/utils/common_utils';
+import ReleasesPaginationApolloClient from '~/releases/components/releases_pagination_apollo_client.vue';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ historyPushState: jest.fn(),
+}));
+
+describe('releases_pagination_apollo_client.vue', () => {
+ const startCursor = 'startCursor';
+ const endCursor = 'endCursor';
+ let wrapper;
+ let onPrev;
+ let onNext;
+
+ const createComponent = (pageInfo) => {
+ onPrev = jest.fn();
+ onNext = jest.fn();
+
+ wrapper = mountExtended(ReleasesPaginationApolloClient, {
+ propsData: {
+ pageInfo,
+ },
+ listeners: {
+ prev: onPrev,
+ next: onNext,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const singlePageInfo = {
+ hasPreviousPage: false,
+ hasNextPage: false,
+ startCursor,
+ endCursor,
+ };
+
+ const onlyNextPageInfo = {
+ hasPreviousPage: false,
+ hasNextPage: true,
+ startCursor,
+ endCursor,
+ };
+
+ const onlyPrevPageInfo = {
+ hasPreviousPage: true,
+ hasNextPage: false,
+ startCursor,
+ endCursor,
+ };
+
+ const prevAndNextPageInfo = {
+ hasPreviousPage: true,
+ hasNextPage: true,
+ startCursor,
+ endCursor,
+ };
+
+ const findPrevButton = () => wrapper.findByTestId('prevButton');
+ const findNextButton = () => wrapper.findByTestId('nextButton');
+
+ describe.each`
+ description | pageInfo | prevEnabled | nextEnabled
+ ${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false}
+ ${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true}
+ ${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false}
+ ${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true}
+ `('component states', ({ description, pageInfo, prevEnabled, nextEnabled }) => {
+ describe(description, () => {
+ beforeEach(() => {
+ createComponent(pageInfo);
+ });
+
+ it(`renders the "Prev" button as ${prevEnabled ? 'enabled' : 'disabled'}`, () => {
+ expect(findPrevButton().attributes().disabled).toBe(prevEnabled ? undefined : 'disabled');
+ });
+
+ it(`renders the "Next" button as ${nextEnabled ? 'enabled' : 'disabled'}`, () => {
+ expect(findNextButton().attributes().disabled).toBe(nextEnabled ? undefined : 'disabled');
+ });
+ });
+ });
+
+ describe('button behavior', () => {
+ beforeEach(() => {
+ createComponent(prevAndNextPageInfo);
+ });
+
+ describe('next button behavior', () => {
+ beforeEach(() => {
+ findNextButton().trigger('click');
+ });
+
+ it('emits an "next" event with the "after" cursor', () => {
+ expect(onNext.mock.calls).toEqual([[endCursor]]);
+ });
+
+ it('calls historyPushState with the new URL', () => {
+ expect(historyPushState.mock.calls).toEqual([
+ [expect.stringContaining(`?after=${endCursor}`)],
+ ]);
+ });
+ });
+
+ describe('prev button behavior', () => {
+ beforeEach(() => {
+ findPrevButton().trigger('click');
+ });
+
+ it('emits an "prev" event with the "before" cursor', () => {
+ expect(onPrev.mock.calls).toEqual([[startCursor]]);
+ });
+
+ it('calls historyPushState with the new URL', () => {
+ expect(historyPushState.mock.calls).toEqual([
+ [expect.stringContaining(`?before=${startCursor}`)],
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/releases_sort_apollo_client_spec.js b/spec/frontend/releases/components/releases_sort_apollo_client_spec.js
new file mode 100644
index 00000000000..d93a932af01
--- /dev/null
+++ b/spec/frontend/releases/components/releases_sort_apollo_client_spec.js
@@ -0,0 +1,103 @@
+import { GlSorting, GlSortingItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ReleasesSortApolloClient from '~/releases/components/releases_sort_apollo_client.vue';
+import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
+
+describe('releases_sort_apollo_client.vue', () => {
+ let wrapper;
+
+ const createComponent = (valueProp = RELEASED_AT_ASC) => {
+ wrapper = shallowMountExtended(ReleasesSortApolloClient, {
+ propsData: {
+ value: valueProp,
+ },
+ stubs: {
+ GlSortingItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSorting = () => wrapper.findComponent(GlSorting);
+ const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
+ const findReleasedDateItem = () =>
+ findSortingItems().wrappers.find((item) => item.text() === 'Released date');
+ const findCreatedDateItem = () =>
+ findSortingItems().wrappers.find((item) => item.text() === 'Created date');
+ const getSortingItemsInfo = () =>
+ findSortingItems().wrappers.map((item) => ({
+ label: item.text(),
+ active: item.attributes().active === 'true',
+ }));
+
+ describe.each`
+ valueProp | text | isAscending | items
+ ${RELEASED_AT_ASC} | ${'Released date'} | ${true} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
+ ${RELEASED_AT_DESC} | ${'Released date'} | ${false} | ${[{ label: 'Released date', active: true }, { label: 'Created date', active: false }]}
+ ${CREATED_ASC} | ${'Created date'} | ${true} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
+ ${CREATED_DESC} | ${'Created date'} | ${false} | ${[{ label: 'Released date', active: false }, { label: 'Created date', active: true }]}
+ `('component states', ({ valueProp, text, isAscending, items }) => {
+ beforeEach(() => {
+ createComponent(valueProp);
+ });
+
+ it(`when the sort is ${valueProp}, provides the GlSorting with the props text="${text}" and isAscending=${isAscending}`, () => {
+ expect(findSorting().props()).toEqual(
+ expect.objectContaining({
+ text,
+ isAscending,
+ }),
+ );
+ });
+
+ it(`when the sort is ${valueProp}, renders the expected dropdown items`, () => {
+ expect(getSortingItemsInfo()).toEqual(items);
+ });
+ });
+
+ const clickReleasedDateItem = () => findReleasedDateItem().vm.$emit('click');
+ const clickCreatedDateItem = () => findCreatedDateItem().vm.$emit('click');
+ const clickSortDirectionButton = () => findSorting().vm.$emit('sortDirectionChange');
+
+ const releasedAtDropdownItemDescription = 'released at dropdown item';
+ const createdAtDropdownItemDescription = 'created at dropdown item';
+ const sortDirectionButtonDescription = 'sort direction button';
+
+ describe.each`
+ initialValueProp | itemClickFn | itemToClickDescription | emittedEvent
+ ${RELEASED_AT_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined}
+ ${RELEASED_AT_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_ASC}
+ ${RELEASED_AT_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_DESC}
+ ${RELEASED_AT_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${undefined}
+ ${RELEASED_AT_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${CREATED_DESC}
+ ${RELEASED_AT_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${RELEASED_AT_ASC}
+ ${CREATED_ASC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_ASC}
+ ${CREATED_ASC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined}
+ ${CREATED_ASC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_DESC}
+ ${CREATED_DESC} | ${clickReleasedDateItem} | ${releasedAtDropdownItemDescription} | ${RELEASED_AT_DESC}
+ ${CREATED_DESC} | ${clickCreatedDateItem} | ${createdAtDropdownItemDescription} | ${undefined}
+ ${CREATED_DESC} | ${clickSortDirectionButton} | ${sortDirectionButtonDescription} | ${CREATED_ASC}
+ `('input event', ({ initialValueProp, itemClickFn, itemToClickDescription, emittedEvent }) => {
+ beforeEach(() => {
+ createComponent(initialValueProp);
+ itemClickFn();
+ });
+
+ it(`emits ${
+ emittedEvent || 'nothing'
+ } when value prop is ${initialValueProp} and the ${itemToClickDescription} is clicked`, () => {
+ expect(wrapper.emitted().input?.[0]?.[0]).toEqual(emittedEvent);
+ });
+ });
+
+ describe('prop validation', () => {
+ it('validates that the `value` prop is one of the expected sort strings', () => {
+ expect(() => {
+ createComponent('not a valid value');
+ }).toThrow('Invalid prop: custom validator check failed');
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 688ec4c0a50..6504a09df2f 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql';
@@ -151,9 +151,9 @@ describe('Release edit/new actions', () => {
it(`shows a flash message`, () => {
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- 'Something went wrong while getting the release details.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Something went wrong while getting the release details.',
+ });
});
});
});
@@ -352,9 +352,9 @@ describe('Release edit/new actions', () => {
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- 'Something went wrong while creating a new release.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Something went wrong while creating a new release.',
+ });
});
});
});
@@ -483,9 +483,9 @@ describe('Release edit/new actions', () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- 'Something went wrong while saving the release details.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Something went wrong while saving the release details.',
+ });
});
});
@@ -503,9 +503,9 @@ describe('Release edit/new actions', () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- 'Something went wrong while saving the release details.',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'Something went wrong while saving the release details.',
+ });
});
};
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
index 1b83d071d17..9dda024bffd 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -20,6 +20,9 @@ describe('Codequality Reports actions', () => {
it('should commit SET_PATHS mutation', (done) => {
const paths = {
basePath: 'basePath',
+ headPath: 'headPath',
+ baseBlobPath: 'baseBlobPath',
+ headBlobPath: 'headBlobPath',
reportsPath: 'reportsPath',
helpPath: 'codequalityHelpPath',
};
diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/reports/codequality_report/store/mutations_spec.js
index 9d4c05afd36..8bc6bb26c2a 100644
--- a/spec/frontend/reports/codequality_report/store/mutations_spec.js
+++ b/spec/frontend/reports/codequality_report/store/mutations_spec.js
@@ -13,16 +13,25 @@ describe('Codequality Reports mutations', () => {
describe('SET_PATHS', () => {
it('sets paths to given values', () => {
const basePath = 'base.json';
+ const headPath = 'head.json';
+ const baseBlobPath = 'base/blob/path/';
+ const headBlobPath = 'head/blob/path/';
const reportsPath = 'reports.json';
const helpPath = 'help.html';
mutations.SET_PATHS(localState, {
basePath,
+ headPath,
+ baseBlobPath,
+ headBlobPath,
reportsPath,
helpPath,
});
expect(localState.basePath).toEqual(basePath);
+ expect(localState.headPath).toEqual(headPath);
+ expect(localState.baseBlobPath).toEqual(baseBlobPath);
+ expect(localState.headBlobPath).toEqual(headBlobPath);
expect(localState.reportsPath).toEqual(reportsPath);
expect(localState.helpPath).toEqual(helpPath);
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index f03df8cf2ac..495039b4ccb 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -5,6 +5,7 @@ import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobHeaderEdit from '~/repository/components/blob_header_edit.vue';
+import BlobReplace from '~/repository/components/blob_replace.vue';
let wrapper;
const simpleMockData = {
@@ -75,10 +76,11 @@ const factory = createFactory(shallowMount);
const fullFactory = createFactory(mount);
describe('Blob content viewer component', () => {
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findBlobHeader = () => wrapper.find(BlobHeader);
- const findBlobHeaderEdit = () => wrapper.find(BlobHeaderEdit);
- const findBlobContent = () => wrapper.find(BlobContent);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findBlobHeader = () => wrapper.findComponent(BlobHeader);
+ const findBlobHeaderEdit = () => wrapper.findComponent(BlobHeaderEdit);
+ const findBlobContent = () => wrapper.findComponent(BlobContent);
+ const findBlobReplace = () => wrapper.findComponent(BlobReplace);
afterEach(() => {
wrapper.destroy();
@@ -162,15 +164,23 @@ describe('Blob content viewer component', () => {
});
describe('BlobHeader action slot', () => {
- it('renders BlobHeaderEdit button in simple viewer', async () => {
+ const { ideEditPath, editBlobPath } = simpleMockData;
+
+ it('renders BlobHeaderEdit buttons in simple viewer', async () => {
fullFactory({
mockData: { blobInfo: simpleMockData },
stubs: {
BlobContent: true,
+ BlobReplace: true,
},
});
+
await nextTick();
- expect(findBlobHeaderEdit().props('editPath')).toEqual('some_file.js/edit');
+
+ expect(findBlobHeaderEdit().props()).toMatchObject({
+ editPath: editBlobPath,
+ webIdePath: ideEditPath,
+ });
});
it('renders BlobHeaderEdit button in rich viewer', async () => {
@@ -178,10 +188,55 @@ describe('Blob content viewer component', () => {
mockData: { blobInfo: richMockData },
stubs: {
BlobContent: true,
+ BlobReplace: true,
},
});
+
await nextTick();
- expect(findBlobHeaderEdit().props('editPath')).toEqual('some_file.js/edit');
+
+ expect(findBlobHeaderEdit().props()).toMatchObject({
+ editPath: editBlobPath,
+ webIdePath: ideEditPath,
+ });
+ });
+
+ describe('BlobReplace', () => {
+ const { name, path } = simpleMockData;
+
+ it('renders component', async () => {
+ window.gon.current_user_id = 1;
+
+ fullFactory({
+ mockData: { blobInfo: simpleMockData },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+
+ await nextTick();
+
+ expect(findBlobReplace().props()).toMatchObject({
+ name,
+ path,
+ });
+ });
+
+ it('does not render if not logged in', async () => {
+ window.gon.current_user_id = null;
+
+ fullFactory({
+ mockData: { blobInfo: simpleMockData },
+ stubs: {
+ BlobContent: true,
+ BlobReplace: true,
+ },
+ });
+
+ await nextTick();
+
+ expect(findBlobReplace().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/repository/components/blob_header_edit_spec.js b/spec/frontend/repository/components/blob_header_edit_spec.js
new file mode 100644
index 00000000000..c0eb7c523c4
--- /dev/null
+++ b/spec/frontend/repository/components/blob_header_edit_spec.js
@@ -0,0 +1,82 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import BlobHeaderEdit from '~/repository/components/blob_header_edit.vue';
+import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+
+const DEFAULT_PROPS = {
+ editPath: 'some_file.js/edit',
+ webIdePath: 'some_file.js/ide/edit',
+};
+
+describe('BlobHeaderEdit component', () => {
+ let wrapper;
+
+ const createComponent = (consolidatedEditButton = false, props = {}) => {
+ wrapper = shallowMount(BlobHeaderEdit, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ consolidatedEditButton,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findButtons = () => wrapper.findAll(GlButton);
+ const findEditButton = () => findButtons().at(0);
+ const findWebIdeButton = () => findButtons().at(1);
+ const findWebIdeLink = () => wrapper.find(WebIdeLink);
+
+ it('renders component', () => {
+ createComponent();
+
+ const { editPath, webIdePath } = DEFAULT_PROPS;
+
+ expect(wrapper.props()).toMatchObject({
+ editPath,
+ webIdePath,
+ });
+ });
+
+ it('renders both buttons', () => {
+ createComponent();
+
+ expect(findButtons()).toHaveLength(2);
+ });
+
+ it('renders the Edit button', () => {
+ createComponent();
+
+ expect(findEditButton().attributes('href')).toBe(DEFAULT_PROPS.editPath);
+ expect(findEditButton().text()).toBe('Edit');
+ expect(findEditButton()).not.toBeDisabled();
+ });
+
+ it('renders the Web IDE button', () => {
+ createComponent();
+
+ expect(findWebIdeButton().attributes('href')).toBe(DEFAULT_PROPS.webIdePath);
+ expect(findWebIdeButton().text()).toBe('Web IDE');
+ expect(findWebIdeButton()).not.toBeDisabled();
+ });
+
+ it('renders WebIdeLink component', () => {
+ createComponent(true);
+
+ const { editPath: editUrl, webIdePath: webIdeUrl } = DEFAULT_PROPS;
+
+ expect(findWebIdeLink().props()).toMatchObject({
+ editUrl,
+ webIdeUrl,
+ isBlob: true,
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/blob_replace_spec.js b/spec/frontend/repository/components/blob_replace_spec.js
new file mode 100644
index 00000000000..4a6f147da22
--- /dev/null
+++ b/spec/frontend/repository/components/blob_replace_spec.js
@@ -0,0 +1,67 @@
+import { shallowMount } from '@vue/test-utils';
+import BlobReplace from '~/repository/components/blob_replace.vue';
+import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
+
+const DEFAULT_PROPS = {
+ name: 'some name',
+ path: 'some/path',
+ canPushCode: true,
+ replacePath: 'some/replace/path',
+};
+
+const DEFAULT_INJECT = {
+ targetBranch: 'master',
+ originalBranch: 'master',
+};
+
+describe('BlobReplace component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(BlobReplace, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ provide: {
+ ...DEFAULT_INJECT,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
+
+ it('renders component', () => {
+ createComponent();
+
+ const { name, path } = DEFAULT_PROPS;
+
+ expect(wrapper.props()).toMatchObject({
+ name,
+ path,
+ });
+ });
+
+ it('renders UploadBlobModal', () => {
+ createComponent();
+
+ const { targetBranch, originalBranch } = DEFAULT_INJECT;
+ const { name, path, canPushCode, replacePath } = DEFAULT_PROPS;
+ const title = `Replace ${name}`;
+
+ expect(findUploadBlobModal().props()).toMatchObject({
+ modalTitle: title,
+ commitMessage: title,
+ targetBranch,
+ originalBranch,
+ canPushCode,
+ path,
+ replacePath,
+ primaryBtnText: 'Replace file',
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 6ba6f993db1..da28c9873d9 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -1,5 +1,6 @@
import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import TableRow from '~/repository/components/table/row.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
@@ -18,6 +19,10 @@ function factory(propsData = {}) {
name: propsData.path,
projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
+ totalEntries: 10,
+ },
+ directives: {
+ GlHoverLoad: createMockDirective(),
},
provide: {
glFeatures: { refactorBlobViewer: true },
@@ -34,6 +39,8 @@ function factory(propsData = {}) {
}
describe('Repository table row component', () => {
+ const findRouterLink = () => vm.find(RouterLinkStub);
+
afterEach(() => {
vm.destroy();
});
@@ -81,6 +88,21 @@ describe('Repository table row component', () => {
});
});
+ it('renders a gl-hover-load directive', () => {
+ factory({
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'blob',
+ currentPath: '/',
+ });
+
+ const hoverLoadDirective = getBinding(findRouterLink().element, 'gl-hover-load');
+
+ expect(hoverLoadDirective).not.toBeUndefined();
+ expect(hoverLoadDirective.value).toBeInstanceOf(Function);
+ });
+
it.each`
type | component | componentName
${'tree'} | ${RouterLinkStub} | ${'RouterLink'}
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 2930e39df8a..d397bc185e2 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,7 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
-import TreeContent, { INITIAL_FETCH_COUNT } from '~/repository/components/tree_content.vue';
+import TreeContent from '~/repository/components/tree_content.vue';
+import { TREE_INITIAL_FETCH_COUNT } from '~/repository/constants';
let vm;
let $apollo;
@@ -128,7 +129,7 @@ describe('Repository table component', () => {
it('has limit of 1000 files on initial load', () => {
factory('/');
- expect(INITIAL_FETCH_COUNT * vm.vm.pageSize).toBe(1000);
+ expect(TREE_INITIAL_FETCH_COUNT * vm.vm.pageSize).toBe(1000);
});
});
});
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index ec85d5666fb..d93b1d7e5f1 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -200,4 +200,84 @@ describe('UploadBlobModal', () => {
});
},
);
+
+ describe('blob file submission type', () => {
+ const submitForm = async () => {
+ wrapper.vm.uploadFile = jest.fn();
+ wrapper.vm.replaceFile = jest.fn();
+ wrapper.vm.submitForm();
+ await wrapper.vm.$nextTick();
+ };
+
+ const submitRequest = async () => {
+ mock = new MockAdapter(axios);
+ findModal().vm.$emit('primary', mockEvent);
+ await waitForPromises();
+ };
+
+ describe('upload blob file', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the default "Upload New File" modal title ', () => {
+ expect(findModal().props('title')).toBe('Upload New File');
+ });
+
+ it('display the defaul primary button text', () => {
+ expect(findModal().props('actionPrimary').text).toBe('Upload file');
+ });
+
+ it('calls the default uploadFile when the form submit', async () => {
+ await submitForm();
+
+ expect(wrapper.vm.uploadFile).toHaveBeenCalled();
+ expect(wrapper.vm.replaceFile).not.toHaveBeenCalled();
+ });
+
+ it('makes a POST request', async () => {
+ await submitRequest();
+
+ expect(mock.history.put).toHaveLength(0);
+ expect(mock.history.post).toHaveLength(1);
+ });
+ });
+
+ describe('replace blob file', () => {
+ const modalTitle = 'Replace foo.js';
+ const replacePath = 'replace-path';
+ const primaryBtnText = 'Replace file';
+
+ beforeEach(() => {
+ createComponent({
+ modalTitle,
+ replacePath,
+ primaryBtnText,
+ });
+ });
+
+ it('displays the passed modal title', () => {
+ expect(findModal().props('title')).toBe(modalTitle);
+ });
+
+ it('display the passed primary button text', () => {
+ expect(findModal().props('actionPrimary').text).toBe(primaryBtnText);
+ });
+
+ it('calls the replaceFile when the form submit', async () => {
+ await submitForm();
+
+ expect(wrapper.vm.replaceFile).toHaveBeenCalled();
+ expect(wrapper.vm.uploadFile).not.toHaveBeenCalled();
+ });
+
+ it('makes a PUT request', async () => {
+ await submitRequest();
+
+ expect(mock.history.put).toHaveLength(1);
+ expect(mock.history.post).toHaveLength(0);
+ expect(mock.history.put[0].url).toBe(replacePath);
+ });
+ });
+ });
});
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index a842053caad..8cabf902a4f 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -69,6 +69,21 @@ describe('fetchLogsTree', () => {
mock.restore();
});
+ it('persists the offset for a given page if offset is larger than maximum offset', async () => {
+ await fetchLogsTree(client, 'path', '1000', resolver, 900).then(() => {});
+
+ await fetchLogsTree(client, 'path', '1100', resolver, 1200).then(() => {
+ expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/-/refs/main/logs_tree/path', {
+ params: { format: 'json', offset: 975 },
+ });
+ });
+ });
+
+ it('does not call axios get if offset is larger than the maximum offset', () =>
+ fetchLogsTree(client, '', '1000', resolver, 900).then(() => {
+ expect(axios.get).not.toHaveBeenCalled();
+ }));
+
it('calls axios get', () =>
fetchLogsTree(client, '', '0', resolver).then(() => {
expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/-/refs/main/logs_tree/', {
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
new file mode 100644
index 00000000000..12651a82a0c
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -0,0 +1,201 @@
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
+import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
+import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
+import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+
+const mockId = '1';
+
+const getRunnersQueryName = getRunnersQuery.definitions[0].name.value;
+
+describe('RunnerTypeCell', () => {
+ let wrapper;
+ let mutate;
+
+ const findEditBtn = () => wrapper.findByTestId('edit-runner');
+ const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
+ const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
+
+ const createComponent = ({ active = true } = {}, options) => {
+ wrapper = extendedWrapper(
+ shallowMount(RunnerActionCell, {
+ propsData: {
+ runner: {
+ id: `gid://gitlab/Ci::Runner/${mockId}`,
+ active,
+ },
+ },
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ ...options,
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ mutate = jest.fn();
+ });
+
+ afterEach(() => {
+ mutate.mockReset();
+ wrapper.destroy();
+ });
+
+ it('Displays the runner edit link with the correct href', () => {
+ createComponent();
+
+ expect(findEditBtn().attributes('href')).toBe('/admin/runners/1');
+ });
+
+ describe.each`
+ state | label | icon | isActive | newActiveValue
+ ${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false}
+ ${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true}
+ `('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => {
+ beforeEach(() => {
+ mutate.mockResolvedValue({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: `gid://gitlab/Ci::Runner/1`,
+ __typename: 'CiRunner',
+ },
+ },
+ },
+ });
+
+ createComponent({ active: isActive });
+ });
+
+ it(`Displays a ${icon} button`, () => {
+ expect(findToggleActiveBtn().props('loading')).toBe(false);
+ expect(findToggleActiveBtn().props('icon')).toBe(icon);
+ expect(findToggleActiveBtn().attributes('title')).toBe(label);
+ expect(findToggleActiveBtn().attributes('aria-label')).toBe(label);
+ });
+
+ it(`After clicking the ${icon} button, the button has a loading state`, async () => {
+ await findToggleActiveBtn().vm.$emit('click');
+
+ expect(findToggleActiveBtn().props('loading')).toBe(true);
+ });
+
+ it(`After the ${icon} button is clicked, stale tooltip is removed`, async () => {
+ await findToggleActiveBtn().vm.$emit('click');
+
+ expect(findToggleActiveBtn().attributes('title')).toBe('');
+ expect(findToggleActiveBtn().attributes('aria-label')).toBe('');
+ });
+
+ describe(`When clicking on the ${icon} button`, () => {
+ beforeEach(async () => {
+ await findToggleActiveBtn().vm.$emit('click');
+ await waitForPromises();
+ });
+
+ it(`The apollo mutation to set active to ${newActiveValue} is called`, () => {
+ expect(mutate).toHaveBeenCalledTimes(1);
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: runnerUpdateMutation,
+ variables: {
+ input: {
+ id: `gid://gitlab/Ci::Runner/${mockId}`,
+ active: newActiveValue,
+ },
+ },
+ });
+ });
+
+ it('The button does not have a loading state', () => {
+ expect(findToggleActiveBtn().props('loading')).toBe(false);
+ });
+ });
+ });
+
+ describe('When the user clicks a runner', () => {
+ beforeEach(() => {
+ createComponent();
+
+ mutate.mockResolvedValue({
+ data: {
+ runnerDelete: {
+ runner: {
+ id: `gid://gitlab/Ci::Runner/1`,
+ __typename: 'CiRunner',
+ },
+ },
+ },
+ });
+
+ jest.spyOn(window, 'confirm');
+ });
+
+ describe('When the user confirms deletion', () => {
+ beforeEach(async () => {
+ window.confirm.mockReturnValue(true);
+ await findDeleteBtn().vm.$emit('click');
+ });
+
+ it('The user sees a confirmation alert', async () => {
+ expect(window.confirm).toHaveBeenCalledTimes(1);
+ expect(window.confirm).toHaveBeenCalledWith(expect.any(String));
+ });
+
+ it('The delete mutation is called correctly', () => {
+ expect(mutate).toHaveBeenCalledTimes(1);
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: deleteRunnerMutation,
+ variables: {
+ input: {
+ id: `gid://gitlab/Ci::Runner/${mockId}`,
+ },
+ },
+ awaitRefetchQueries: true,
+ refetchQueries: [getRunnersQueryName],
+ });
+ });
+
+ it('The delete button does not have a loading state', () => {
+ expect(findDeleteBtn().props('loading')).toBe(false);
+ expect(findDeleteBtn().attributes('title')).toBe('Remove');
+ });
+
+ it('After the delete button is clicked, loading state is shown', async () => {
+ await findDeleteBtn().vm.$emit('click');
+
+ expect(findDeleteBtn().props('loading')).toBe(true);
+ });
+
+ it('After the delete button is clicked, stale tooltip is removed', async () => {
+ await findDeleteBtn().vm.$emit('click');
+
+ expect(findDeleteBtn().attributes('title')).toBe('');
+ });
+ });
+
+ describe('When the user does not confirm deletion', () => {
+ beforeEach(async () => {
+ window.confirm.mockReturnValue(false);
+ await findDeleteBtn().vm.$emit('click');
+ });
+
+ it('The user sees a confirmation alert', () => {
+ expect(window.confirm).toHaveBeenCalledTimes(1);
+ });
+
+ it('The delete mutation is not called', () => {
+ expect(mutate).toHaveBeenCalledTimes(0);
+ });
+
+ it('The delete button does not have a loading state', () => {
+ expect(findDeleteBtn().props('loading')).toBe(false);
+ expect(findDeleteBtn().attributes('title')).toBe('Remove');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/cells/runner_name_cell_spec.js b/spec/frontend/runner/components/cells/runner_name_cell_spec.js
new file mode 100644
index 00000000000..26055fc0faf
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_name_cell_spec.js
@@ -0,0 +1,42 @@
+import { GlLink } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import RunnerNameCell from '~/runner/components/cells/runner_name_cell.vue';
+
+const mockId = '1';
+const mockShortSha = '2P6oDVDm';
+const mockDescription = 'runner-1';
+
+describe('RunnerTypeCell', () => {
+ let wrapper;
+
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = () => {
+ wrapper = mount(RunnerNameCell, {
+ propsData: {
+ runner: {
+ id: `gid://gitlab/Ci::Runner/${mockId}`,
+ shortSha: mockShortSha,
+ description: mockDescription,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays the runner link with id and short token', () => {
+ expect(findLink().text()).toBe(`#${mockId} (${mockShortSha})`);
+ expect(findLink().attributes('href')).toBe(`/admin/runners/${mockId}`);
+ });
+
+ it('Displays the runner description', () => {
+ expect(wrapper.text()).toContain(mockDescription);
+ });
+});
diff --git a/spec/frontend/runner/components/cells/runner_type_cell_spec.js b/spec/frontend/runner/components/cells/runner_type_cell_spec.js
new file mode 100644
index 00000000000..48958a282fc
--- /dev/null
+++ b/spec/frontend/runner/components/cells/runner_type_cell_spec.js
@@ -0,0 +1,48 @@
+import { GlBadge } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import RunnerTypeCell from '~/runner/components/cells/runner_type_cell.vue';
+import { INSTANCE_TYPE } from '~/runner/constants';
+
+describe('RunnerTypeCell', () => {
+ let wrapper;
+
+ const findBadges = () => wrapper.findAllComponents(GlBadge);
+
+ const createComponent = ({ runner = {} } = {}) => {
+ wrapper = mount(RunnerTypeCell, {
+ propsData: {
+ runner: {
+ runnerType: INSTANCE_TYPE,
+ active: true,
+ locked: false,
+ ...runner,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays the runner type', () => {
+ createComponent();
+
+ expect(findBadges()).toHaveLength(1);
+ expect(findBadges().at(0).text()).toBe('shared');
+ });
+
+ it('Displays locked and paused states', () => {
+ createComponent({
+ runner: {
+ active: false,
+ locked: true,
+ },
+ });
+
+ expect(findBadges()).toHaveLength(3);
+ expect(findBadges().at(0).text()).toBe('shared');
+ expect(findBadges().at(1).text()).toBe('locked');
+ expect(findBadges().at(2).text()).toBe('paused');
+ });
+});
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
new file mode 100644
index 00000000000..61a8f821b30
--- /dev/null
+++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
@@ -0,0 +1,137 @@
+import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+import { PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE } from '~/runner/constants';
+import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+
+describe('RunnerList', () => {
+ let wrapper;
+
+ const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
+ const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem);
+
+ const mockDefaultSort = 'CREATED_DESC';
+ const mockOtherSort = 'CONTACTED_DESC';
+ const mockFilters = [
+ { type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'filtered-search-term', value: { data: '' } },
+ ];
+
+ const createComponent = ({ props = {}, options = {} } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(RunnerFilteredSearchBar, {
+ propsData: {
+ value: {
+ filters: [],
+ sort: mockDefaultSort,
+ },
+ ...props,
+ },
+ attrs: { namespace: 'runners' },
+ stubs: {
+ FilteredSearch,
+ GlFilteredSearch,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ ...options,
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('binds a namespace to the filtered search', () => {
+ expect(findFilteredSearch().props('namespace')).toBe('runners');
+ });
+
+ it('sets sorting options', () => {
+ const SORT_OPTIONS_COUNT = 2;
+
+ expect(findSortOptions()).toHaveLength(SORT_OPTIONS_COUNT);
+ expect(findSortOptions().at(0).text()).toBe('Created date');
+ expect(findSortOptions().at(1).text()).toBe('Last contact');
+ });
+
+ it('sets tokens', () => {
+ expect(findFilteredSearch().props('tokens')).toEqual([
+ expect.objectContaining({
+ type: PARAM_KEY_STATUS,
+ options: expect.any(Array),
+ }),
+ expect.objectContaining({
+ type: PARAM_KEY_RUNNER_TYPE,
+ options: expect.any(Array),
+ }),
+ ]);
+ });
+
+ it('fails validation for v-model with the wrong shape', () => {
+ expect(() => {
+ createComponent({ props: { value: { filters: 'wrong_filters', sort: 'sort' } } });
+ }).toThrow('Invalid prop: custom validator check failed');
+
+ expect(() => {
+ createComponent({ props: { value: { sort: 'sort' } } });
+ }).toThrow('Invalid prop: custom validator check failed');
+ });
+
+ describe('when a search is preselected', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ value: {
+ sort: mockOtherSort,
+ filters: mockFilters,
+ },
+ },
+ });
+ });
+
+ it('filter values are shown', () => {
+ expect(findGlFilteredSearch().props('value')).toEqual(mockFilters);
+ });
+
+ it('sort option is selected', () => {
+ expect(
+ findSortOptions()
+ .filter((w) => w.props('isChecked'))
+ .at(0)
+ .text(),
+ ).toEqual('Last contact');
+ });
+ });
+
+ it('when the user sets a filter, the "search" is emitted with filters', () => {
+ findGlFilteredSearch().vm.$emit('input', mockFilters);
+ findGlFilteredSearch().vm.$emit('submit');
+
+ expect(wrapper.emitted('input')[0]).toEqual([
+ {
+ filters: mockFilters,
+ sort: mockDefaultSort,
+ pagination: { page: 1 },
+ },
+ ]);
+ });
+
+ it('when the user sets a sorting method, the "search" is emitted with the sort', () => {
+ findSortOptions().at(1).vm.$emit('click');
+
+ expect(wrapper.emitted('input')[0]).toEqual([
+ {
+ filters: [],
+ sort: mockOtherSort,
+ pagination: { page: 1 },
+ },
+ ]);
+ });
+});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
new file mode 100644
index 00000000000..d88d7b3fbee
--- /dev/null
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -0,0 +1,130 @@
+import { GlLink, GlTable, GlSkeletonLoader } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import RunnerList from '~/runner/components/runner_list.vue';
+import { runnersData } from '../mock_data';
+
+const mockRunners = runnersData.data.runners.nodes;
+const mockActiveRunnersCount = mockRunners.length;
+
+describe('RunnerList', () => {
+ let wrapper;
+
+ const findActiveRunnersMessage = () => wrapper.findByTestId('active-runners-message');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findHeaders = () => wrapper.findAll('th');
+ const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
+ const findCell = ({ row = 0, fieldKey }) =>
+ extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ wrapper = extendedWrapper(
+ mountFn(RunnerList, {
+ propsData: {
+ runners: mockRunners,
+ activeRunnersCount: mockActiveRunnersCount,
+ ...props,
+ },
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ createComponent({}, mount);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays active runner count', () => {
+ expect(findActiveRunnersMessage().text()).toBe(
+ `Runners currently online: ${mockActiveRunnersCount}`,
+ );
+ });
+
+ it('Displays a large active runner count', () => {
+ createComponent({ props: { activeRunnersCount: 2000 } });
+
+ expect(findActiveRunnersMessage().text()).toBe('Runners currently online: 2,000');
+ });
+
+ it('Displays headers', () => {
+ const headerLabels = findHeaders().wrappers.map((w) => w.text());
+
+ expect(headerLabels).toEqual([
+ 'Type/State',
+ 'Runner',
+ 'Version',
+ 'IP Address',
+ 'Projects',
+ 'Jobs',
+ 'Tags',
+ 'Last contact',
+ '', // actions has no label
+ ]);
+ });
+
+ it('Displays a list of runners', () => {
+ expect(findRows()).toHaveLength(3);
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('Displays details of a runner', () => {
+ const { id, description, version, ipAddress, shortSha } = mockRunners[0];
+
+ // Badges
+ expect(findCell({ fieldKey: 'type' }).text()).toMatchInterpolatedText('specific paused');
+
+ // Runner identifier
+ expect(findCell({ fieldKey: 'name' }).text()).toContain(
+ `#${getIdFromGraphQLId(id)} (${shortSha})`,
+ );
+ expect(findCell({ fieldKey: 'name' }).text()).toContain(description);
+
+ // Other fields: some cells are empty in the first iteration
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/329658#pending-features
+ expect(findCell({ fieldKey: 'version' }).text()).toBe(version);
+ expect(findCell({ fieldKey: 'ipAddress' }).text()).toBe(ipAddress);
+ expect(findCell({ fieldKey: 'projectCount' }).text()).toBe('');
+ expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('');
+ expect(findCell({ fieldKey: 'tagList' }).text()).toBe('');
+ expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String));
+
+ // Actions
+ const actions = findCell({ fieldKey: 'actions' });
+
+ expect(actions.findByTestId('edit-runner').exists()).toBe(true);
+ expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
+ });
+
+ it('Links to the runner page', () => {
+ const { id } = mockRunners[0];
+
+ expect(findCell({ fieldKey: 'name' }).find(GlLink).attributes('href')).toBe(
+ `/admin/runners/${getIdFromGraphQLId(id)}`,
+ );
+ });
+
+ describe('When data is loading', () => {
+ it('shows a busy state', () => {
+ createComponent({ props: { runners: [], loading: true } });
+ expect(findTable().attributes('busy')).toBeTruthy();
+ });
+
+ it('when there are no runners, shows an skeleton loader', () => {
+ createComponent({ props: { runners: [], loading: true } }, mount);
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('when there are runners, shows a busy indicator skeleton loader', () => {
+ createComponent({ props: { loading: true } }, mount);
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_manual_setup_help_spec.js b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
new file mode 100644
index 00000000000..ca5c88f6e28
--- /dev/null
+++ b/spec/frontend/runner/components/runner_manual_setup_help_spec.js
@@ -0,0 +1,84 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+
+const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+const mockRunnerInstallHelpPage = 'https://docs.gitlab.com/runner/install/';
+
+describe('RunnerManualSetupHelp', () => {
+ let wrapper;
+ let originalGon;
+
+ const findRunnerInstructions = () => wrapper.findComponent(RunnerInstructions);
+ const findClipboardButtons = () => wrapper.findAllComponents(ClipboardButton);
+ const findRunnerHelpTitle = () => wrapper.findByTestId('runner-help-title');
+ const findCoordinatorUrl = () => wrapper.findByTestId('coordinator-url');
+ const findRegistrationToken = () => wrapper.findByTestId('registration-token');
+ const findRunnerHelpLink = () => wrapper.findByTestId('runner-help-link');
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(RunnerManualSetupHelp, {
+ provide: {
+ runnerInstallHelpPage: mockRunnerInstallHelpPage,
+ },
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ }),
+ );
+ };
+
+ beforeAll(() => {
+ originalGon = global.gon;
+ global.gon = { gitlab_url: TEST_HOST };
+ });
+
+ afterAll(() => {
+ global.gon = originalGon;
+ });
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Title contains the default runner type', () => {
+ expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a shared runner manually');
+ });
+
+ it('Title contains the group runner type', () => {
+ createComponent({ props: { typeName: 'group' } });
+
+ expect(findRunnerHelpTitle().text()).toMatchInterpolatedText('Set up a group runner manually');
+ });
+
+ it('Runner Install Page link', () => {
+ expect(findRunnerHelpLink().attributes('href')).toBe(mockRunnerInstallHelpPage);
+ });
+
+ it('Displays the coordinator URL token', () => {
+ expect(findCoordinatorUrl().text()).toBe(TEST_HOST);
+ expect(findClipboardButtons().at(0).props('text')).toBe(TEST_HOST);
+ });
+
+ it('Displays the registration token', () => {
+ expect(findRegistrationToken().text()).toBe(mockRegistrationToken);
+ expect(findClipboardButtons().at(1).props('text')).toBe(mockRegistrationToken);
+ });
+
+ it('Displays the runner instructions', () => {
+ expect(findRunnerInstructions().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/runner/components/runner_pagination_spec.js b/spec/frontend/runner/components/runner_pagination_spec.js
new file mode 100644
index 00000000000..59feb32dd2a
--- /dev/null
+++ b/spec/frontend/runner/components/runner_pagination_spec.js
@@ -0,0 +1,160 @@
+import { GlPagination } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
+
+const mockStartCursor = 'START_CURSOR';
+const mockEndCursor = 'END_CURSOR';
+
+describe('RunnerPagination', () => {
+ let wrapper;
+
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ const createComponent = ({ page = 1, hasPreviousPage = false, hasNextPage = true } = {}) => {
+ wrapper = mount(RunnerPagination, {
+ propsData: {
+ value: {
+ page,
+ },
+ pageInfo: {
+ hasPreviousPage,
+ hasNextPage,
+ startCursor: mockStartCursor,
+ endCursor: mockEndCursor,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('When on the first page', () => {
+ beforeEach(() => {
+ createComponent({
+ page: 1,
+ hasPreviousPage: false,
+ hasNextPage: true,
+ });
+ });
+
+ it('Contains the current page information', () => {
+ expect(findPagination().props('value')).toBe(1);
+ expect(findPagination().props('prevPage')).toBe(null);
+ expect(findPagination().props('nextPage')).toBe(2);
+ });
+
+ it('Shows prev page disabled', () => {
+ expect(findPagination().find('[aria-disabled]').text()).toBe('Prev');
+ });
+
+ it('Shows next page link', () => {
+ expect(findPagination().find('a').text()).toBe('Next');
+ });
+
+ it('Goes to the second page', () => {
+ findPagination().vm.$emit('input', 2);
+
+ expect(wrapper.emitted('input')[0]).toEqual([
+ {
+ after: mockEndCursor,
+ page: 2,
+ },
+ ]);
+ });
+ });
+
+ describe('When in between pages', () => {
+ beforeEach(() => {
+ createComponent({
+ page: 2,
+ hasPreviousPage: true,
+ hasNextPage: true,
+ });
+ });
+
+ it('Contains the current page information', () => {
+ expect(findPagination().props('value')).toBe(2);
+ expect(findPagination().props('prevPage')).toBe(1);
+ expect(findPagination().props('nextPage')).toBe(3);
+ });
+
+ it('Shows the next and previous pages', () => {
+ const links = findPagination().findAll('a');
+
+ expect(links).toHaveLength(2);
+ expect(links.at(0).text()).toBe('Prev');
+ expect(links.at(1).text()).toBe('Next');
+ });
+
+ it('Goes to the last page', () => {
+ findPagination().vm.$emit('input', 3);
+
+ expect(wrapper.emitted('input')[0]).toEqual([
+ {
+ after: mockEndCursor,
+ page: 3,
+ },
+ ]);
+ });
+
+ it('Goes to the first page', () => {
+ findPagination().vm.$emit('input', 1);
+
+ expect(wrapper.emitted('input')[0]).toEqual([
+ {
+ before: mockStartCursor,
+ page: 1,
+ },
+ ]);
+ });
+ });
+
+ describe('When in the last page', () => {
+ beforeEach(() => {
+ createComponent({
+ page: 3,
+ hasPreviousPage: true,
+ hasNextPage: false,
+ });
+ });
+
+ it('Contains the current page', () => {
+ expect(findPagination().props('value')).toBe(3);
+ expect(findPagination().props('prevPage')).toBe(2);
+ expect(findPagination().props('nextPage')).toBe(null);
+ });
+
+ it('Shows next page link', () => {
+ expect(findPagination().find('a').text()).toBe('Prev');
+ });
+
+ it('Shows next page disabled', () => {
+ expect(findPagination().find('[aria-disabled]').text()).toBe('Next');
+ });
+ });
+
+ describe('When only one page', () => {
+ beforeEach(() => {
+ createComponent({
+ page: 1,
+ hasPreviousPage: false,
+ hasNextPage: false,
+ });
+ });
+
+ it('does not display pagination', () => {
+ expect(wrapper.html()).toBe('');
+ });
+
+ it('Contains the current page', () => {
+ expect(findPagination().props('value')).toBe(1);
+ });
+
+ it('Shows no more page buttons', () => {
+ expect(findPagination().props('prevPage')).toBe(null);
+ expect(findPagination().props('nextPage')).toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js
new file mode 100644
index 00000000000..7bb3f65e4ba
--- /dev/null
+++ b/spec/frontend/runner/components/runner_tags_spec.js
@@ -0,0 +1,64 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTags from '~/runner/components/runner_tags.vue';
+
+describe('RunnerTags', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findBadgesAt = (i = 0) => wrapper.findAllComponents(GlBadge).at(i);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(RunnerTags, {
+ propsData: {
+ tagList: ['tag1', 'tag2'],
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays tags text', () => {
+ expect(wrapper.text()).toMatchInterpolatedText('tag1 tag2');
+
+ expect(findBadgesAt(0).text()).toBe('tag1');
+ expect(findBadgesAt(1).text()).toBe('tag2');
+ });
+
+ it('Displays tags with correct style', () => {
+ expect(findBadge().props('size')).toBe('md');
+ expect(findBadge().props('variant')).toBe('info');
+ });
+
+ it('Displays tags with small size', () => {
+ createComponent({
+ props: { size: 'sm' },
+ });
+
+ expect(findBadge().props('size')).toBe('sm');
+ });
+
+ it('Displays tags with a variant', () => {
+ createComponent({
+ props: { variant: 'warning' },
+ });
+
+ expect(findBadge().props('variant')).toBe('warning');
+ });
+
+ it('Is empty when there are no tags', () => {
+ createComponent({
+ props: { tagList: null },
+ });
+
+ expect(wrapper.text()).toBe('');
+ expect(findBadge().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/runner/components/runner_type_alert_spec.js b/spec/frontend/runner/components/runner_type_alert_spec.js
new file mode 100644
index 00000000000..5b136a77eeb
--- /dev/null
+++ b/spec/frontend/runner/components/runner_type_alert_spec.js
@@ -0,0 +1,61 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTypeAlert from '~/runner/components/runner_type_alert.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+
+describe('RunnerTypeAlert', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(RunnerTypeAlert, {
+ propsData: {
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ type | exampleText | anchor | variant
+ ${INSTANCE_TYPE} | ${'Shared runners are available to every project'} | ${'#shared-runners'} | ${'success'}
+ ${GROUP_TYPE} | ${'Use Group runners when you want all projects in a group'} | ${'#group-runners'} | ${'success'}
+ ${PROJECT_TYPE} | ${'You can set up a specific runner to be used by multiple projects'} | ${'#specific-runners'} | ${'info'}
+ `('When it is an $type level runner', ({ type, exampleText, anchor, variant }) => {
+ beforeEach(() => {
+ createComponent({ props: { type } });
+ });
+
+ it('Describes runner type', () => {
+ expect(wrapper.text()).toMatch(exampleText);
+ });
+
+ it(`Shows a ${variant} variant`, () => {
+ expect(findAlert().props('variant')).toBe(variant);
+ });
+
+ it(`Links to anchor "${anchor}"`, () => {
+ expect(findLink().attributes('href')).toBe(`/help/ci/runners/runners_scope${anchor}`);
+ });
+ });
+
+ describe('When runner type is not correct', () => {
+ it('Does not render content when type is missing', () => {
+ createComponent({ props: { type: undefined } });
+
+ expect(wrapper.html()).toBe('');
+ });
+
+ it('Validation fails for an incorrect type', () => {
+ expect(() => {
+ createComponent({ props: { type: 'NOT_A_TYPE' } });
+ }).toThrow();
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js
index 8e52d3398bd..ab5ccf6390f 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/runner/components/runner_type_badge_spec.js
@@ -32,8 +32,14 @@ describe('RunnerTypeBadge', () => {
expect(findBadge().props('variant')).toBe(variant);
});
- it('does not display a badge when type is unknown', () => {
- createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
+ it('validation fails for an incorrect type', () => {
+ expect(() => {
+ createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
+ }).toThrow();
+ });
+
+ it('does not render content when type is missing', () => {
+ createComponent({ props: { type: undefined } });
expect(findBadge().exists()).toBe(false);
});
diff --git a/spec/frontend/runner/components/runner_type_help_spec.js b/spec/frontend/runner/components/runner_type_help_spec.js
new file mode 100644
index 00000000000..f0d03282f8e
--- /dev/null
+++ b/spec/frontend/runner/components/runner_type_help_spec.js
@@ -0,0 +1,32 @@
+import { GlBadge } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
+
+describe('RunnerTypeHelp', () => {
+ let wrapper;
+
+ const findBadges = () => wrapper.findAllComponents(GlBadge);
+
+ const createComponent = () => {
+ wrapper = mount(RunnerTypeHelp);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Displays each of the runner types', () => {
+ expect(findBadges().at(0).text()).toBe('shared');
+ expect(findBadges().at(1).text()).toBe('group');
+ expect(findBadges().at(2).text()).toBe('specific');
+ });
+
+ it('Displays runner states', () => {
+ expect(findBadges().at(3).text()).toBe('locked');
+ expect(findBadges().at(4).text()).toBe('paused');
+ });
+});
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
new file mode 100644
index 00000000000..6333ed7118a
--- /dev/null
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -0,0 +1,263 @@
+import { GlForm } from '@gitlab/ui';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ ACCESS_LEVEL_REF_PROTECTED,
+ ACCESS_LEVEL_NOT_PROTECTED,
+} from '~/runner/constants';
+import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import { runnerData } from '../mock_data';
+
+jest.mock('~/flash');
+
+const mockRunner = runnerData.data.runner;
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('RunnerUpdateForm', () => {
+ let wrapper;
+ let runnerUpdateHandler;
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findPausedCheckbox = () => wrapper.findByTestId('runner-field-paused');
+ const findProtectedCheckbox = () => wrapper.findByTestId('runner-field-protected');
+ const findRunUntaggedCheckbox = () => wrapper.findByTestId('runner-field-run-untagged');
+ const findLockedCheckbox = () => wrapper.findByTestId('runner-field-locked');
+
+ const findIpInput = () => wrapper.findByTestId('runner-field-ip-address').find('input');
+
+ const findDescriptionInput = () => wrapper.findByTestId('runner-field-description').find('input');
+ const findMaxJobTimeoutInput = () =>
+ wrapper.findByTestId('runner-field-max-timeout').find('input');
+ const findTagsInput = () => wrapper.findByTestId('runner-field-tags').find('input');
+
+ const findSubmit = () => wrapper.find('[type="submit"]');
+ const findSubmitDisabledAttr = () => findSubmit().attributes('disabled');
+ const submitForm = () => findForm().trigger('submit');
+ const submitFormAndWait = () => submitForm().then(waitForPromises);
+
+ const getFieldsModel = () => ({
+ active: !findPausedCheckbox().element.checked,
+ accessLevel: findProtectedCheckbox().element.checked
+ ? ACCESS_LEVEL_REF_PROTECTED
+ : ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: findRunUntaggedCheckbox().element.checked,
+ locked: findLockedCheckbox().element.checked,
+ ipAddress: findIpInput().element.value,
+ maximumTimeout: findMaxJobTimeoutInput().element.value || null,
+ tagList: findTagsInput().element.value.split(',').filter(Boolean),
+ });
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = extendedWrapper(
+ mount(RunnerUpdateForm, {
+ localVue,
+ propsData: {
+ runner: mockRunner,
+ ...props,
+ },
+ apolloProvider: createMockApollo([[runnerUpdateMutation, runnerUpdateHandler]]),
+ }),
+ );
+ };
+
+ const expectToHaveSubmittedRunnerContaining = (submittedRunner) => {
+ expect(runnerUpdateHandler).toHaveBeenCalledTimes(1);
+ expect(runnerUpdateHandler).toHaveBeenCalledWith({
+ input: expect.objectContaining(submittedRunner),
+ });
+
+ expect(createFlash).toHaveBeenLastCalledWith({
+ message: expect.stringContaining('saved'),
+ type: FLASH_TYPES.SUCCESS,
+ });
+
+ expect(findSubmitDisabledAttr()).toBeUndefined();
+ };
+
+ beforeEach(() => {
+ runnerUpdateHandler = jest.fn().mockImplementation(({ input }) => {
+ return Promise.resolve({
+ data: {
+ runnerUpdate: {
+ runner: {
+ ...mockRunner,
+ ...input,
+ },
+ errors: [],
+ },
+ },
+ });
+ });
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Form has a submit button', () => {
+ expect(findSubmit().exists()).toBe(true);
+ });
+
+ it('Form fields match data', () => {
+ expect(mockRunner).toMatchObject(getFieldsModel());
+ });
+
+ it('Form prevent multiple submissions', async () => {
+ await submitForm();
+
+ expect(findSubmitDisabledAttr()).toBe('disabled');
+ });
+
+ it('Updates runner with no changes', async () => {
+ await submitFormAndWait();
+
+ // Some fields are not submitted
+ const { ipAddress, runnerType, ...submitted } = mockRunner;
+
+ expectToHaveSubmittedRunnerContaining(submitted);
+ });
+
+ describe('When data is being loaded', () => {
+ beforeEach(() => {
+ createComponent({ props: { runner: null } });
+ });
+
+ it('Form cannot be submitted', () => {
+ expect(findSubmit().props('loading')).toBe(true);
+ });
+
+ it('Form is updated when data loads', async () => {
+ wrapper.setProps({
+ runner: mockRunner,
+ });
+
+ await nextTick();
+
+ expect(mockRunner).toMatchObject(getFieldsModel());
+ });
+ });
+
+ it.each`
+ runnerType | attrDisabled | outcome
+ ${INSTANCE_TYPE} | ${'disabled'} | ${'disabled'}
+ ${GROUP_TYPE} | ${'disabled'} | ${'disabled'}
+ ${PROJECT_TYPE} | ${undefined} | ${'enabled'}
+ `(`When runner is $runnerType, locked field is $outcome`, ({ runnerType, attrDisabled }) => {
+ const runner = { ...mockRunner, runnerType };
+ createComponent({ props: { runner } });
+
+ expect(findLockedCheckbox().attributes('disabled')).toBe(attrDisabled);
+ });
+
+ describe('On submit, runner gets updated', () => {
+ it.each`
+ test | initialValue | findCheckbox | checked | submitted
+ ${'pauses'} | ${{ active: true }} | ${findPausedCheckbox} | ${true} | ${{ active: false }}
+ ${'activates'} | ${{ active: false }} | ${findPausedCheckbox} | ${false} | ${{ active: true }}
+ ${'unprotects'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} | ${findProtectedCheckbox} | ${true} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }}
+ ${'protects'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} | ${findProtectedCheckbox} | ${false} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }}
+ ${'"runs untagged jobs"'} | ${{ runUntagged: true }} | ${findRunUntaggedCheckbox} | ${false} | ${{ runUntagged: false }}
+ ${'"runs tagged jobs"'} | ${{ runUntagged: false }} | ${findRunUntaggedCheckbox} | ${true} | ${{ runUntagged: true }}
+ ${'locks'} | ${{ runnerType: PROJECT_TYPE, locked: true }} | ${findLockedCheckbox} | ${false} | ${{ locked: false }}
+ ${'unlocks'} | ${{ runnerType: PROJECT_TYPE, locked: false }} | ${findLockedCheckbox} | ${true} | ${{ locked: true }}
+ `('Checkbox $test runner', async ({ initialValue, findCheckbox, checked, submitted }) => {
+ const runner = { ...mockRunner, ...initialValue };
+ createComponent({ props: { runner } });
+
+ await findCheckbox().setChecked(checked);
+ await submitFormAndWait();
+
+ expectToHaveSubmittedRunnerContaining({
+ id: runner.id,
+ ...submitted,
+ });
+ });
+
+ it.each`
+ test | initialValue | findInput | value | submitted
+ ${'description'} | ${{ description: 'Desc. 1' }} | ${findDescriptionInput} | ${'Desc. 2'} | ${{ description: 'Desc. 2' }}
+ ${'max timeout'} | ${{ maximumTimeout: 36000 }} | ${findMaxJobTimeoutInput} | ${'40000'} | ${{ maximumTimeout: 40000 }}
+ ${'tags'} | ${{ tagList: ['tag1'] }} | ${findTagsInput} | ${'tag2, tag3'} | ${{ tagList: ['tag2', 'tag3'] }}
+ `("Field updates runner's $test", async ({ initialValue, findInput, value, submitted }) => {
+ const runner = { ...mockRunner, ...initialValue };
+ createComponent({ props: { runner } });
+
+ await findInput().setValue(value);
+ await submitFormAndWait();
+
+ expectToHaveSubmittedRunnerContaining({
+ id: runner.id,
+ ...submitted,
+ });
+ });
+
+ it.each`
+ value | submitted
+ ${''} | ${{ tagList: [] }}
+ ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }}
+ ${'with spaces'} | ${{ tagList: ['with spaces'] }}
+ ${',,,,, commas'} | ${{ tagList: ['commas'] }}
+ ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }}
+ ${' trimmed , trimmed2 '} | ${{ tagList: ['trimmed', 'trimmed2'] }}
+ `('Field updates runner\'s tags for "$value"', async ({ value, submitted }) => {
+ const runner = { ...mockRunner, tagList: ['tag1'] };
+ createComponent({ props: { runner } });
+
+ await findTagsInput().setValue(value);
+ await submitFormAndWait();
+
+ expectToHaveSubmittedRunnerContaining({
+ id: runner.id,
+ ...submitted,
+ });
+ });
+ });
+
+ describe('On error', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('On network error, error message is shown', async () => {
+ runnerUpdateHandler.mockRejectedValue(new Error('Something went wrong'));
+
+ await submitFormAndWait();
+
+ expect(createFlash).toHaveBeenLastCalledWith({
+ message: 'Network error: Something went wrong',
+ });
+ expect(findSubmitDisabledAttr()).toBeUndefined();
+ });
+
+ it('On validation error, error message is shown', async () => {
+ runnerUpdateHandler.mockResolvedValue({
+ data: {
+ runnerUpdate: {
+ runner: mockRunner,
+ errors: ['A value is invalid'],
+ },
+ },
+ });
+
+ await submitFormAndWait();
+
+ expect(createFlash).toHaveBeenLastCalledWith({
+ message: 'A value is invalid',
+ });
+ expect(findSubmitDisabledAttr()).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
new file mode 100644
index 00000000000..8f551feca6e
--- /dev/null
+++ b/spec/frontend/runner/mock_data.js
@@ -0,0 +1,6 @@
+// Fixtures generated by: spec/frontend/fixtures/runner.rb
+export const runnersData = getJSONFixture('graphql/runner/get_runners.query.graphql.json');
+export const runnersDataPaginated = getJSONFixture(
+ 'graphql/runner/get_runners.query.graphql.paginated.json',
+);
+export const runnerData = getJSONFixture('graphql/runner/get_runner.query.graphql.json');
diff --git a/spec/frontend/runner/runner_detail/runner_details_app_spec.js b/spec/frontend/runner/runner_detail/runner_details_app_spec.js
index c61cb647ae6..d0bd701458d 100644
--- a/spec/frontend/runner/runner_detail/runner_details_app_spec.js
+++ b/spec/frontend/runner/runner_detail/runner_details_app_spec.js
@@ -3,12 +3,15 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
-import { INSTANCE_TYPE } from '~/runner/constants';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
import RunnerDetailsApp from '~/runner/runner_details/runner_details_app.vue';
-const mockRunnerId = '55';
+import { runnerData } from '../mock_data';
+
+const mockRunnerGraphqlId = runnerData.data.runner.id;
+const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -35,15 +38,7 @@ describe('RunnerDetailsApp', () => {
};
beforeEach(async () => {
- mockRunnerQuery = jest.fn().mockResolvedValue({
- data: {
- runner: {
- id: `gid://gitlab/Ci::Runner/${mockRunnerId}`,
- runnerType: INSTANCE_TYPE,
- __typename: 'CiRunner',
- },
- },
- });
+ mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
});
afterEach(() => {
@@ -54,13 +49,13 @@ describe('RunnerDetailsApp', () => {
it('expect GraphQL ID to be requested', async () => {
await createComponentWithApollo();
- expect(mockRunnerQuery).toHaveBeenCalledWith({ id: `gid://gitlab/Ci::Runner/${mockRunnerId}` });
+ expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
it('displays the runner id', async () => {
await createComponentWithApollo();
- expect(wrapper.text()).toContain('Runner #55');
+ expect(wrapper.text()).toContain(`Runner #${mockRunnerId}`);
});
it('displays the runner type', async () => {
diff --git a/spec/frontend/runner/runner_list/runner_list_app_spec.js b/spec/frontend/runner/runner_list/runner_list_app_spec.js
new file mode 100644
index 00000000000..dd913df7143
--- /dev/null
+++ b/spec/frontend/runner/runner_list/runner_list_app_spec.js
@@ -0,0 +1,232 @@
+import * as Sentry from '@sentry/browser';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import { updateHistory } from '~/lib/utils/url_utility';
+
+import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/runner/components/runner_list.vue';
+import RunnerManualSetupHelp from '~/runner/components/runner_manual_setup_help.vue';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import RunnerTypeHelp from '~/runner/components/runner_type_help.vue';
+
+import {
+ CREATED_ASC,
+ CREATED_DESC,
+ DEFAULT_SORT,
+ INSTANCE_TYPE,
+ PARAM_KEY_STATUS,
+ STATUS_ACTIVE,
+ RUNNER_PAGE_SIZE,
+} from '~/runner/constants';
+import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
+import RunnerListApp from '~/runner/runner_list/runner_list_app.vue';
+
+import { runnersData, runnersDataPaginated } from '../mock_data';
+
+const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+const mockActiveRunnersCount = 2;
+
+jest.mock('@sentry/browser');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('RunnerListApp', () => {
+ let wrapper;
+ let mockRunnersQuery;
+ let originalLocation;
+
+ const findRunnerTypeHelp = () => wrapper.findComponent(RunnerTypeHelp);
+ const findRunnerManualSetupHelp = () => wrapper.findComponent(RunnerManualSetupHelp);
+ const findRunnerList = () => wrapper.findComponent(RunnerList);
+ const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
+ const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
+
+ const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const handlers = [[getRunnersQuery, mockRunnersQuery]];
+
+ wrapper = mountFn(RunnerListApp, {
+ localVue,
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ activeRunnersCount: mockActiveRunnersCount,
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
+ });
+ };
+
+ const setQuery = (query) => {
+ window.location.href = `${TEST_HOST}/admin/runners/${query}`;
+ window.location.search = query;
+ };
+
+ beforeAll(() => {
+ originalLocation = window.location;
+ Object.defineProperty(window, 'location', { writable: true, value: { href: '', search: '' } });
+ });
+
+ afterAll(() => {
+ window.location = originalLocation;
+ });
+
+ beforeEach(async () => {
+ setQuery('');
+
+ Sentry.withScope.mockImplementation((fn) => {
+ const scope = { setTag: jest.fn() };
+ fn(scope);
+ });
+
+ mockRunnersQuery = jest.fn().mockResolvedValue(runnersData);
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ afterEach(() => {
+ mockRunnersQuery.mockReset();
+ wrapper.destroy();
+ });
+
+ it('shows the runners list', () => {
+ expect(runnersData.data.runners.nodes).toMatchObject(findRunnerList().props('runners'));
+ });
+
+ it('requests the runners with no filters', () => {
+ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
+ status: undefined,
+ type: undefined,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+
+ it('shows the runner type help', () => {
+ expect(findRunnerTypeHelp().exists()).toBe(true);
+ });
+
+ it('shows the runner setup instructions', () => {
+ expect(findRunnerManualSetupHelp().exists()).toBe(true);
+ expect(findRunnerManualSetupHelp().props('registrationToken')).toBe(mockRegistrationToken);
+ });
+
+ describe('when a filter is preselected', () => {
+ beforeEach(async () => {
+ window.location.search = `?status[]=${STATUS_ACTIVE}&runner_type[]=${INSTANCE_TYPE}`;
+
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('sets the filters in the search bar', () => {
+ expect(findRunnerFilteredSearchBar().props('value')).toEqual({
+ filters: [
+ { type: 'status', value: { data: STATUS_ACTIVE, operator: '=' } },
+ { type: 'runner_type', value: { data: INSTANCE_TYPE, operator: '=' } },
+ ],
+ sort: 'CREATED_DESC',
+ pagination: { page: 1 },
+ });
+ });
+
+ it('requests the runners with filter parameters', () => {
+ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
+ status: STATUS_ACTIVE,
+ type: INSTANCE_TYPE,
+ sort: DEFAULT_SORT,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+ });
+
+ describe('when a filter is selected by the user', () => {
+ beforeEach(() => {
+ findRunnerFilteredSearchBar().vm.$emit('input', {
+ filters: [{ type: PARAM_KEY_STATUS, value: { data: 'ACTIVE', operator: '=' } }],
+ sort: CREATED_ASC,
+ });
+ });
+
+ it('updates the browser url', () => {
+ expect(updateHistory).toHaveBeenLastCalledWith({
+ title: expect.any(String),
+ url: 'http://test.host/admin/runners/?status[]=ACTIVE&sort=CREATED_ASC',
+ });
+ });
+
+ it('requests the runners with filters', () => {
+ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
+ status: STATUS_ACTIVE,
+ sort: CREATED_ASC,
+ first: RUNNER_PAGE_SIZE,
+ });
+ });
+ });
+
+ describe('when no runners are found', () => {
+ beforeEach(async () => {
+ mockRunnersQuery = jest.fn().mockResolvedValue({ data: { runners: { nodes: [] } } });
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('shows a message for no results', async () => {
+ expect(wrapper.text()).toContain('No runners found');
+ });
+ });
+
+ it('when runners have not loaded, shows a loading state', () => {
+ createComponentWithApollo();
+ expect(findRunnerList().props('loading')).toBe(true);
+ });
+
+ describe('when runners query fails', () => {
+ beforeEach(async () => {
+ mockRunnersQuery = jest.fn().mockRejectedValue(new Error());
+ createComponentWithApollo();
+
+ await waitForPromises();
+ });
+
+ it('error is reported to sentry', async () => {
+ expect(Sentry.withScope).toHaveBeenCalled();
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ mockRunnersQuery = jest.fn().mockResolvedValue(runnersDataPaginated);
+
+ createComponentWithApollo({ mountFn: mount });
+ });
+
+ it('more pages can be selected', () => {
+ expect(findRunnerPagination().text()).toMatchInterpolatedText('Prev Next');
+ });
+
+ it('cannot navigate to the previous page', () => {
+ expect(findRunnerPagination().find('[aria-disabled]').text()).toBe('Prev');
+ });
+
+ it('navigates to the next page', async () => {
+ const nextPageBtn = findRunnerPagination().find('a');
+ expect(nextPageBtn.text()).toBe('Next');
+
+ await nextPageBtn.trigger('click');
+
+ expect(mockRunnersQuery).toHaveBeenLastCalledWith({
+ sort: CREATED_DESC,
+ first: RUNNER_PAGE_SIZE,
+ after: runnersDataPaginated.data.runners.pageInfo.endCursor,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/runner_list/runner_search_utils_spec.js b/spec/frontend/runner/runner_list/runner_search_utils_spec.js
new file mode 100644
index 00000000000..a1f33e9c880
--- /dev/null
+++ b/spec/frontend/runner/runner_list/runner_search_utils_spec.js
@@ -0,0 +1,239 @@
+import { RUNNER_PAGE_SIZE } from '~/runner/constants';
+import {
+ fromUrlQueryToSearch,
+ fromSearchToUrl,
+ fromSearchToVariables,
+} from '~/runner/runner_list/runner_search_utils';
+
+describe('search_params.js', () => {
+ const examples = [
+ {
+ name: 'a default query',
+ urlQuery: '',
+ search: { filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' },
+ graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'a single status',
+ urlQuery: '?status[]=ACTIVE',
+ search: {
+ filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'a single term text search',
+ urlQuery: '?search=something',
+ search: {
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'something' },
+ },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'a two terms text search',
+ urlQuery: '?search=something+else',
+ search: {
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'something' },
+ },
+ {
+ type: 'filtered-search-term',
+ value: { data: 'else' },
+ },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'single instance type',
+ urlQuery: '?runner_type[]=INSTANCE_TYPE',
+ search: {
+ filters: [{ type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } }],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'multiple runner status',
+ urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
+ search: {
+ filters: [
+ { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'status', value: { data: 'PAUSED', operator: '=' } },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'multiple status, a single instance type and a non default sort',
+ urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
+ search: {
+ filters: [
+ { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
+ ],
+ pagination: { page: 1 },
+ sort: 'CREATED_ASC',
+ },
+ graphqlVariables: {
+ status: 'ACTIVE',
+ type: 'INSTANCE_TYPE',
+ sort: 'CREATED_ASC',
+ first: RUNNER_PAGE_SIZE,
+ },
+ },
+ {
+ name: 'the next page',
+ urlQuery: '?page=2&after=AFTER_CURSOR',
+ search: { filters: [], pagination: { page: 2, after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC' },
+ graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE },
+ },
+ {
+ name: 'the previous page',
+ urlQuery: '?page=2&before=BEFORE_CURSOR',
+ search: {
+ filters: [],
+ pagination: { page: 2, before: 'BEFORE_CURSOR' },
+ sort: 'CREATED_DESC',
+ },
+ graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE },
+ },
+ {
+ name:
+ 'the next page filtered by multiple status, a single instance type and a non default sort',
+ urlQuery:
+ '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC&page=2&after=AFTER_CURSOR',
+ search: {
+ filters: [
+ { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'runner_type', value: { data: 'INSTANCE_TYPE', operator: '=' } },
+ ],
+ pagination: { page: 2, after: 'AFTER_CURSOR' },
+ sort: 'CREATED_ASC',
+ },
+ graphqlVariables: {
+ status: 'ACTIVE',
+ type: 'INSTANCE_TYPE',
+ sort: 'CREATED_ASC',
+ after: 'AFTER_CURSOR',
+ first: RUNNER_PAGE_SIZE,
+ },
+ },
+ ];
+
+ describe('fromUrlQueryToSearch', () => {
+ examples.forEach(({ name, urlQuery, search }) => {
+ it(`Converts ${name} to a search object`, () => {
+ expect(fromUrlQueryToSearch(urlQuery)).toEqual(search);
+ });
+ });
+
+ it('When search params appear as array, they are concatenated', () => {
+ expect(fromUrlQueryToSearch('?search[]=my&search[]=text').filters).toEqual([
+ { type: 'filtered-search-term', value: { data: 'my' } },
+ { type: 'filtered-search-term', value: { data: 'text' } },
+ ]);
+ });
+
+ it('When a page cannot be parsed as a number, it defaults to `1`', () => {
+ expect(fromUrlQueryToSearch('?page=NONSENSE&after=AFTER_CURSOR').pagination).toEqual({
+ page: 1,
+ });
+ });
+
+ it('When a page is less than 1, it defaults to `1`', () => {
+ expect(fromUrlQueryToSearch('?page=0&after=AFTER_CURSOR').pagination).toEqual({
+ page: 1,
+ });
+ });
+
+ it('When a page with no cursor is given, it defaults to `1`', () => {
+ expect(fromUrlQueryToSearch('?page=2').pagination).toEqual({
+ page: 1,
+ });
+ });
+ });
+
+ describe('fromSearchToUrl', () => {
+ examples.forEach(({ name, urlQuery, search }) => {
+ it(`Converts ${name} to a url`, () => {
+ expect(fromSearchToUrl(search)).toEqual(`http://test.host/${urlQuery}`);
+ });
+ });
+
+ it.each([
+ 'http://test.host/?status[]=ACTIVE',
+ 'http://test.host/?runner_type[]=INSTANCE_TYPE',
+ 'http://test.host/?search=my_text',
+ ])('When a filter is removed, it is removed from the URL', (initalUrl) => {
+ const search = { filters: [], sort: 'CREATED_DESC' };
+ const expectedUrl = `http://test.host/`;
+
+ expect(fromSearchToUrl(search, initalUrl)).toEqual(expectedUrl);
+ });
+
+ it('When unrelated search parameter is present, it does not get removed', () => {
+ const initialUrl = `http://test.host/?unrelated=UNRELATED&status[]=ACTIVE`;
+ const search = { filters: [], sort: 'CREATED_DESC' };
+ const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
+
+ expect(fromSearchToUrl(search, initialUrl)).toEqual(expectedUrl);
+ });
+ });
+
+ describe('fromSearchToVariables', () => {
+ examples.forEach(({ name, graphqlVariables, search }) => {
+ it(`Converts ${name} to a GraphQL query variables object`, () => {
+ expect(fromSearchToVariables(search)).toEqual(graphqlVariables);
+ });
+ });
+
+ it('When a search param is empty, it gets removed', () => {
+ expect(
+ fromSearchToVariables({
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: '' },
+ },
+ ],
+ }),
+ ).toMatchObject({
+ search: '',
+ });
+
+ expect(
+ fromSearchToVariables({
+ filters: [
+ {
+ type: 'filtered-search-term',
+ value: { data: 'something' },
+ },
+ {
+ type: 'filtered-search-term',
+ value: { data: '' },
+ },
+ ],
+ }),
+ ).toMatchObject({
+ search: 'something',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index d076997b04a..fbe01f372b0 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -2,47 +2,49 @@ export const MOCK_QUERY = {
scope: 'issues',
state: 'all',
confidential: null,
- group_id: 'test_1',
+ group_id: 1,
};
export const MOCK_GROUP = {
name: 'test group',
- full_name: 'full name test group',
- id: 'test_1',
+ full_name: 'full name / test group',
+ id: 1,
};
export const MOCK_GROUPS = [
{
+ avatar_url: null,
name: 'test group',
- full_name: 'full name test group',
- id: 'test_1',
+ full_name: 'full name / test group',
+ id: 1,
},
{
+ avatar_url: 'https://avatar.com',
name: 'test group 2',
- full_name: 'full name test group 2',
- id: 'test_2',
+ full_name: 'full name / test group 2',
+ id: 2,
},
];
export const MOCK_PROJECT = {
name: 'test project',
namespace: MOCK_GROUP,
- nameWithNamespace: 'test group test project',
- id: 'test_1',
+ nameWithNamespace: 'test group / test project',
+ id: 1,
};
export const MOCK_PROJECTS = [
{
name: 'test project',
namespace: MOCK_GROUP,
- name_with_namespace: 'test group test project',
- id: 'test_1',
+ name_with_namespace: 'test group / test project',
+ id: 1,
},
{
name: 'test project 2',
namespace: MOCK_GROUP,
- name_with_namespace: 'test group test project 2',
- id: 'test_2',
+ name_with_namespace: 'test group / test project 2',
+ id: 2,
},
];
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index ab622c53387..634661c5843 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -20,9 +20,8 @@ describe('Global Search Store Actions', () => {
let mock;
let state;
- const noCallback = () => {};
- const flashCallback = () => {
- expect(createFlash).toHaveBeenCalledTimes(1);
+ const flashCallback = (callCount) => {
+ expect(createFlash).toHaveBeenCalledTimes(callCount);
createFlash.mockClear();
};
@@ -37,19 +36,21 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | callback
- ${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${noCallback}
- ${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${flashCallback}
- ${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${noCallback}
- ${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${flashCallback}
- `(`axios calls`, ({ action, axiosMock, type, expectedMutations, callback }) => {
+ 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 }]} | ${2}
+ `(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
});
it(`should dispatch the correct mutations`, () => {
- return testAction({ action, state, expectedMutations }).then(() => callback());
+ return testAction({ action, state, expectedMutations }).then(() =>
+ flashCallback(flashCallCount),
+ );
});
});
});
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
new file mode 100644
index 00000000000..e51fe9a4cf9
--- /dev/null
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
@@ -0,0 +1,97 @@
+import { GlDropdownItem, GlAvatar } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { MOCK_GROUPS } from 'jest/search/mock_data';
+import { truncateNamespace } from '~/lib/utils/text_utility';
+import SearchableDropdownItem from '~/search/topbar/components/searchable_dropdown_item.vue';
+import { GROUP_DATA } from '~/search/topbar/constants';
+
+describe('Global Search Searchable Dropdown Item', () => {
+ let wrapper;
+
+ const defaultProps = {
+ item: MOCK_GROUPS[0],
+ selectedItem: MOCK_GROUPS[0],
+ name: GROUP_DATA.name,
+ fullName: GROUP_DATA.fullName,
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(SearchableDropdownItem, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findGlAvatar = () => wrapper.findComponent(GlAvatar);
+ const findDropdownTitle = () => wrapper.findByTestId('item-title');
+ const findDropdownSubtitle = () => wrapper.findByTestId('item-namespace');
+
+ describe('template', () => {
+ describe('always', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders GlDropdownItem', () => {
+ expect(findGlDropdownItem().exists()).toBe(true);
+ });
+
+ it('renders GlAvatar', () => {
+ expect(findGlAvatar().exists()).toBe(true);
+ });
+
+ it('renders Dropdown Title correctly', () => {
+ const titleEl = findDropdownTitle();
+
+ expect(titleEl.exists()).toBe(true);
+ expect(titleEl.text()).toBe(MOCK_GROUPS[0][GROUP_DATA.name]);
+ });
+
+ it('renders Dropdown Subtitle correctly', () => {
+ const subtitleEl = findDropdownSubtitle();
+
+ expect(subtitleEl.exists()).toBe(true);
+ expect(subtitleEl.text()).toBe(truncateNamespace(MOCK_GROUPS[0][GROUP_DATA.fullName]));
+ });
+ });
+
+ describe('when item === selectedItem', () => {
+ beforeEach(() => {
+ createComponent({ item: MOCK_GROUPS[0], selectedItem: MOCK_GROUPS[0] });
+ });
+
+ it('marks the dropdown as checked', () => {
+ expect(findGlDropdownItem().attributes('ischecked')).toBe('true');
+ });
+ });
+
+ describe('when item !== selectedItem', () => {
+ beforeEach(() => {
+ createComponent({ item: MOCK_GROUPS[0], selectedItem: MOCK_GROUPS[1] });
+ });
+
+ it('marks the dropdown as not checked', () => {
+ expect(findGlDropdownItem().attributes('ischecked')).toBeUndefined();
+ });
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('clicking the dropdown item $emits change with the item', () => {
+ findGlDropdownItem().vm.$emit('click');
+
+ expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]);
+ });
+ });
+});
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index 5de948592d4..10d779f0f90 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -1,20 +1,21 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
-import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
import SearchableDropdown from '~/search/topbar/components/searchable_dropdown.vue';
+import SearchableDropdownItem from '~/search/topbar/components/searchable_dropdown_item.vue';
import { ANY_OPTION, GROUP_DATA } from '~/search/topbar/constants';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(Vuex);
describe('Global Search Searchable Dropdown', () => {
let wrapper;
const defaultProps = {
headerText: GROUP_DATA.headerText,
- selectedDisplayValue: GROUP_DATA.selectedDisplayValue,
- itemsDisplayValue: GROUP_DATA.itemsDisplayValue,
+ name: GROUP_DATA.name,
+ fullName: GROUP_DATA.fullName,
loading: false,
selectedItem: ANY_OPTION,
items: [],
@@ -29,7 +30,6 @@ describe('Global Search Searchable Dropdown', () => {
});
wrapper = mountFn(SearchableDropdown, {
- localVue,
store,
propsData: {
...defaultProps,
@@ -40,17 +40,16 @@ describe('Global Search Searchable Dropdown', () => {
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- const findGlDropdown = () => wrapper.find(GlDropdown);
- const findGlDropdownSearch = () => findGlDropdown().find(GlSearchBoxByType);
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType);
const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text');
- const findDropdownItems = () => findGlDropdown().findAll(GlDropdownItem);
- const findDropdownItemsText = () => findDropdownItems().wrappers.map((w) => w.text());
- const findAnyDropdownItem = () => findDropdownItems().at(0);
- const findFirstGroupDropdownItem = () => findDropdownItems().at(1);
- const findLoader = () => wrapper.find(GlSkeletonLoader);
+ const findSearchableDropdownItems = () =>
+ findGlDropdown().findAllComponents(SearchableDropdownItem);
+ const findAnyDropdownItem = () => findGlDropdown().findComponent(GlDropdownItem);
+ const findFirstGroupDropdownItem = () => findSearchableDropdownItems().at(0);
+ const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
describe('template', () => {
beforeEach(() => {
@@ -93,9 +92,12 @@ describe('Global Search Searchable Dropdown', () => {
expect(findLoader().exists()).toBe(false);
});
- it('renders an instance for each namespace', () => {
- const resultsIncludeAny = ['Any'].concat(MOCK_GROUPS.map((n) => n.full_name));
- expect(findDropdownItemsText()).toStrictEqual(resultsIncludeAny);
+ it('renders the Any Dropdown', () => {
+ expect(findAnyDropdownItem().exists()).toBe(true);
+ });
+
+ it('renders SearchableDropdownItem for each item', () => {
+ expect(findSearchableDropdownItems()).toHaveLength(MOCK_GROUPS.length);
});
});
@@ -108,18 +110,12 @@ describe('Global Search Searchable Dropdown', () => {
expect(findLoader().exists()).toBe(true);
});
- it('renders only Any in dropdown', () => {
- expect(findDropdownItemsText()).toStrictEqual(['Any']);
- });
- });
-
- describe('when item is selected', () => {
- beforeEach(() => {
- createComponent({}, { items: MOCK_GROUPS, selectedItem: MOCK_GROUPS[0] });
+ it('renders the Any Dropdown', () => {
+ expect(findAnyDropdownItem().exists()).toBe(true);
});
- it('marks the dropdown as checked', () => {
- expect(findFirstGroupDropdownItem().attributes('ischecked')).toBe('true');
+ it('does not render SearchableDropdownItem', () => {
+ expect(findSearchableDropdownItems()).toHaveLength(0);
});
});
});
@@ -140,8 +136,8 @@ describe('Global Search Searchable Dropdown', () => {
createComponent({}, { selectedItem: MOCK_GROUP }, mount);
});
- it('sets dropdown text to the selectedItem selectedDisplayValue', () => {
- expect(findDropdownText().text()).toBe(MOCK_GROUP[GROUP_DATA.selectedDisplayValue]);
+ it('sets dropdown text to the selectedItem name', () => {
+ expect(findDropdownText().text()).toBe(MOCK_GROUP[GROUP_DATA.name]);
});
});
});
@@ -158,8 +154,8 @@ describe('Global Search Searchable Dropdown', () => {
expect(wrapper.emitted('change')[0]).toEqual([ANY_OPTION]);
});
- it('clicking result dropdown item $emits @change with result', () => {
- findFirstGroupDropdownItem().vm.$emit('click');
+ it('on SearchableDropdownItem @change, the wrapper $emits change with the item', () => {
+ findFirstGroupDropdownItem().vm.$emit('change', MOCK_GROUPS[0]);
expect(wrapper.emitted('change')[0]).toEqual([MOCK_GROUPS[0]]);
});
diff --git a/spec/frontend/security_configuration/components/redesigned_app_spec.js b/spec/frontend/security_configuration/components/redesigned_app_spec.js
new file mode 100644
index 00000000000..7e27a3e1108
--- /dev/null
+++ b/spec/frontend/security_configuration/components/redesigned_app_spec.js
@@ -0,0 +1,232 @@
+import { GlTab } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ SAST_NAME,
+ SAST_SHORT_NAME,
+ SAST_DESCRIPTION,
+ SAST_HELP_PATH,
+ SAST_CONFIG_HELP_PATH,
+ LICENSE_COMPLIANCE_NAME,
+ LICENSE_COMPLIANCE_DESCRIPTION,
+ LICENSE_COMPLIANCE_HELP_PATH,
+} from '~/security_configuration/components/constants';
+import FeatureCard from '~/security_configuration/components/feature_card.vue';
+import RedesignedSecurityConfigurationApp, {
+ i18n,
+} from '~/security_configuration/components/redesigned_app.vue';
+import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
+import {
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+ REPORT_TYPE_SAST,
+} from '~/vue_shared/security_reports/constants';
+
+const upgradePath = '/upgrade';
+
+describe('redesigned App component', () => {
+ let wrapper;
+ let userCalloutDismissSpy;
+
+ const createComponent = ({ shouldShowCallout = true, ...propsData }) => {
+ userCalloutDismissSpy = jest.fn();
+
+ wrapper = extendedWrapper(
+ mount(RedesignedSecurityConfigurationApp, {
+ propsData,
+ provide: {
+ upgradePath,
+ },
+ stubs: {
+ UserCalloutDismisser: makeMockUserCalloutDismisser({
+ dismiss: userCalloutDismissSpy,
+ shouldShowCallout,
+ }),
+ },
+ }),
+ );
+ };
+
+ const findMainHeading = () => wrapper.find('h1');
+ const findTab = () => wrapper.findComponent(GlTab);
+ const findTabs = () => wrapper.findAllComponents(GlTab);
+ const findByTestId = (id) => wrapper.findByTestId(id);
+ const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
+ const findComplianceViewHistoryLink = () => findByTestId('compliance-view-history-link');
+ const findSecurityViewHistoryLink = () => findByTestId('security-view-history-link');
+ const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
+
+ const securityFeaturesMock = [
+ {
+ name: SAST_NAME,
+ shortName: SAST_SHORT_NAME,
+ description: SAST_DESCRIPTION,
+ helpPath: SAST_HELP_PATH,
+ configurationHelpPath: SAST_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST,
+ available: true,
+ },
+ ];
+
+ const complianceFeaturesMock = [
+ {
+ name: LICENSE_COMPLIANCE_NAME,
+ description: LICENSE_COMPLIANCE_DESCRIPTION,
+ helpPath: LICENSE_COMPLIANCE_HELP_PATH,
+ type: REPORT_TYPE_LICENSE_COMPLIANCE,
+ configurationHelpPath: LICENSE_COMPLIANCE_HELP_PATH,
+ },
+ ];
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('basic structure', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ });
+ });
+
+ it('renders main-heading with correct text', () => {
+ const mainHeading = findMainHeading();
+ expect(mainHeading).toExist();
+ expect(mainHeading.text()).toContain('Security Configuration');
+ });
+
+ it('renders GlTab Component ', () => {
+ expect(findTab()).toExist();
+ });
+
+ it('renders right amount of tabs with correct title ', () => {
+ expect(findTabs()).toHaveLength(2);
+ });
+
+ it('renders security-testing tab', () => {
+ expect(findByTestId('security-testing-tab').exists()).toBe(true);
+ });
+
+ it('renders compliance-testing tab', () => {
+ expect(findByTestId('compliance-testing-tab').exists()).toBe(true);
+ });
+
+ it('renders right amount of feature cards for given props with correct props', () => {
+ const cards = findFeatureCards();
+ expect(cards).toHaveLength(2);
+ expect(cards.at(0).props()).toEqual({ feature: securityFeaturesMock[0] });
+ expect(cards.at(1).props()).toEqual({ feature: complianceFeaturesMock[0] });
+ });
+
+ it('should not show latest pipeline link when latestPipelinePath is not defined', () => {
+ expect(findByTestId('latest-pipeline-info').exists()).toBe(false);
+ });
+
+ it('should not show configuration History Link when gitlabCiPresent & gitlabCiHistoryPath are not defined', () => {
+ expect(findComplianceViewHistoryLink().exists()).toBe(false);
+ expect(findSecurityViewHistoryLink().exists()).toBe(false);
+ });
+ });
+
+ describe('upgrade banner', () => {
+ const makeAvailable = (available) => (feature) => ({ ...feature, available });
+
+ describe('given at least one unavailable feature', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(false)),
+ });
+ });
+
+ it('renders the banner', () => {
+ expect(findUpgradeBanner().exists()).toBe(true);
+ });
+
+ it('calls the dismiss callback when closing the banner', () => {
+ expect(userCalloutDismissSpy).not.toHaveBeenCalled();
+
+ findUpgradeBanner().vm.$emit('close');
+
+ expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('given at least one unavailable feature, but banner is already dismissed', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(false)),
+ shouldShowCallout: false,
+ });
+ });
+
+ it('does not render the banner', () => {
+ expect(findUpgradeBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('given all features are available', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock.map(makeAvailable(true)),
+ augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(true)),
+ });
+ });
+
+ it('does not render the banner', () => {
+ expect(findUpgradeBanner().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when given latestPipelinePath props', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ latestPipelinePath: 'test/path',
+ });
+ });
+
+ it('should show latest pipeline info on the security tab with correct link when latestPipelinePath is defined', () => {
+ const latestPipelineInfoSecurity = findByTestId('latest-pipeline-info-security');
+
+ expect(latestPipelineInfoSecurity.exists()).toBe(true);
+ expect(latestPipelineInfoSecurity.text()).toMatchInterpolatedText(
+ i18n.securityTestingDescription,
+ );
+ expect(latestPipelineInfoSecurity.find('a').attributes('href')).toBe('test/path');
+ });
+
+ it('should show latest pipeline info on the compliance tab with correct link when latestPipelinePath is defined', () => {
+ const latestPipelineInfoCompliance = findByTestId('latest-pipeline-info-compliance');
+
+ expect(latestPipelineInfoCompliance.exists()).toBe(true);
+ expect(latestPipelineInfoCompliance.text()).toMatchInterpolatedText(
+ i18n.securityTestingDescription,
+ );
+ expect(latestPipelineInfoCompliance.find('a').attributes('href')).toBe('test/path');
+ });
+ });
+
+ describe('given gitlabCiPresent & gitlabCiHistoryPath props', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ augmentedComplianceFeatures: complianceFeaturesMock,
+ gitlabCiPresent: true,
+ gitlabCiHistoryPath: 'test/historyPath',
+ });
+ });
+
+ it('should show configuration History Link', () => {
+ expect(findComplianceViewHistoryLink().exists()).toBe(true);
+ expect(findSecurityViewHistoryLink().exists()).toBe(true);
+
+ expect(findComplianceViewHistoryLink().attributes('href')).toBe('test/historyPath');
+ expect(findSecurityViewHistoryLink().attributes('href')).toBe('test/historyPath');
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/components/section_layout_spec.js b/spec/frontend/security_configuration/components/section_layout_spec.js
new file mode 100644
index 00000000000..75da380bbb8
--- /dev/null
+++ b/spec/frontend/security_configuration/components/section_layout_spec.js
@@ -0,0 +1,49 @@
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import SectionLayout from '~/security_configuration/components/section_layout.vue';
+
+describe('Section Layout component', () => {
+ let wrapper;
+
+ const createComponent = (propsData) => {
+ wrapper = extendedWrapper(
+ mount(SectionLayout, {
+ propsData,
+ scopedSlots: {
+ description: '<span>foo</span>',
+ features: '<span>bar</span>',
+ },
+ }),
+ );
+ };
+
+ const findHeading = () => wrapper.find('h2');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('basic structure', () => {
+ beforeEach(() => {
+ createComponent({ heading: 'testheading' });
+ });
+
+ const slots = {
+ description: 'foo',
+ features: 'bar',
+ };
+
+ it('should render heading when passed in as props', () => {
+ expect(findHeading().exists()).toBe(true);
+ expect(findHeading().text()).toBe('testheading');
+ });
+
+ Object.keys(slots).forEach((slot) => {
+ it('renders the slots', () => {
+ const slotContent = slots[slot];
+ createComponent({ heading: '' });
+ expect(wrapper.text()).toContain(slotContent);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/components/upgrade_banner_spec.js b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
new file mode 100644
index 00000000000..cf7945343af
--- /dev/null
+++ b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
@@ -0,0 +1,60 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
+
+const upgradePath = '/upgrade';
+
+describe('UpgradeBanner component', () => {
+ let wrapper;
+ let closeSpy;
+
+ const createComponent = (propsData) => {
+ closeSpy = jest.fn();
+
+ wrapper = shallowMountExtended(UpgradeBanner, {
+ provide: {
+ upgradePath,
+ },
+ propsData,
+ listeners: {
+ close: closeSpy,
+ },
+ });
+ };
+
+ const findGlBanner = () => wrapper.findComponent(GlBanner);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('passes the expected props to GlBanner', () => {
+ expect(findGlBanner().props()).toMatchObject({
+ title: UpgradeBanner.i18n.title,
+ buttonText: UpgradeBanner.i18n.buttonText,
+ buttonLink: upgradePath,
+ });
+ });
+
+ it('renders the list of benefits', () => {
+ const wrapperText = wrapper.text();
+
+ expect(wrapperText).toContain('GitLab Ultimate checks your application');
+ expect(wrapperText).toContain('statistics in the merge request');
+ expect(wrapperText).toContain('statistics across projects');
+ expect(wrapperText).toContain('Runtime security metrics');
+ expect(wrapperText).toContain('risk analysis and remediation');
+ });
+
+ it(`re-emits GlBanner's close event`, () => {
+ expect(closeSpy).not.toHaveBeenCalled();
+
+ wrapper.findComponent(GlBanner).vm.$emit('close');
+
+ expect(closeSpy).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/security_configuration/utils_spec.js b/spec/frontend/security_configuration/utils_spec.js
new file mode 100644
index 00000000000..6ad167cadda
--- /dev/null
+++ b/spec/frontend/security_configuration/utils_spec.js
@@ -0,0 +1,81 @@
+import { augmentFeatures } from '~/security_configuration/utils';
+
+const mockSecurityFeatures = [
+ {
+ name: 'SAST',
+ type: 'SAST',
+ },
+];
+
+const mockComplianceFeatures = [
+ {
+ name: 'LICENSE_COMPLIANCE',
+ type: 'LICENSE_COMPLIANCE',
+ },
+];
+
+const mockFeaturesWithSecondary = [
+ {
+ name: 'DAST',
+ type: 'DAST',
+ secondary: {
+ type: 'DAST PROFILES',
+ name: 'DAST PROFILES',
+ },
+ },
+];
+
+const mockInvalidCustomFeature = [
+ {
+ foo: 'bar',
+ },
+];
+
+const mockValidCustomFeature = [
+ {
+ name: 'SAST',
+ type: 'SAST',
+ customfield: 'customvalue',
+ },
+];
+
+const expectedOutputDefault = {
+ augmentedSecurityFeatures: mockSecurityFeatures,
+ augmentedComplianceFeatures: mockComplianceFeatures,
+};
+
+const expectedOutputSecondary = {
+ augmentedSecurityFeatures: mockSecurityFeatures,
+ augmentedComplianceFeatures: mockFeaturesWithSecondary,
+};
+
+const expectedOutputCustomFeature = {
+ augmentedSecurityFeatures: mockValidCustomFeature,
+ augmentedComplianceFeatures: mockComplianceFeatures,
+};
+
+describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => {
+ it('given an empty array', () => {
+ expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual(
+ expectedOutputDefault,
+ );
+ });
+
+ it('given an invalid populated array', () => {
+ expect(
+ augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature),
+ ).toEqual(expectedOutputDefault);
+ });
+
+ it('features have secondary key', () => {
+ expect(augmentFeatures(mockSecurityFeatures, mockFeaturesWithSecondary, [])).toEqual(
+ expectedOutputSecondary,
+ );
+ });
+
+ it('given a valid populated array', () => {
+ expect(
+ augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature),
+ ).toEqual(expectedOutputCustomFeature);
+ });
+});
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
index 226e580a8e8..523f4e88985 100644
--- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
+++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
@@ -45,7 +45,9 @@ exports[`self monitor component When the self monitor project has not been creat
Enabling this feature creates a project that can be used to monitor the health of your instance.
</p>
- <gl-form-group-stub>
+ <gl-form-group-stub
+ labeldescription=""
+ >
<gl-toggle-stub
label="Create Project"
labelposition="top"
diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
index 33df3a66fcd..36f6746b754 100644
--- a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
@@ -11,7 +11,7 @@ exports[`EmptyStateComponent should render content 1`] = `
<p>In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. <gl-link-stub href=\\"/help\\">More information</gl-link-stub>
</p>
<div class=\\"gl-display-flex gl-flex-wrap gl-justify-content-center\\">
- <gl-button-stub category=\\"primary\\" variant=\\"confirm\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" href=\\"/clusters\\" class=\\"gl-mb-3 gl-mx-2\\">Install Knative</gl-button-stub>
+ <!---->
<!---->
</div>
</div>
diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js
index d5b187452c6..1b93fd784e1 100644
--- a/spec/frontend/serverless/components/missing_prometheus_spec.js
+++ b/spec/frontend/serverless/components/missing_prometheus_spec.js
@@ -21,7 +21,7 @@ describe('missingPrometheusComponent', () => {
const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
- 'Function invocation metrics require Prometheus to be installed first.',
+ 'Function invocation metrics require the Prometheus cluster integration.',
);
expect(wrapper.find(GlButton).attributes('variant')).toBe('success');
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index 82fc06e1166..3ff6d1f9597 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -2,7 +2,8 @@ import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { initEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import EmojiPicker from '~/emoji/components/picker.vue';
+import createFlash from '~/flash';
import SetStatusModalWrapper, {
AVAILABILITY_STATUS,
} from '~/set_status_modal/set_status_modal_wrapper.vue';
@@ -25,7 +26,7 @@ describe('SetStatusModalWrapper', () => {
defaultEmoji,
};
- const createComponent = (props = {}) => {
+ const createComponent = (props = {}, improvedEmojiPicker = false) => {
return shallowMount(SetStatusModalWrapper, {
propsData: {
...defaultProps,
@@ -34,6 +35,9 @@ describe('SetStatusModalWrapper', () => {
mocks: {
$toast,
},
+ provide: {
+ glFeatures: { improvedEmojiPicker },
+ },
});
};
@@ -106,6 +110,20 @@ describe('SetStatusModalWrapper', () => {
});
});
+ describe('improvedEmojiPicker is true', () => {
+ beforeEach(async () => {
+ mockEmoji = await initEmojiMock();
+ wrapper = createComponent({}, true);
+ return initModal();
+ });
+
+ it('sets emojiTag when clicking in emoji picker', async () => {
+ await wrapper.findComponent(EmojiPicker).vm.$emit('click', 'thumbsup');
+
+ expect(wrapper.vm.emojiTag).toContain('data-name="thumbsup"');
+ });
+ });
+
describe('with no currentMessage set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
@@ -271,9 +289,9 @@ describe('SetStatusModalWrapper', () => {
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
- expect(createFlash).toHaveBeenCalledWith(
- "Sorry, we weren't able to set your status. Please try again later.",
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: "Sorry, we weren't able to set your status. Please try again later.",
+ });
});
});
});
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index 74dce499999..be27a800418 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -19,7 +19,7 @@ describe('Assignee component', () => {
});
};
- const findComponentTextNoUsers = () => wrapper.find('.assign-yourself');
+ const findComponentTextNoUsers = () => wrapper.find('[data-testid="no-value"]');
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
afterEach(() => {
@@ -64,7 +64,7 @@ describe('Assignee component', () => {
});
jest.spyOn(wrapper.vm, '$emit');
- wrapper.find('.assign-yourself .btn-link').trigger('click');
+ wrapper.find('[data-testid="assign-yourself"]').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('assign-self')).toBeTruthy();
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
index cfbe7227915..b738d931040 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
@@ -4,11 +4,16 @@ import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_
describe('Sidebar invite members component', () => {
let wrapper;
+ const issuableType = 'issue';
const findDirectInviteLink = () => wrapper.findComponent(InviteMembersTrigger);
const createComponent = () => {
- wrapper = shallowMount(SidebarInviteMembers);
+ wrapper = shallowMount(SidebarInviteMembers, {
+ propsData: {
+ issuableType,
+ },
+ });
};
afterEach(() => {
@@ -23,5 +28,9 @@ describe('Sidebar invite members component', () => {
it('renders a direct link to project members path', () => {
expect(findDirectInviteLink().exists()).toBe(true);
});
+
+ it('has expected attributes on the trigger', () => {
+ expect(findDirectInviteLink().props('triggerSource')).toBe('issue-assignee-dropdown');
+ });
});
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index 91cbcc6cc27..619e89beb23 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -22,6 +22,10 @@ describe('Sidebar date Widget', () => {
let fakeApollo;
const date = '2021-04-15';
+ window.gon = {
+ first_day_of_week: 1,
+ };
+
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]');
const findDatePicker = () => wrapper.find(GlDatepicker);
@@ -119,11 +123,12 @@ describe('Sidebar date Widget', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]);
});
- it('uses a correct prop to set the initial date for GlDatePicker', () => {
+ it('uses a correct prop to set the initial date and first day of the week for GlDatePicker', () => {
expect(findDatePicker().props()).toMatchObject({
value: null,
autocomplete: 'off',
defaultDate: expect.any(Object),
+ firstDay: window.gon.first_day_of_week,
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
new file mode 100644
index 00000000000..8d58854b013
--- /dev/null
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -0,0 +1,503 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlLink,
+ GlSearchBoxByType,
+ GlFormInput,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { IssuableType } from '~/issue_show/constants';
+import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
+import { IssuableAttributeType } from '~/sidebar/constants';
+import projectIssueMilestoneMutation from '~/sidebar/queries/project_issue_milestone.mutation.graphql';
+import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+
+import {
+ mockIssue,
+ mockProjectMilestonesResponse,
+ noCurrentMilestoneResponse,
+ mockMilestoneMutationResponse,
+ mockMilestone2,
+ emptyProjectMilestonesResponse,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+
+describe('SidebarDropdownWidget', () => {
+ let wrapper;
+ let mockApollo;
+
+ const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } };
+ const firstErrorMsg = 'first error';
+ const promiseWithErrors = {
+ ...promiseData,
+ issuableSetAttribute: { ...promiseData.issuableSetAttribute, errors: [firstErrorMsg] },
+ };
+
+ const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData });
+ const mutationError = () =>
+ jest.fn().mockRejectedValue('Failed to set milestone on this issue. Please try again.');
+ const mutationSuccessWithErrors = () => jest.fn().mockResolvedValue({ data: promiseWithErrors });
+
+ const findGlLink = () => wrapper.findComponent(GlLink);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownText = () => wrapper.findComponent(GlDropdownText);
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownItemWithText = (text) =>
+ findAllDropdownItems().wrappers.find((x) => x.text() === text);
+
+ const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]');
+ const findEditableLoadingIcon = () => findSidebarEditableItem().findComponent(GlLoadingIcon);
+ const findAttributeItems = () => wrapper.findByTestId('milestone-items');
+ const findSelectedAttribute = () => wrapper.findByTestId('select-milestone');
+ const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item');
+ const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
+
+ const waitForDropdown = async () => {
+ // BDropdown first changes its `visible` property
+ // in a requestAnimationFrame callback.
+ // It then emits `shown` event in a watcher for `visible`
+ // Hence we need both of these:
+ await waitForPromises();
+ await wrapper.vm.$nextTick();
+ };
+
+ const waitForApollo = async () => {
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ };
+
+ // Used with createComponentWithApollo which uses 'mount'
+ const clickEdit = async () => {
+ await findEditButton().trigger('click');
+
+ await waitForDropdown();
+
+ // We should wait for attributes list to be fetched.
+ await waitForApollo();
+ };
+
+ // Used with createComponent which shallow mounts components
+ const toggleDropdown = async () => {
+ wrapper.vm.$refs.editable.expand();
+
+ await waitForDropdown();
+ };
+
+ const createComponentWithApollo = async ({
+ requestHandlers = [],
+ projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse),
+ currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse),
+ } = {}) => {
+ localVue.use(VueApollo);
+ mockApollo = createMockApollo([
+ [projectMilestonesQuery, projectMilestonesSpy],
+ [projectIssueMilestoneQuery, currentMilestoneSpy],
+ ...requestHandlers,
+ ]);
+
+ wrapper = extendedWrapper(
+ mount(SidebarDropdownWidget, {
+ localVue,
+ provide: { canUpdate: true },
+ apolloProvider: mockApollo,
+ propsData: {
+ workspacePath: mockIssue.projectPath,
+ attrWorkspacePath: mockIssue.projectPath,
+ iid: mockIssue.iid,
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ },
+ attachTo: document.body,
+ }),
+ );
+
+ await waitForApollo();
+ };
+
+ const createComponent = ({ data = {}, mutationPromise = mutationSuccess, queries = {} } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(SidebarDropdownWidget, {
+ provide: { canUpdate: true },
+ data() {
+ return data;
+ },
+ propsData: {
+ workspacePath: '',
+ attrWorkspacePath: '',
+ iid: '',
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ },
+ mocks: {
+ $apollo: {
+ mutate: mutationPromise(),
+ queries: {
+ currentAttribute: { loading: false },
+ attributesList: { loading: false },
+ ...queries,
+ },
+ },
+ },
+ stubs: {
+ SidebarEditableItem,
+ GlSearchBoxByType,
+ GlDropdown,
+ },
+ }),
+ );
+
+ // We need to mock out `showDropdown` which
+ // invokes `show` method of BDropdown used inside GlDropdown.
+ jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not editing', () => {
+ beforeEach(() => {
+ createComponent({
+ data: {
+ currentAttribute: { id: 'id', title: 'title', webUrl: 'webUrl' },
+ },
+ stubs: {
+ GlDropdown,
+ SidebarEditableItem,
+ },
+ });
+ });
+
+ it('shows the current attribute', () => {
+ expect(findSelectedAttribute().text()).toBe('title');
+ });
+
+ it('links to the current attribute', () => {
+ expect(findGlLink().attributes().href).toBe('webUrl');
+ });
+
+ it('does not show a loading spinner next to the heading', () => {
+ expect(findEditableLoadingIcon().exists()).toBe(false);
+ });
+
+ it('shows a loading spinner while fetching the current attribute', () => {
+ createComponent({
+ queries: {
+ currentAttribute: { loading: true },
+ },
+ });
+
+ expect(findEditableLoadingIcon().exists()).toBe(true);
+ });
+
+ it('shows the loading spinner and the title of the selected attribute while updating', () => {
+ createComponent({
+ data: {
+ updating: true,
+ selectedTitle: 'Some milestone title',
+ },
+ queries: {
+ currentAttribute: { loading: false },
+ },
+ });
+
+ expect(findEditableLoadingIcon().exists()).toBe(true);
+ expect(findSelectedAttribute().text()).toBe('Some milestone title');
+ });
+
+ describe('when current attribute does not exist', () => {
+ it('renders "None" as the selected attribute title', () => {
+ createComponent();
+
+ expect(findSelectedAttribute().text()).toBe('None');
+ });
+ });
+ });
+
+ describe('when a user can edit', () => {
+ describe('when user is editing', () => {
+ describe('when rendering the dropdown', () => {
+ it('shows a loading spinner while fetching a list of attributes', async () => {
+ createComponent({
+ queries: {
+ attributesList: { loading: true },
+ },
+ });
+
+ await toggleDropdown();
+
+ expect(findLoadingIconDropdown().exists()).toBe(true);
+ });
+
+ describe('GlDropdownItem with the right title and id', () => {
+ const id = 'id';
+ const title = 'title';
+
+ beforeEach(async () => {
+ createComponent({
+ data: { attributesList: [{ id, title }], currentAttribute: { id, title } },
+ });
+
+ await toggleDropdown();
+ });
+
+ it('does not show a loading spinner', () => {
+ expect(findLoadingIconDropdown().exists()).toBe(false);
+ });
+
+ it('renders title $title', () => {
+ expect(findDropdownItemWithText(title).exists()).toBe(true);
+ });
+
+ it('checks the correct dropdown item', () => {
+ expect(
+ findAllDropdownItems()
+ .filter((w) => w.props('isChecked') === true)
+ .at(0)
+ .text(),
+ ).toBe(title);
+ });
+ });
+
+ describe('when no data is assigned', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await toggleDropdown();
+ });
+
+ it('finds GlDropdownItem with "No milestone"', () => {
+ expect(findNoAttributeItem().text()).toBe('No milestone');
+ });
+
+ it('"No milestone" is checked', () => {
+ expect(findNoAttributeItem().props('isChecked')).toBe(true);
+ });
+
+ it('does not render any dropdown item', () => {
+ expect(findAttributeItems().exists()).toBe(false);
+ });
+ });
+
+ describe('when clicking on dropdown item', () => {
+ describe('when currentAttribute is equal to attribute id', () => {
+ it('does not call setIssueAttribute mutation', async () => {
+ createComponent({
+ data: {
+ attributesList: [{ id: 'id', title: 'title' }],
+ currentAttribute: { id: 'id', title: 'title' },
+ },
+ });
+
+ await toggleDropdown();
+
+ findDropdownItemWithText('title').vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('when currentAttribute is not equal to attribute id', () => {
+ describe('when error', () => {
+ const bootstrapComponent = (mutationResp) => {
+ createComponent({
+ data: {
+ attributesList: [
+ { id: '123', title: '123' },
+ { id: 'id', title: 'title' },
+ ],
+ currentAttribute: '123',
+ },
+ mutationPromise: mutationResp,
+ });
+ };
+
+ describe.each`
+ description | mutationResp | expectedMsg
+ ${'top-level error'} | ${mutationError} | ${'Failed to set milestone on this issue. Please try again.'}
+ ${'user-recoverable error'} | ${mutationSuccessWithErrors} | ${firstErrorMsg}
+ `(`$description`, ({ mutationResp, expectedMsg }) => {
+ beforeEach(async () => {
+ bootstrapComponent(mutationResp);
+
+ await toggleDropdown();
+
+ findDropdownItemWithText('title').vm.$emit('click');
+ });
+
+ it(`calls createFlash with "${expectedMsg}"`, async () => {
+ await wrapper.vm.$nextTick();
+ expect(createFlash).toHaveBeenCalledWith({
+ message: expectedMsg,
+ captureError: true,
+ error: expectedMsg,
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('when a user is searching', () => {
+ describe('when search result is not found', () => {
+ it('renders "No milestone found"', async () => {
+ createComponent();
+
+ await toggleDropdown();
+
+ findSearchBox().vm.$emit('input', 'non existing milestones');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findDropdownText().text()).toBe('No milestone found');
+ });
+ });
+ });
+ });
+ });
+
+ describe('with mock apollo', () => {
+ let error;
+
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException');
+ error = new Error('mayday');
+ });
+
+ describe("when issuable type is 'issue'", () => {
+ describe('when dropdown is expanded and user can edit', () => {
+ let milestoneMutationSpy;
+ beforeEach(async () => {
+ milestoneMutationSpy = jest.fn().mockResolvedValue(mockMilestoneMutationResponse);
+
+ await createComponentWithApollo({
+ requestHandlers: [[projectIssueMilestoneMutation, milestoneMutationSpy]],
+ });
+
+ await clickEdit();
+ });
+
+ it('renders the dropdown on clicking edit', async () => {
+ expect(findDropdown().isVisible()).toBe(true);
+ });
+
+ it('focuses on the input when dropdown is shown', async () => {
+ expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element);
+ });
+
+ describe('when currentAttribute is not equal to attribute id', () => {
+ describe('when update is successful', () => {
+ beforeEach(() => {
+ findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
+ });
+
+ it('calls setIssueAttribute mutation', () => {
+ expect(milestoneMutationSpy).toHaveBeenCalledWith({
+ iid: mockIssue.iid,
+ attributeId: getIdFromGraphQLId(mockMilestone2.id),
+ fullPath: mockIssue.projectPath,
+ });
+ });
+
+ it('sets the value returned from the mutation to currentAttribute', async () => {
+ expect(findSelectedAttribute().text()).toBe(mockMilestone2.title);
+ });
+ });
+ });
+
+ describe('milestones', () => {
+ let projectMilestonesSpy;
+
+ it('should call createFlash if milestones query fails', async () => {
+ await createComponentWithApollo({
+ projectMilestonesSpy: jest.fn().mockRejectedValue(error),
+ });
+
+ await clickEdit();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: wrapper.vm.i18n.listFetchError,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+
+ it('only fetches attributes when dropdown is opened', async () => {
+ projectMilestonesSpy = jest.fn().mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ await createComponentWithApollo({ projectMilestonesSpy });
+
+ expect(projectMilestonesSpy).not.toHaveBeenCalled();
+
+ await clickEdit();
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
+ fullPath: mockIssue.projectPath,
+ title: '',
+ state: 'active',
+ });
+ });
+
+ describe('when a user is searching', () => {
+ const mockSearchTerm = 'foobar';
+
+ beforeEach(async () => {
+ projectMilestonesSpy = jest
+ .fn()
+ .mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ await createComponentWithApollo({ projectMilestonesSpy });
+
+ await clickEdit();
+ });
+
+ it('sends a projectMilestones query with the entered search term "foo"', async () => {
+ findSearchBox().vm.$emit('input', mockSearchTerm);
+ await wrapper.vm.$nextTick();
+
+ // Account for debouncing
+ jest.runAllTimers();
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
+ fullPath: mockIssue.projectPath,
+ title: mockSearchTerm,
+ state: 'active',
+ });
+ });
+ });
+ });
+ });
+
+ describe('currentAttributes', () => {
+ it('should call createFlash if currentAttributes query fails', async () => {
+ await createComponentWithApollo({
+ currentMilestoneSpy: jest.fn().mockRejectedValue(error),
+ });
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: wrapper.vm.i18n.currentFetchError,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 0aa5aa2f691..710fae8ddf7 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -36,7 +36,7 @@ describe('Issuable Time Tracking Report', () => {
issuableId: 1,
issuableType,
},
- propsData: { limitToHours },
+ propsData: { limitToHours, issuableId: '1' },
localVue,
apolloProvider: fakeApollo,
});
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index f26cdcb8b20..e08bd80b18e 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -1,7 +1,11 @@
import { mount } from '@vue/test-utils';
+
import { stubTransition } from 'helpers/stub_transition';
import { createMockDirective } from 'helpers/vue_mock_directive';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
+import SidebarEventHub from '~/sidebar/event_hub';
+
+import { issuableTimeTrackingResponse } from '../../mock_data';
describe('Issuable Time Tracker', () => {
let wrapper;
@@ -13,21 +17,39 @@ describe('Issuable Time Tracker', () => {
const findReportLink = () => findByTestId('reportLink');
const defaultProps = {
- timeEstimate: 10_000, // 2h 46m
- timeSpent: 5_000, // 1h 23m
- humanTimeEstimate: '2h 46m',
- humanTimeSpent: '1h 23m',
limitToHours: false,
+ fullPath: 'gitlab-org/gitlab-test',
+ issuableIid: '1',
+ initialTimeTracking: {
+ ...issuableTimeTrackingResponse.data.workspace.issuable,
+ },
};
- const mountComponent = ({ props = {} } = {}) =>
- mount(TimeTracker, {
+ const issuableTimeTrackingRefetchSpy = jest.fn();
+
+ const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => {
+ return mount(TimeTracker, {
propsData: { ...defaultProps, ...props },
directives: { GlTooltip: createMockDirective() },
stubs: {
transition: stubTransition(),
},
+ provide: {
+ issuableType,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ issuableTimeTracking: {
+ loading,
+ refetch: issuableTimeTrackingRefetchSpy,
+ query: jest.fn().mockResolvedValue(issuableTimeTrackingResponse),
+ },
+ },
+ },
+ },
});
+ };
afterEach(() => {
wrapper.destroy();
@@ -44,13 +66,13 @@ describe('Issuable Time Tracker', () => {
it('should correctly render timeEstimate', () => {
expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
- defaultProps.humanTimeEstimate,
+ defaultProps.initialTimeTracking.humanTimeEstimate,
);
});
- it('should correctly render time_spent', () => {
+ it('should correctly render totalTimeSpent', () => {
expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
- defaultProps.humanTimeSpent,
+ defaultProps.initialTimeTracking.humanTotalTimeSpent,
);
});
});
@@ -78,10 +100,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeEstimate: 100_000, // 1d 3h
- timeSpent: 5_000, // 1h 23m
- humanTimeEstimate: '1d 3h',
- humanTimeSpent: '1h 23m',
+ initialTimeTracking: {
+ timeEstimate: 100_000, // 1d 3h
+ totalTimeSpent: 5_000, // 1h 23m
+ humanTimeEstimate: '1d 3h',
+ humanTotalTimeSpent: '1h 23m',
+ },
},
});
});
@@ -108,8 +132,11 @@ describe('Issuable Time Tracker', () => {
it('should display the remaining meter with the correct background color when over estimate', () => {
wrapper = mountComponent({
props: {
- timeEstimate: 10_000, // 2h 46m
- timeSpent: 20_000_000, // 231 days
+ initialTimeTracking: {
+ ...defaultProps.initialTimeTracking,
+ timeEstimate: 10_000, // 2h 46m
+ totalTimeSpent: 20_000_000, // 231 days
+ },
},
});
@@ -122,8 +149,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => {
wrapper = mountComponent({
props: {
- timeEstimate: 100_000, // 1d 3h
limitToHours: true,
+ initialTimeTracking: {
+ ...defaultProps.initialTimeTracking,
+ timeEstimate: 100_000, // 1d 3h
+ },
},
});
});
@@ -140,10 +170,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(async () => {
wrapper = mountComponent({
props: {
- timeEstimate: 10_000, // 2h 46m
- timeSpent: 0,
- timeEstimateHumanReadable: '2h 46m',
- timeSpentHumanReadable: '',
+ initialTimeTracking: {
+ timeEstimate: 10_000, // 2h 46m
+ totalTimeSpent: 0,
+ humanTimeEstimate: '2h 46m',
+ humanTotalTimeSpent: '',
+ },
},
});
await wrapper.vm.$nextTick();
@@ -159,10 +191,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeEstimate: 0,
- timeSpent: 5_000, // 1h 23m
- timeEstimateHumanReadable: '2h 46m',
- timeSpentHumanReadable: '1h 23m',
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 5_000, // 1h 23m
+ humanTimeEstimate: '2h 46m',
+ humanTotalTimeSpent: '1h 23m',
+ },
},
});
});
@@ -177,10 +211,12 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeEstimate: 0,
- timeSpent: 0,
- timeEstimateHumanReadable: '',
- timeSpentHumanReadable: '',
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 0,
+ humanTimeEstimate: '',
+ humanTotalTimeSpent: '',
+ },
},
});
});
@@ -198,8 +234,11 @@ describe('Issuable Time Tracker', () => {
beforeEach(() => {
wrapper = mountComponent({
props: {
- timeSpent: 0,
- timeSpentHumanReadable: '',
+ initialTimeTracking: {
+ ...defaultProps.initialTimeTracking,
+ totalTimeSpent: 0,
+ humanTotalTimeSpent: '',
+ },
},
});
});
@@ -210,13 +249,20 @@ describe('Issuable Time Tracker', () => {
});
describe('When time spent', () => {
- beforeEach(() => {
+ it('link should appear on issue', () => {
wrapper = mountComponent();
+ expect(findReportLink().exists()).toBe(true);
});
- it('link should appear', () => {
+ it('link should appear on merge request', () => {
+ wrapper = mountComponent({ issuableType: 'merge_request' });
expect(findReportLink().exists()).toBe(true);
});
+
+ it('link should not appear on milestone', () => {
+ wrapper = mountComponent({ issuableType: 'milestone' });
+ expect(findReportLink().exists()).toBe(false);
+ });
});
});
@@ -225,7 +271,16 @@ describe('Issuable Time Tracker', () => {
const findCloseHelpButton = () => findByTestId('closeHelpButton');
beforeEach(async () => {
- wrapper = mountComponent({ props: { timeEstimate: 0, timeSpent: 0 } });
+ wrapper = mountComponent({
+ props: {
+ initialTimeTracking: {
+ timeEstimate: 0,
+ totalTimeSpent: 0,
+ humanTimeEstimate: '',
+ humanTotalTimeSpent: '',
+ },
+ },
+ });
await wrapper.vm.$nextTick();
});
@@ -254,4 +309,14 @@ describe('Issuable Time Tracker', () => {
});
});
});
+
+ describe('Event listeners', () => {
+ it('refetches issuableTimeTracking query when eventHub emits `timeTracker:refresh` event', async () => {
+ SidebarEventHub.$emit('timeTracker:refresh');
+
+ await wrapper.vm.$nextTick();
+
+ expect(issuableTimeTrackingRefetchSpy).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index b052038661a..d6287b93fb9 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -513,4 +513,100 @@ export const participantsQueryResponse = {
},
};
+export const mockGroupPath = 'gitlab-org';
+export const mockProjectPath = `${mockGroupPath}/some-project`;
+
+export const mockIssue = {
+ projectPath: mockProjectPath,
+ iid: '1',
+ groupPath: mockGroupPath,
+};
+
+export const mockIssueId = 'gid://gitlab/Issue/1';
+
+export const mockMilestone1 = {
+ __typename: 'Milestone',
+ id: 'gid://gitlab/Milestone/1',
+ title: 'Foobar Milestone',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1',
+ state: 'active',
+};
+
+export const mockMilestone2 = {
+ __typename: 'Milestone',
+ id: 'gid://gitlab/Milestone/2',
+ title: 'Awesome Milestone',
+ webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2',
+ state: 'active',
+};
+
+export const mockProjectMilestonesResponse = {
+ data: {
+ workspace: {
+ attributes: {
+ nodes: [mockMilestone1, mockMilestone2],
+ },
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+};
+
+export const noCurrentMilestoneResponse = {
+ data: {
+ workspace: {
+ issuable: { id: mockIssueId, attribute: null, __typename: 'Issue' },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockMilestoneMutationResponse = {
+ data: {
+ issuableSetAttribute: {
+ errors: [],
+ issuable: {
+ id: 'gid://gitlab/Issue/1',
+ attribute: {
+ id: 'gid://gitlab/Milestone/2',
+ title: 'Awesome Milestone',
+ state: 'active',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'UpdateIssuePayload',
+ },
+ },
+};
+
+export const emptyProjectMilestonesResponse = {
+ data: {
+ workspace: {
+ attributes: {
+ nodes: [],
+ },
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+};
+
+export const issuableTimeTrackingResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/1',
+ title: 'Commodi incidunt eos eos libero dicta dolores sed.',
+ timeEstimate: 10_000, // 2h 46m
+ totalTimeSpent: 5_000, // 1h 23m
+ humanTimeEstimate: '2h 46m',
+ humanTotalTimeSpent: '1h 23m',
+ },
+ },
+ },
+};
+
export default mockData;
diff --git a/spec/frontend/sidebar/track_invite_members_spec.js b/spec/frontend/sidebar/track_invite_members_spec.js
new file mode 100644
index 00000000000..6c96e4cfc76
--- /dev/null
+++ b/spec/frontend/sidebar/track_invite_members_spec.js
@@ -0,0 +1,37 @@
+import $ from 'jquery';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
+
+describe('Track user dropdown open', () => {
+ let trackingSpy;
+ let dropdownElement;
+
+ beforeEach(() => {
+ document.body.innerHTML = `
+ <div id="dummy-wrapper-element">
+ <div class="js-sidebar-assignee-dropdown">
+ <div class="js-invite-members-track" data-track-event="_track_event_" data-track-label="_track_label_">
+ </div>
+ </div>
+ </div>
+ `;
+
+ dropdownElement = document.querySelector('.js-sidebar-assignee-dropdown');
+ trackingSpy = mockTracking('_category_', dropdownElement, jest.spyOn);
+ document.body.dataset.page = 'some:page';
+
+ trackShowInviteMemberLink(dropdownElement);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('sends a tracking event when the dropdown is opened and contains Invite Members link', () => {
+ $(dropdownElement).trigger('shown.bs.dropdown');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, '_track_event_', {
+ label: '_track_label_',
+ });
+ });
+});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
index 95da67c2bbf..5df69ffb5f8 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
@@ -22,6 +22,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
<gl-form-group-stub
class="gl-mb-0"
id="visibility-level-setting"
+ labeldescription=""
>
<gl-form-radio-group-stub
checked="private"
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index a7ab205ca7b..4b3b21c5507 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -4,7 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
@@ -125,9 +125,9 @@ describe('Snippet Blob Edit component', () => {
it('should call flash', async () => {
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith(
- "Can't fetch content for the blob: Error: Request failed with status code 500",
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: "Can't fetch content for the blob: Error: Request failed with status code 500",
+ });
});
});
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
index 17fb3fe788a..1d6245e9dbb 100644
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -7,8 +7,8 @@ import EditDrawer from '~/static_site_editor/components/edit_drawer.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import UnsavedChangesConfirmDialog from '~/static_site_editor/components/unsaved_changes_confirm_dialog.vue';
-import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
-import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+import { EDITOR_TYPES } from '~/static_site_editor/rich_content_editor/constants';
+import RichContentEditor from '~/static_site_editor/rich_content_editor/rich_content_editor.vue';
import {
sourceContentTitle as title,
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/static_site_editor/rich_content_editor/editor_service_spec.js
index ce2b0d1ddc1..cd0d09c085f 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/editor_service_spec.js
@@ -1,5 +1,5 @@
-import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
-import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
+import buildCustomRenderer from '~/static_site_editor/rich_content_editor/services/build_custom_renderer';
+import buildHTMLToMarkdownRenderer from '~/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer';
import {
generateToolbarItem,
addCustomEventListener,
@@ -9,12 +9,12 @@ import {
insertVideo,
getMarkdown,
getEditorOptions,
-} from '~/vue_shared/components/rich_content_editor/services/editor_service';
-import sanitizeHTML from '~/vue_shared/components/rich_content_editor/services/sanitize_html';
+} from '~/static_site_editor/rich_content_editor/services/editor_service';
+import sanitizeHTML from '~/static_site_editor/rich_content_editor/services/sanitize_html';
-jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer');
-jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer');
-jest.mock('~/vue_shared/components/rich_content_editor/services/sanitize_html');
+jest.mock('~/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer');
+jest.mock('~/static_site_editor/rich_content_editor/services/build_custom_renderer');
+jest.mock('~/static_site_editor/rich_content_editor/services/sanitize_html');
describe('Editor Service', () => {
let mockInstance;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
index 97aecda97d2..86ae016987d 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/add_image_modal_spec.js
@@ -1,8 +1,8 @@
import { GlModal, GlTabs } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { IMAGE_TABS } from '~/vue_shared/components/rich_content_editor/constants';
-import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
-import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue';
+import { IMAGE_TABS } from '~/static_site_editor/rich_content_editor/constants';
+import AddImageModal from '~/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue';
+import UploadImageTab from '~/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue';
describe('Add Image Modal', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab_spec.js
index 81fd059ce4f..11b73d58259 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import UploadImageTab from '~/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue';
+import UploadImageTab from '~/static_site_editor/rich_content_editor/modals/add_image/upload_image_tab.vue';
describe('Upload Image Tab', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js b/spec/frontend/static_site_editor/rich_content_editor/modals/insert_video_modal_spec.js
index 3e9eaf58181..392d31bf039 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/modals/insert_video_modal_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/modals/insert_video_modal_spec.js
@@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
+import InsertVideoModal from '~/static_site_editor/rich_content_editor/modals/insert_video_modal.vue';
describe('Insert Video Modal', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js b/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_integration_spec.js
index 47b1abd2ad2..6c02ec506c6 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_integration_spec.js
@@ -1,8 +1,8 @@
import Editor from '@toast-ui/editor';
-import buildMarkdownToHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
-import { registerHTMLToMarkdownRenderer } from '~/vue_shared/components/rich_content_editor/services/editor_service';
+import buildMarkdownToHTMLRenderer from '~/static_site_editor/rich_content_editor/services/build_custom_renderer';
+import { registerHTMLToMarkdownRenderer } from '~/static_site_editor/rich_content_editor/services/editor_service';
-describe('vue_shared/components/rich_content_editor', () => {
+describe('static_site_editor/rich_content_editor', () => {
let editor;
const buildEditor = () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_spec.js
index 8eb880b3984..3b0d2993a5d 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/rich_content_editor_spec.js
@@ -5,10 +5,10 @@ import {
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
CUSTOM_EVENTS,
-} from '~/vue_shared/components/rich_content_editor/constants';
-import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
-import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
-import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+} from '~/static_site_editor/rich_content_editor/constants';
+import AddImageModal from '~/static_site_editor/rich_content_editor/modals/add_image/add_image_modal.vue';
+import InsertVideoModal from '~/static_site_editor/rich_content_editor/modals/insert_video_modal.vue';
+import RichContentEditor from '~/static_site_editor/rich_content_editor/rich_content_editor.vue';
import {
addCustomEventListener,
@@ -18,9 +18,9 @@ import {
registerHTMLToMarkdownRenderer,
getEditorOptions,
getMarkdown,
-} from '~/vue_shared/components/rich_content_editor/services/editor_service';
+} from '~/static_site_editor/rich_content_editor/services/editor_service';
-jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({
+jest.mock('~/static_site_editor/rich_content_editor/services/editor_service', () => ({
addCustomEventListener: jest.fn(),
removeCustomEventListener: jest.fn(),
addImage: jest.fn(),
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/build_custom_renderer_spec.js
index a823d04024d..202e13e8bff 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/build_custom_renderer_spec.js
@@ -1,4 +1,4 @@
-import buildCustomHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
+import buildCustomHTMLRenderer from '~/static_site_editor/rich_content_editor/services/build_custom_renderer';
describe('Build Custom Renderer Service', () => {
describe('buildCustomHTMLRenderer', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
index 3caf03dabba..c9cba3e8689 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
@@ -1,4 +1,4 @@
-import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
+import buildHTMLToMarkdownRenderer from '~/static_site_editor/rich_content_editor/services/build_html_to_markdown_renderer';
import { attributeDefinition } from './renderers/mock_data';
describe('rich_content_editor/services/html_to_markdown_renderer', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token_spec.js
index 7a7e3055520..ef3ff052cb2 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token_spec.js
@@ -6,7 +6,7 @@ import {
buildUneditableBlockTokens,
buildUneditableInlineTokens,
buildUneditableHtmlAsTextTokens,
-} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+} from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
import {
originInlineToken,
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/mock_data.js
index 407072fb596..407072fb596 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/mock_data.js
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition_spec.js
index 69fd9a67a21..6d96dd3bbca 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition_spec.js
@@ -1,4 +1,4 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_attribute_definition';
import { attributeDefinition } from './mock_data';
describe('rich_content_editor/renderers/render_attribute_definition', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
index 0c59d9f569b..29e2b5b3b16 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
@@ -1,5 +1,5 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text';
-import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_embedded_ruby_text';
+import { renderUneditableLeaf } from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
import { buildMockTextNode, normalTextNode } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
index c1aaed6f0c3..0fda847b688 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js
@@ -1,5 +1,5 @@
-import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline';
+import { buildUneditableInlineTokens } from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_font_awesome_html_inline';
import { normalTextNode } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_heading_spec.js
index 76abc1ec3d8..cf4a90885df 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_heading_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_heading_spec.js
@@ -1,5 +1,5 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_heading';
-import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_heading';
+import * as renderUtils from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
describe('rich_content_editor/renderers/render_heading', () => {
it('canRender delegates to renderUtils.willAlwaysRender', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_html_block_spec.js
index 234f6a4d4ca..9c937ac22f4 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_html_block_spec.js
@@ -1,5 +1,5 @@
-import { buildUneditableHtmlAsTextTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html_block';
+import { buildUneditableHtmlAsTextTokens } from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_html_block';
describe('rich_content_editor/services/renderers/render_html_block', () => {
const htmlBlockNode = {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
index 425d0f41bcd..15fb2c3a430 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js
@@ -1,5 +1,5 @@
-import { buildUneditableInlineTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text';
+import { buildUneditableInlineTokens } from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text';
import { buildMockTextNode, normalTextNode } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
index 470cf9bddaa..6a2b89a8dcf 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
@@ -1,4 +1,4 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_identifier_paragraph';
import { buildMockTextNode } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_list_item_spec.js
index c1ab700535b..1e8e62b9dd2 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_list_item_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_list_item_spec.js
@@ -1,5 +1,5 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_list_item';
-import * as renderUtils from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_list_item';
+import * as renderUtils from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
describe('rich_content_editor/renderers/render_list_item', () => {
it('canRender delegates to renderUtils.willAlwaysRender', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_softbreak_spec.js
index 3c3d2354cb9..d8d1e6ff295 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_softbreak_spec.js
@@ -1,4 +1,4 @@
-import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_softbreak';
+import renderer from '~/static_site_editor/rich_content_editor/services/renderers/render_softbreak';
describe('Render softbreak renderer', () => {
describe('canRender', () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_utils_spec.js
index 7c1809c290c..49b8936a9f7 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/renderers/render_utils_spec.js
@@ -1,13 +1,13 @@
import {
buildUneditableBlockTokens,
buildUneditableOpenTokens,
-} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+} from '~/static_site_editor/rich_content_editor/services/renderers/build_uneditable_token';
import {
renderUneditableLeaf,
renderUneditableBranch,
renderWithAttributeDefinitions,
willAlwaysRender,
-} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+} from '~/static_site_editor/rich_content_editor/services/renderers/render_utils';
import { originToken, uneditableCloseToken, attributeDefinition } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/sanitize_html_spec.js b/spec/frontend/static_site_editor/rich_content_editor/services/sanitize_html_spec.js
index f2182ef60d7..2f2d3beb53d 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/sanitize_html_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/services/sanitize_html_spec.js
@@ -1,4 +1,4 @@
-import sanitizeHTML from '~/vue_shared/components/rich_content_editor/services/sanitize_html';
+import sanitizeHTML from '~/static_site_editor/rich_content_editor/services/sanitize_html';
describe('rich_content_editor/services/sanitize_html', () => {
it.each`
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js b/spec/frontend/static_site_editor/rich_content_editor/toolbar_item_spec.js
index 5a56b499769..c9dcf9cfe2e 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
+++ b/spec/frontend/static_site_editor/rich_content_editor/toolbar_item_spec.js
@@ -1,7 +1,7 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ToolbarItem from '~/vue_shared/components/rich_content_editor/toolbar_item.vue';
+import ToolbarItem from '~/static_site_editor/rich_content_editor/toolbar_item.vue';
describe('Toolbar Item', () => {
let wrapper;
diff --git a/spec/frontend/tracking/get_standard_context_spec.js b/spec/frontend/tracking/get_standard_context_spec.js
new file mode 100644
index 00000000000..b7bdc56b801
--- /dev/null
+++ b/spec/frontend/tracking/get_standard_context_spec.js
@@ -0,0 +1,53 @@
+import { SNOWPLOW_JS_SOURCE } from '~/tracking/constants';
+import getStandardContext from '~/tracking/get_standard_context';
+
+describe('~/tracking/get_standard_context', () => {
+ beforeEach(() => {
+ window.gl = window.gl || {};
+ window.gl.snowplowStandardContext = {};
+ });
+
+ it('returns default object if called without server context', () => {
+ expect(getStandardContext()).toStrictEqual({
+ schema: undefined,
+ data: {
+ source: SNOWPLOW_JS_SOURCE,
+ extra: {},
+ },
+ });
+ });
+
+ it('returns filled object if called with server context', () => {
+ window.gl.snowplowStandardContext = {
+ schema: 'iglu:com.gitlab/gitlab_standard',
+ data: {
+ environment: 'testing',
+ },
+ };
+
+ expect(getStandardContext()).toStrictEqual({
+ schema: 'iglu:com.gitlab/gitlab_standard',
+ data: {
+ environment: 'testing',
+ source: SNOWPLOW_JS_SOURCE,
+ extra: {},
+ },
+ });
+ });
+
+ it('always overrides `source` property', () => {
+ window.gl.snowplowStandardContext = {
+ data: {
+ source: 'custom_source',
+ },
+ };
+
+ expect(getStandardContext().data.source).toBe(SNOWPLOW_JS_SOURCE);
+ });
+
+ it('accepts optional `extra` property', () => {
+ const extra = { foo: 'bar' };
+
+ expect(getStandardContext({ extra }).data.extra).toBe(extra);
+ });
+});
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index dd4c8198b72..d8dae2b2dc0 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -1,14 +1,31 @@
import { setHTMLFixture } from 'helpers/fixtures';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils';
-import Tracking, { initUserTracking, initDefaultTrackers, STANDARD_CONTEXT } from '~/tracking';
+import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
+import getStandardContext from '~/tracking/get_standard_context';
jest.mock('~/experimentation/utils', () => ({ getExperimentData: jest.fn() }));
describe('Tracking', () => {
+ let standardContext;
let snowplowSpy;
let bindDocumentSpy;
let trackLoadEventsSpy;
+ let enableFormTracking;
+
+ beforeAll(() => {
+ window.gl = window.gl || {};
+ window.gl.snowplowStandardContext = {
+ schema: 'iglu:com.gitlab/gitlab_standard',
+ data: {
+ environment: 'testing',
+ source: 'unknown',
+ extra: {},
+ },
+ };
+
+ standardContext = getStandardContext();
+ });
beforeEach(() => {
getExperimentData.mockReturnValue(undefined);
@@ -38,6 +55,10 @@ describe('Tracking', () => {
formTracking: false,
linkClickTracking: false,
pageUnloadTimer: 10,
+ formTrackingConfig: {
+ fields: { allow: [] },
+ forms: { allow: [] },
+ },
});
});
});
@@ -46,12 +67,15 @@ describe('Tracking', () => {
beforeEach(() => {
bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
+ enableFormTracking = jest
+ .spyOn(Tracking, 'enableFormTracking')
+ .mockImplementation(() => null);
});
it('should activate features based on what has been enabled', () => {
initDefaultTrackers();
expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [STANDARD_CONTEXT]);
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [standardContext]);
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
@@ -59,10 +83,11 @@ describe('Tracking', () => {
...window.snowplowOptions,
formTracking: true,
linkClickTracking: true,
+ formTrackingConfig: { forms: { whitelist: ['foo'] }, fields: { whitelist: ['bar'] } },
};
initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking');
+ expect(enableFormTracking).toHaveBeenCalledWith(window.snowplowOptions.formTrackingConfig);
expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking');
});
@@ -84,34 +109,6 @@ describe('Tracking', () => {
navigator.msDoNotTrack = undefined;
});
- describe('builds the standard context', () => {
- let standardContext;
-
- beforeAll(async () => {
- window.gl = window.gl || {};
- window.gl.snowplowStandardContext = {
- schema: 'iglu:com.gitlab/gitlab_standard',
- data: {
- environment: 'testing',
- source: 'unknown',
- },
- };
-
- jest.resetModules();
-
- ({ STANDARD_CONTEXT: standardContext } = await import('~/tracking'));
- });
-
- it('uses server data', () => {
- expect(standardContext.schema).toBe('iglu:com.gitlab/gitlab_standard');
- expect(standardContext.data.environment).toBe('testing');
- });
-
- it('overrides schema source', () => {
- expect(standardContext.data.source).toBe('gitlab-javascript');
- });
- });
-
it('tracks to snowplow (our current tracking system)', () => {
Tracking.event('_category_', '_eventName_', { label: '_label_' });
@@ -122,7 +119,31 @@ describe('Tracking', () => {
'_label_',
undefined,
undefined,
- [STANDARD_CONTEXT],
+ [standardContext],
+ );
+ });
+
+ it('allows adding extra data to the default context', () => {
+ const extra = { foo: 'bar' };
+
+ Tracking.event('_category_', '_eventName_', { extra });
+
+ expect(snowplowSpy).toHaveBeenCalledWith(
+ 'trackStructEvent',
+ '_category_',
+ '_eventName_',
+ undefined,
+ undefined,
+ undefined,
+ [
+ {
+ ...standardContext,
+ data: {
+ ...standardContext.data,
+ extra,
+ },
+ },
+ ],
);
});
@@ -156,26 +177,23 @@ describe('Tracking', () => {
});
describe('.enableFormTracking', () => {
- it('tells snowplow to enable form tracking', () => {
- const config = { forms: { whitelist: [''] }, fields: { whitelist: [''] } };
- Tracking.enableFormTracking(config, ['_passed_context_']);
+ it('tells snowplow to enable form tracking, with only explicit contexts', () => {
+ const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } };
+ Tracking.enableFormTracking(config, ['_passed_context_', standardContext]);
- expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking', config, [
- { data: { source: 'gitlab-javascript' }, schema: undefined },
- '_passed_context_',
- ]);
+ expect(snowplowSpy).toHaveBeenCalledWith(
+ 'enableFormTracking',
+ { forms: { whitelist: ['form-class1'] }, fields: { whitelist: ['input-class1'] } },
+ ['_passed_context_'],
+ );
});
- it('throws an error if no whitelist rules are provided', () => {
- const expectedError = new Error(
- 'Unable to enable form event tracking without whitelist rules.',
- );
+ it('throws an error if no allow rules are provided', () => {
+ const expectedError = new Error('Unable to enable form event tracking without allow rules.');
expect(() => Tracking.enableFormTracking()).toThrow(expectedError);
- expect(() => Tracking.enableFormTracking({ fields: { whitelist: [] } })).toThrow(
- expectedError,
- );
- expect(() => Tracking.enableFormTracking({ fields: { whitelist: [1] } })).not.toThrow(
+ expect(() => Tracking.enableFormTracking({ fields: { allow: true } })).toThrow(expectedError);
+ expect(() => Tracking.enableFormTracking({ fields: { allow: [] } })).not.toThrow(
expectedError,
);
});
@@ -197,7 +215,7 @@ describe('Tracking', () => {
'_label_',
undefined,
undefined,
- [STANDARD_CONTEXT],
+ [standardContext],
);
});
});
@@ -213,13 +231,15 @@ describe('Tracking', () => {
eventSpy = jest.spyOn(Tracking, 'event');
Tracking.bindDocument('_category_'); // only happens once
setHTMLFixture(`
- <input data-track-${term}="click_input1" data-track-label="_label_" value="_value_"/>
- <input data-track-${term}="click_input2" data-track-value="_value_override_" value="_value_"/>
- <input type="checkbox" data-track-${term}="toggle_checkbox" value="_value_" checked/>
+ <input data-track-${term}="click_input1" data-track-label="_label_" value=0 />
+ <input data-track-${term}="click_input2" data-track-value=0 value=0/>
+ <input type="checkbox" data-track-${term}="toggle_checkbox" value=1 checked/>
<input class="dropdown" data-track-${term}="toggle_dropdown"/>
<div data-track-${term}="nested_event"><span class="nested"></span></div>
<input data-track-bogus="click_bogusinput" data-track-label="_label_" value="_value_"/>
<input data-track-${term}="click_input3" data-track-experiment="example" value="_value_"/>
+ <input data-track-${term}="event_with_extra" data-track-extra='{ "foo": "bar" }' />
+ <input data-track-${term}="event_with_invalid_extra" data-track-extra="invalid_json" />
`);
});
@@ -228,7 +248,7 @@ describe('Tracking', () => {
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', {
label: '_label_',
- value: '_value_',
+ value: '0',
});
});
@@ -242,7 +262,7 @@ describe('Tracking', () => {
document.querySelector(`[data-track-${term}="click_input2"]`).click();
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', {
- value: '_value_override_',
+ value: '0',
});
});
@@ -252,13 +272,13 @@ describe('Tracking', () => {
checkbox.click(); // unchecking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
- value: false,
+ value: 0,
});
checkbox.click(); // checking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
- value: '_value_',
+ value: '1',
});
});
@@ -295,6 +315,20 @@ describe('Tracking', () => {
context: { schema: TRACKING_CONTEXT_SCHEMA, data: mockExperimentData },
});
});
+
+ it('supports extra data as JSON', () => {
+ document.querySelector(`[data-track-${term}="event_with_extra"]`).click();
+
+ expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_extra', {
+ extra: { foo: 'bar' },
+ });
+ });
+
+ it('ignores extra if provided JSON is invalid', () => {
+ document.querySelector(`[data-track-${term}="event_with_invalid_extra"]`).click();
+
+ expect(eventSpy).toHaveBeenCalledWith('_category_', 'event_with_invalid_extra', {});
+ });
});
describe.each`
@@ -307,8 +341,8 @@ describe('Tracking', () => {
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
setHTMLFixture(`
- <input data-track-${term}="render" data-track-label="label1" value="_value_" data-track-property="_property_"/>
- <span data-track-${term}="render" data-track-label="label2" data-track-value="_value_">
+ <input data-track-${term}="render" data-track-label="label1" value=1 data-track-property="_property_"/>
+ <span data-track-${term}="render" data-track-label="label2" data-track-value=1>
Something
</span>
<input data-track-${term}="_render_bogus_" data-track-label="label3" value="_value_" data-track-property="_property_"/>
@@ -323,7 +357,7 @@ describe('Tracking', () => {
'render',
{
label: 'label1',
- value: '_value_',
+ value: '1',
property: '_property_',
},
],
@@ -332,7 +366,7 @@ describe('Tracking', () => {
'render',
{
label: 'label2',
- value: '_value_',
+ value: '1',
},
],
]);
diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js
new file mode 100644
index 00000000000..7a33c6faac9
--- /dev/null
+++ b/spec/frontend/user_lists/components/user_lists_spec.js
@@ -0,0 +1,195 @@
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { within } from '@testing-library/dom';
+import { mount, createWrapper } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
+import Api from '~/api';
+import UserListsComponent from '~/user_lists/components/user_lists.vue';
+import UserListsTable from '~/user_lists/components/user_lists_table.vue';
+import createStore from '~/user_lists/store/index';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { userList } from '../../feature_flags/mock_data';
+
+jest.mock('~/api');
+
+Vue.use(Vuex);
+
+describe('~/user_lists/components/user_lists.vue', () => {
+ const mockProvide = {
+ newUserListPath: '/user-lists/new',
+ featureFlagsHelpPagePath: '/help/feature-flags',
+ errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
+ };
+
+ const mockState = {
+ projectId: '1',
+ };
+
+ let wrapper;
+ let store;
+
+ const factory = (provide = mockProvide, fn = mount) => {
+ store = createStore(mockState);
+ wrapper = fn(UserListsComponent, {
+ store,
+ provide,
+ });
+ };
+
+ const newButton = () => within(wrapper.element).queryAllByText('New user list');
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('without permissions', () => {
+ const provideData = {
+ ...mockProvide,
+ newUserListPath: null,
+ };
+
+ beforeEach(() => {
+ Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [], headers: {} });
+ factory(provideData);
+ });
+
+ it('does not render new user list button', () => {
+ expect(newButton()).toHaveLength(0);
+ });
+ });
+
+ describe('loading state', () => {
+ it('renders a loading icon', () => {
+ Api.fetchFeatureFlagUserLists.mockReturnValue(new Promise(() => {}));
+
+ factory();
+
+ const loadingElement = wrapper.findComponent(GlLoadingIcon);
+
+ expect(loadingElement.exists()).toBe(true);
+ expect(loadingElement.props('label')).toEqual('Loading user lists');
+ });
+ });
+
+ describe('successful request', () => {
+ describe('without user lists', () => {
+ let emptyState;
+
+ beforeEach(async () => {
+ Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [], headers: {} });
+
+ factory();
+ await waitForPromises();
+ await Vue.nextTick();
+
+ emptyState = wrapper.findComponent(GlEmptyState);
+ });
+
+ it('should render the empty state', async () => {
+ expect(emptyState.exists()).toBe(true);
+ });
+
+ it('renders new feature flag button', () => {
+ expect(newButton()).not.toHaveLength(0);
+ });
+
+ it('renders generic title', () => {
+ const title = createWrapper(
+ within(emptyState.element).getByText('Get started with user lists'),
+ );
+ expect(title.exists()).toBe(true);
+ });
+
+ it('renders generic description', () => {
+ const description = createWrapper(
+ within(emptyState.element).getByText(
+ 'User lists allow you to define a set of users to use with Feature Flags.',
+ ),
+ );
+ expect(description.exists()).toBe(true);
+ });
+ });
+
+ describe('with paginated user lists', () => {
+ let table;
+
+ beforeEach(async () => {
+ Api.fetchFeatureFlagUserLists.mockResolvedValue({
+ data: [userList],
+ headers: {
+ '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');
+ await Vue.nextTick();
+ table = wrapper.findComponent(UserListsTable);
+ });
+
+ it('should render a table with feature flags', () => {
+ expect(table.exists()).toBe(true);
+ expect(table.props('userLists')).toEqual([userList]);
+ });
+
+ it('renders new feature flag button', () => {
+ expect(newButton()).not.toHaveLength(0);
+ });
+
+ describe('pagination', () => {
+ let pagination;
+
+ beforeEach(() => {
+ pagination = wrapper.findComponent(TablePagination);
+ });
+
+ it('should render pagination', () => {
+ expect(pagination.exists()).toBe(true);
+ });
+
+ it('should make an API request when page is clicked', () => {
+ jest.spyOn(store, 'dispatch');
+ pagination.vm.change('4');
+
+ expect(store.dispatch).toHaveBeenCalledWith('setUserListsOptions', {
+ page: '4',
+ });
+ });
+ });
+ });
+ });
+
+ describe('unsuccessful request', () => {
+ beforeEach(async () => {
+ Api.fetchFeatureFlagUserLists.mockRejectedValue();
+ factory();
+
+ await Vue.nextTick();
+ });
+
+ it('should render error state', () => {
+ const emptyState = wrapper.findComponent(GlEmptyState);
+ const title = createWrapper(
+ within(emptyState.element).getByText('There was an error fetching the user lists.'),
+ );
+ expect(title.exists()).toBe(true);
+ const description = createWrapper(
+ within(emptyState.element).getByText(
+ 'Try again in a few moments or contact your support team.',
+ ),
+ );
+ expect(description.exists()).toBe(true);
+ });
+
+ it('renders new feature flag button', () => {
+ expect(newButton()).not.toHaveLength(0);
+ });
+ });
+});
diff --git a/spec/frontend/feature_flags/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js
index 1b04ecee146..7f4d510a39c 100644
--- a/spec/frontend/feature_flags/components/user_lists_table_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_table_spec.js
@@ -1,8 +1,8 @@
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import * as timeago from 'timeago.js';
-import UserListsTable from '~/feature_flags/components/user_lists_table.vue';
-import { userList } from '../mock_data';
+import UserListsTable from '~/user_lists/components/user_lists_table.vue';
+import { userList } from '../../feature_flags/mock_data';
jest.mock('timeago.js', () => ({
format: jest.fn().mockReturnValue('2 weeks ago'),
@@ -35,7 +35,7 @@ describe('User Lists Table', () => {
it('should set the title for a tooltip on the created stamp', () => {
expect(wrapper.find('[data-testid="ffUserListTimestamp"]').attributes('title')).toBe(
- 'Feb 4, 2020 8:13am GMT+0000',
+ 'Feb 4, 2020 8:13am UTC',
);
});
diff --git a/spec/frontend/user_lists/store/index/actions_spec.js b/spec/frontend/user_lists/store/index/actions_spec.js
new file mode 100644
index 00000000000..c5d7d557de9
--- /dev/null
+++ b/spec/frontend/user_lists/store/index/actions_spec.js
@@ -0,0 +1,203 @@
+import testAction from 'helpers/vuex_action_helper';
+import Api from '~/api';
+import {
+ setUserListsOptions,
+ requestUserLists,
+ receiveUserListsSuccess,
+ receiveUserListsError,
+ fetchUserLists,
+ deleteUserList,
+ receiveDeleteUserListError,
+ clearAlert,
+} from '~/user_lists/store/index/actions';
+import * as types from '~/user_lists/store/index/mutation_types';
+import createState from '~/user_lists/store/index/state';
+import { userList } from '../../../feature_flags/mock_data';
+
+jest.mock('~/api.js');
+
+describe('~/user_lists/store/index/actions', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState({ projectId: '1' });
+ });
+
+ describe('setUserListsOptions', () => {
+ it('should commit SET_USER_LISTS_OPTIONS mutation', (done) => {
+ testAction(
+ setUserListsOptions,
+ { page: '1', scope: 'all' },
+ state,
+ [{ type: types.SET_USER_LISTS_OPTIONS, payload: { page: '1', scope: 'all' } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchUserLists', () => {
+ beforeEach(() => {
+ Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [userList], headers: {} });
+ });
+
+ describe('success', () => {
+ it('dispatches requestUserLists and receiveUserListsSuccess ', (done) => {
+ testAction(
+ fetchUserLists,
+ null,
+ state,
+ [],
+ [
+ {
+ type: 'requestUserLists',
+ },
+ {
+ payload: { data: [userList], headers: {} },
+ type: 'receiveUserListsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ it('dispatches requestUserLists and receiveUserListsError ', (done) => {
+ Api.fetchFeatureFlagUserLists.mockRejectedValue();
+
+ testAction(
+ fetchUserLists,
+ null,
+ state,
+ [],
+ [
+ {
+ type: 'requestUserLists',
+ },
+ {
+ type: 'receiveUserListsError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('requestUserLists', () => {
+ it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
+ testAction(requestUserLists, null, state, [{ type: types.REQUEST_USER_LISTS }], [], done);
+ });
+ });
+
+ describe('receiveUserListsSuccess', () => {
+ it('should commit RECEIVE_USER_LISTS_SUCCESS mutation', (done) => {
+ testAction(
+ receiveUserListsSuccess,
+ { data: [userList], headers: {} },
+ state,
+ [
+ {
+ type: types.RECEIVE_USER_LISTS_SUCCESS,
+ payload: { data: [userList], headers: {} },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveUserListsError', () => {
+ it('should commit RECEIVE_USER_LISTS_ERROR mutation', (done) => {
+ testAction(
+ receiveUserListsError,
+ null,
+ state,
+ [{ type: types.RECEIVE_USER_LISTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('deleteUserList', () => {
+ beforeEach(() => {
+ state.userLists = [userList];
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ Api.deleteFeatureFlagUserList.mockResolvedValue();
+ });
+
+ it('should refresh the user lists', (done) => {
+ testAction(
+ deleteUserList,
+ userList,
+ state,
+ [],
+ [{ type: 'requestDeleteUserList', payload: userList }, { type: 'fetchUserLists' }],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ Api.deleteFeatureFlagUserList.mockRejectedValue({ response: { data: 'some error' } });
+ });
+
+ it('should dispatch receiveDeleteUserListError', (done) => {
+ testAction(
+ deleteUserList,
+ userList,
+ state,
+ [],
+ [
+ { type: 'requestDeleteUserList', payload: userList },
+ {
+ type: 'receiveDeleteUserListError',
+ payload: { list: userList, error: 'some error' },
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveDeleteUserListError', () => {
+ it('should commit RECEIVE_DELETE_USER_LIST_ERROR with the given list', (done) => {
+ testAction(
+ receiveDeleteUserListError,
+ { list: userList, error: 'mock error' },
+ state,
+ [
+ {
+ type: 'RECEIVE_DELETE_USER_LIST_ERROR',
+ payload: { list: userList, error: 'mock error' },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('clearAlert', () => {
+ it('should commit RECEIVE_CLEAR_ALERT', (done) => {
+ const alertIndex = 3;
+
+ testAction(
+ clearAlert,
+ alertIndex,
+ state,
+ [{ type: 'RECEIVE_CLEAR_ALERT', payload: alertIndex }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/user_lists/store/index/mutations_spec.js b/spec/frontend/user_lists/store/index/mutations_spec.js
new file mode 100644
index 00000000000..370838ae5fb
--- /dev/null
+++ b/spec/frontend/user_lists/store/index/mutations_spec.js
@@ -0,0 +1,121 @@
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import * as types from '~/user_lists/store/index/mutation_types';
+import mutations from '~/user_lists/store/index/mutations';
+import createState from '~/user_lists/store/index/state';
+import { userList } from '../../../feature_flags/mock_data';
+
+describe('~/user_lists/store/index/mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState({ projectId: '1' });
+ });
+
+ describe('SET_USER_LISTS_OPTIONS', () => {
+ it('should set provided options', () => {
+ mutations[types.SET_USER_LISTS_OPTIONS](state, { page: '1', scope: 'all' });
+
+ expect(state.options).toEqual({ page: '1', scope: 'all' });
+ });
+ });
+
+ describe('REQUEST_USER_LISTS', () => {
+ it('sets isLoading to true', () => {
+ mutations[types.REQUEST_USER_LISTS](state);
+ expect(state.isLoading).toBe(true);
+ });
+ });
+
+ describe('RECEIVE_USER_LISTS_SUCCESS', () => {
+ const headers = {
+ 'x-next-page': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '2',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '5',
+ };
+
+ beforeEach(() => {
+ mutations[types.RECEIVE_USER_LISTS_SUCCESS](state, { data: [userList], headers });
+ });
+
+ it('sets isLoading to false', () => {
+ expect(state.isLoading).toBe(false);
+ });
+
+ it('sets userLists to the received userLists', () => {
+ expect(state.userLists).toEqual([userList]);
+ });
+
+ it('sets pagination info for user lits', () => {
+ expect(state.pageInfo).toEqual(parseIntPagination(normalizeHeaders(headers)));
+ });
+
+ it('sets the count for user lists', () => {
+ expect(state.count).toBe(parseInt(headers['X-TOTAL'], 10));
+ });
+ });
+
+ describe('RECEIVE_USER_LISTS_ERROR', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_USER_LISTS_ERROR](state);
+ });
+
+ it('should set isLoading to false', () => {
+ expect(state.isLoading).toEqual(false);
+ });
+
+ it('should set hasError to true', () => {
+ expect(state.hasError).toEqual(true);
+ });
+ });
+
+ describe('REQUEST_DELETE_USER_LIST', () => {
+ beforeEach(() => {
+ state.userLists = [userList];
+ mutations[types.REQUEST_DELETE_USER_LIST](state, userList);
+ });
+
+ it('should remove the deleted list', () => {
+ expect(state.userLists).not.toContain(userList);
+ });
+ });
+
+ describe('RECEIVE_DELETE_USER_LIST_ERROR', () => {
+ beforeEach(() => {
+ state.userLists = [];
+ mutations[types.RECEIVE_DELETE_USER_LIST_ERROR](state, {
+ list: userList,
+ error: 'some error',
+ });
+ });
+
+ it('should set isLoading to false and hasError to false', () => {
+ expect(state.isLoading).toBe(false);
+ expect(state.hasError).toBe(false);
+ });
+
+ it('should add the user list back to the list of user lists', () => {
+ expect(state.userLists).toContain(userList);
+ });
+ });
+
+ describe('RECEIVE_CLEAR_ALERT', () => {
+ it('clears the alert', () => {
+ state.alerts = ['a server error'];
+
+ mutations[types.RECEIVE_CLEAR_ALERT](state, 0);
+
+ expect(state.alerts).toEqual([]);
+ });
+
+ it('clears the alert at the specified index', () => {
+ state.alerts = ['a server error', 'another error', 'final error'];
+
+ mutations[types.RECEIVE_CLEAR_ALERT](state, 1);
+
+ expect(state.alerts).toEqual(['a server error', 'final error']);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
index d6a1c2d3b07..af6624a6c43 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
@@ -1,6 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
@@ -125,7 +125,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error', () => {
- expect(createFlash).toHaveBeenCalledWith(FETCH_ERROR);
+ expect(createFlash).toHaveBeenCalledWith({ message: FETCH_ERROR });
});
});
@@ -264,7 +264,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error message', () => {
- expect(createFlash).toHaveBeenCalledWith(APPROVE_ERROR);
+ expect(createFlash).toHaveBeenCalledWith({ message: APPROVE_ERROR });
});
});
});
@@ -315,7 +315,7 @@ describe('MRWidget approvals', () => {
});
it('flashes error message', () => {
- expect(createFlash).toHaveBeenCalledWith(UNAPPROVE_ERROR);
+ expect(createFlash).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
index 07e869a070f..5d923d0383f 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_alert_message_spec.js
@@ -1,76 +1,45 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlLink, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
-describe('MrWidgetAlertMessage', () => {
- let wrapper;
-
- beforeEach(() => {
- const localVue = createLocalVue();
+let wrapper;
- wrapper = shallowMount(localVue.extend(MrWidgetAlertMessage), {
- propsData: {},
- localVue,
- });
+function createComponent(propsData = {}) {
+ wrapper = shallowMount(MrWidgetAlertMessage, {
+ propsData,
});
+}
+describe('MrWidgetAlertMessage', () => {
afterEach(() => {
wrapper.destroy();
});
- describe('when type is not provided', () => {
- it('should render a red message', (done) => {
- wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain('danger_message');
- expect(wrapper.classes()).not.toContain('warning_message');
- done();
- });
- });
- });
-
- describe('when type === "danger"', () => {
- it('should render a red message', (done) => {
- wrapper.setProps({ type: 'danger' });
- wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain('danger_message');
- expect(wrapper.classes()).not.toContain('warning_message');
- done();
- });
- });
- });
+ it('should render a GlAert', () => {
+ createComponent({ type: 'danger' });
- describe('when type === "warning"', () => {
- it('should render a red message', (done) => {
- wrapper.setProps({ type: 'warning' });
- wrapper.vm.$nextTick(() => {
- expect(wrapper.classes()).toContain('warning_message');
- expect(wrapper.classes()).not.toContain('danger_message');
- done();
- });
- });
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
+ expect(wrapper.findComponent(GlAlert).props('variant')).toBe('danger');
});
describe('when helpPath is not provided', () => {
- it('should not render a help icon/link', (done) => {
- wrapper.vm.$nextTick(() => {
- const link = wrapper.find(GlLink);
+ it('should not render a help link', () => {
+ createComponent({ type: 'info' });
+
+ const link = wrapper.findComponent(GlLink);
- expect(link.exists()).toBe(false);
- done();
- });
+ expect(link.exists()).toBe(false);
});
});
describe('when helpPath is provided', () => {
- it('should render a help icon/link', (done) => {
- wrapper.setProps({ helpPath: '/path/to/help/docs' });
- wrapper.vm.$nextTick(() => {
- const link = wrapper.find(GlLink);
+ it('should render a help link', () => {
+ createComponent({ type: 'info', helpPath: 'https://gitlab.com' });
+
+ const link = wrapper.findComponent(GlLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes().href).toBe('/path/to/help/docs');
- done();
- });
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe('https://gitlab.com');
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 924dc37aab9..ecaca16a2cd 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -94,7 +94,7 @@ describe('MRWidgetPipeline', () => {
it('should render pipeline finished timestamp', () => {
expect(findPipelineFinishedAt().attributes()).toMatchObject({
- title: 'Apr 7, 2017 2:00pm GMT+0000',
+ title: 'Apr 7, 2017 2:00pm UTC',
datetime: mockData.pipeline.details.finished_at,
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js
index 55d7e2391b2..6ae218ce6f8 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -18,8 +18,8 @@ describe('MRWidgetClosed', () => {
avatarUrl:
'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
- mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
- closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ mergedAt: 'Jan 24, 2018 1:02pm UTC',
+ closedAt: 'Jan 24, 2018 1:02pm UTC',
readableMergedAt: '',
readableClosedAt: 'less than a minute ago',
},
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 6af8ac9e18e..6bb87893c31 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -37,10 +37,10 @@ describe('MRWidgetMerged', () => {
avatarUrl:
'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
- mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ mergedAt: 'Jan 24, 2018 1:02pm UTC',
readableMergedAt: '',
closedBy: {},
- closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ closedAt: 'Jan 24, 2018 1:02pm UTC',
readableClosedAt: '',
},
updatedAt: 'mergedUpdatedAt',
@@ -236,6 +236,6 @@ describe('MRWidgetMerged', () => {
});
it('should use mergedEvent mergedAt as tooltip title', () => {
- expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm GMT+0000');
+ expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm UTC');
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index bd77a1d657e..9b10b078e89 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -22,7 +22,7 @@ describe('MRWidgetPipelineBlocked', () => {
createWrapper();
expect(wrapper.text()).toBe(
- 'Pipeline blocked. The pipeline for this merge request requires a manual action to proceed',
+ "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.",
);
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 85a42946325..2d00cd8e8d4 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -59,12 +59,17 @@ const createTestService = () => ({
});
let wrapper;
-const createComponent = (customConfig = {}) => {
+const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(ReadyToMerge, {
propsData: {
mr: createTestMr(customConfig),
service: createTestService(),
},
+ provide: {
+ glFeatures: {
+ mergeRequestWidgetGraphql,
+ },
+ },
});
};
@@ -123,26 +128,26 @@ describe('ReadyToMerge', () => {
});
describe('mergeButtonVariant', () => {
- it('defaults to success class', () => {
+ it('defaults to confirm class', () => {
createComponent({
mr: { availableAutoMergeStrategies: [] },
});
- expect(wrapper.vm.mergeButtonVariant).toEqual('success');
+ expect(wrapper.vm.mergeButtonVariant).toEqual('confirm');
});
- it('returns success class for success status', () => {
+ it('returns confirm class for success status', () => {
createComponent({
mr: { availableAutoMergeStrategies: [], pipeline: true },
});
- expect(wrapper.vm.mergeButtonVariant).toEqual('success');
+ expect(wrapper.vm.mergeButtonVariant).toEqual('confirm');
});
- it('returns info class for pending status', () => {
+ it('returns confirm class for pending status', () => {
createComponent();
- expect(wrapper.vm.mergeButtonVariant).toEqual('info');
+ expect(wrapper.vm.mergeButtonVariant).toEqual('confirm');
});
it('returns danger class for failed status', () => {
@@ -673,6 +678,34 @@ describe('ReadyToMerge', () => {
expect(findCommitEditElements().length).toBe(2);
});
+ it('should have two edit components when squash is enabled and there is more than 1 commit and mergeRequestWidgetGraphql is enabled', async () => {
+ createComponent(
+ {
+ mr: {
+ commitsCount: 2,
+ squashIsSelected: true,
+ enableSquashBeforeMerge: true,
+ },
+ },
+ true,
+ );
+
+ wrapper.setData({
+ loading: false,
+ state: {
+ ...createTestMr({}),
+ userPermissions: {},
+ squash: true,
+ mergeable: true,
+ commitCount: 2,
+ commitsWithoutMergeCommits: {},
+ },
+ });
+ await wrapper.vm.$nextTick();
+
+ expect(findCommitEditElements().length).toBe(2);
+ });
+
it('should have one edit components when squash is enabled and there is 1 commit only', () => {
createComponent({
mr: {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
index e0077a008a2..0609086997b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -63,10 +63,10 @@ describe('Wip', () => {
setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- expect(createFlash).toHaveBeenCalledWith(
- 'The merge request can now be merged.',
- 'notice',
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'The merge request can now be merged.',
+ type: 'notice',
+ });
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
index 22e58ac6abf..49783560bf2 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import {
CREATED,
@@ -203,9 +203,9 @@ describe('DeploymentAction component', () => {
it('should call createFlash with error message', () => {
expect(createFlash).toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith(
- actionButtonMocks[configConst].errorMessage,
- );
+ expect(createFlash).toHaveBeenCalledWith({
+ message: actionButtonMocks[configConst].errorMessage,
+ });
});
});
});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 446cd2a1e2f..9da370747fc 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -26,7 +26,7 @@ describe('MrWidgetOptions', () => {
let wrapper;
let mock;
- const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch';
+ const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
beforeEach(() => {
gl.mrWidgetData = { ...mockData };
@@ -532,7 +532,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
const tooltip = wrapper.find('[data-testid="question-o-icon"]');
- expect(wrapper.text()).toContain('Deletes source branch');
+ expect(wrapper.text()).toContain('The source branch will be deleted');
expect(tooltip.attributes('title')).toBe(
'A user with write access to the source branch selected this option',
);
@@ -548,7 +548,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
expect(wrapper.text()).toContain('The source branch has been deleted');
- expect(wrapper.text()).not.toContain('Deletes source branch');
+ expect(wrapper.text()).not.toContain('The source branch will be deleted');
done();
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index c532f688cbd..3fc13243bce 100644
--- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -1,5 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql';
import Tracking from '~/tracking';
@@ -10,9 +10,10 @@ const mockAlert = mockAlerts[0];
describe('AlertManagementStatus', () => {
let wrapper;
- const findStatusDropdown = () => wrapper.find(GlDropdown);
- const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
+ const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
+ const findFirstStatusOption = () => findStatusDropdown().findComponent(GlDropdownItem);
const findAllStatusOptions = () => findStatusDropdown().findAll(GlDropdownItem);
+ const findStatusDropdownHeader = () => wrapper.findByTestId('dropdown-header');
const selectFirstStatusOption = () => {
findFirstStatusOption().vm.$emit('click');
@@ -21,7 +22,7 @@ describe('AlertManagementStatus', () => {
};
function mountComponent({ props = {}, provide = {}, loading = false, stubs = {} } = {}) {
- wrapper = shallowMount(AlertManagementStatus, {
+ wrapper = shallowMountExtended(AlertManagementStatus, {
propsData: {
alert: { ...mockAlert },
projectPath: 'gitlab-org/gitlab',
@@ -43,17 +44,29 @@ describe('AlertManagementStatus', () => {
});
}
- beforeEach(() => {
- mountComponent();
- });
-
afterEach(() => {
if (wrapper) {
wrapper.destroy();
- wrapper = null;
}
});
+ describe('sidebar', () => {
+ it('displays the dropdown status header', () => {
+ mountComponent({ props: { isSidebar: true } });
+ expect(findStatusDropdownHeader().exists()).toBe(true);
+ });
+
+ it('hides the dropdown by default', () => {
+ mountComponent({ props: { isSidebar: true } });
+ expect(wrapper.classes()).toContain('gl-display-none');
+ });
+
+ it('shows the dropdown', () => {
+ mountComponent({ props: { isSidebar: true, isDropdownShowing: true } });
+ expect(wrapper.classes()).toContain('show');
+ });
+ });
+
describe('updating the alert status', () => {
const iid = '1527542';
const mockUpdatedMutationResult = {
@@ -99,6 +112,13 @@ describe('AlertManagementStatus', () => {
]);
});
+ it('emits an update event at the start and ending of the updating', async () => {
+ await selectFirstStatusOption();
+ expect(wrapper.emitted('handle-updating').length > 1).toBe(true);
+ expect(wrapper.emitted('handle-updating')[0]).toEqual([true]);
+ expect(wrapper.emitted('handle-updating')[1]).toEqual([false]);
+ });
+
it('emits an error when triggered a second time', async () => {
await selectFirstStatusOption();
await wrapper.vm.$nextTick();
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 db9b0930c06..9ae45071f45 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
@@ -21,6 +21,7 @@ describe('Alert Details Sidebar Assignees', () => {
id: 1,
name: 'User 1',
username: 'root',
+ webUrl: 'https://gitlab:3443/root',
},
{
avatar_url:
@@ -28,6 +29,7 @@ describe('Alert Details Sidebar Assignees', () => {
id: 2,
name: 'User 2',
username: 'not-root',
+ webUrl: 'https://gitlab:3443/non-root',
},
];
@@ -128,7 +130,7 @@ describe('Alert Details Sidebar Assignees', () => {
variables: {
iid: '1527542',
assigneeUsernames: ['root'],
- projectPath: 'projectPath',
+ fullPath: 'projectPath',
},
});
});
@@ -137,7 +139,7 @@ describe('Alert Details Sidebar Assignees', () => {
wrapper.setData({ isDropdownSearching: false });
const errorMutationResult = {
data: {
- alertSetAssignees: {
+ issuableSetAssignees: {
errors: ['There was a problem for sure.'],
alert: {},
},
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
index d5be5b623b8..b00a20dab1a 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
@@ -1,6 +1,5 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
+import { GlDropdown, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
import { PAGE_CONFIG } from '~/vue_shared/alert_details/constants';
@@ -11,9 +10,7 @@ const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar Status', () => {
let wrapper;
const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
- const findStatusDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findStatusLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findStatusDropdownHeader = () => wrapper.findByTestId('dropdown-header');
const findAlertStatus = () => wrapper.findComponent(AlertStatus);
const findStatus = () => wrapper.findByTestId('status');
const findSidebarIcon = () => wrapper.findByTestId('status-icon');
@@ -25,7 +22,7 @@ describe('Alert Details Sidebar Status', () => {
stubs = {},
provide = {},
} = {}) {
- wrapper = mountExtended(AlertSidebarStatus, {
+ wrapper = shallowMountExtended(AlertSidebarStatus, {
propsData: {
alert: { ...mockAlert },
...data,
@@ -63,11 +60,7 @@ describe('Alert Details Sidebar Status', () => {
});
it('displays status dropdown', () => {
- expect(findStatusDropdown().exists()).toBe(true);
- });
-
- it('displays the dropdown status header', () => {
- expect(findStatusDropdownHeader().exists()).toBe(true);
+ expect(findAlertStatus().exists()).toBe(true);
});
it('does not display the collapsed sidebar icon', () => {
@@ -75,42 +68,24 @@ describe('Alert Details Sidebar Status', () => {
});
describe('updating the alert status', () => {
- const mockUpdatedMutationResult = {
- data: {
- updateAlertStatus: {
- errors: [],
- alert: {
- status: 'acknowledged',
- },
- },
- },
- };
-
- beforeEach(() => {
+ it('ensures dropdown is hidden when loading', async () => {
mountComponent({
data: { alert: mockAlert },
sidebarCollapsed: false,
loading: false,
});
- });
-
- it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
- findStatusDropdownItem().vm.$emit('click');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateAlertStatusMutation,
- variables: {
- iid: '1527542',
- status: 'TRIGGERED',
- projectPath: 'projectPath',
- },
- });
+ findAlertStatus().vm.$emit('handle-updating', true);
+ await wrapper.vm.$nextTick();
+ expect(findStatusLoadingIcon().exists()).toBe(true);
});
it('stops updating when the request fails', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
- findStatusDropdownItem().vm.$emit('click');
+ mountComponent({
+ data: { alert: mockAlert },
+ sidebarCollapsed: false,
+ loading: false,
+ });
+ findAlertStatus().vm.$emit('handle-updating', false);
expect(findStatusLoadingIcon().exists()).toBe(false);
expect(findStatus().text()).toBe('Triggered');
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
index 3be609f0dad..3f91591f5cd 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap
@@ -5,7 +5,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
class="awards js-awards-block"
>
<button
- class="btn gl-mr-3 btn-default btn-md gl-button"
+ class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada, Leonardo, and Marie"
type="button"
@@ -35,7 +35,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
</button>
<button
- class="btn gl-mr-3 btn-default btn-md gl-button selected"
+ class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, and Marie"
type="button"
@@ -65,7 +65,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
</button>
<button
- class="btn gl-mr-3 btn-default btn-md gl-button"
+ class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
title="Ada and Jane"
type="button"
@@ -95,7 +95,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
</button>
<button
- class="btn gl-mr-3 btn-default btn-md gl-button selected"
+ class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You, Ada, Jane, and Leonardo"
type="button"
@@ -125,7 +125,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
</button>
<button
- class="btn gl-mr-3 btn-default btn-md gl-button selected"
+ class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
type="button"
@@ -155,7 +155,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
</button>
<button
- class="btn gl-mr-3 btn-default btn-md gl-button"
+ class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button"
data-testid="award-button"
title="Marie"
type="button"
@@ -185,7 +185,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</span>
</button>
<button
- class="btn gl-mr-3 btn-default btn-md gl-button selected"
+ class="btn gl-mr-3 gl-my-2 btn-default btn-md gl-button selected"
data-testid="award-button"
title="You"
type="button"
@@ -216,7 +216,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
</button>
<div
- class="award-menu-holder"
+ class="award-menu-holder gl-my-2"
>
<button
aria-label="Add reaction"
@@ -238,6 +238,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
aria-hidden="true"
class="gl-icon s16"
data-testid="slight-smile-icon"
+ role="img"
>
<use
href="#slight-smile"
@@ -252,6 +253,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
aria-hidden="true"
class="gl-icon s16"
data-testid="smiley-icon"
+ role="img"
>
<use
href="#smiley"
@@ -266,6 +268,7 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
aria-hidden="true"
class="gl-icon s16"
data-testid="smile-icon"
+ role="img"
>
<use
href="#smile"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index adb6c935f96..45d34bcdd3f 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -14,6 +14,7 @@ exports[`Expand button on click when short text is provided renders button after
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
+ role="img"
>
<use
href="#ellipsis_h"
@@ -43,6 +44,7 @@ exports[`Expand button on click when short text is provided renders button after
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
+ role="img"
>
<use
href="#ellipsis_h"
@@ -67,6 +69,7 @@ exports[`Expand button when short text is provided renders button before text 1`
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
+ role="img"
>
<use
href="#ellipsis_h"
@@ -96,6 +99,7 @@ exports[`Expand button when short text is provided renders button before text 1`
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="ellipsis_h-icon"
+ role="img"
>
<use
href="#ellipsis_h"
diff --git a/spec/frontend/vue_shared/components/alert_details_table_spec.js b/spec/frontend/vue_shared/components/alert_details_table_spec.js
index 03b04a92bdf..b9a8a5bee97 100644
--- a/spec/frontend/vue_shared/components/alert_details_table_spec.js
+++ b/spec/frontend/vue_shared/components/alert_details_table_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
@@ -7,6 +7,9 @@ const mockAlert = {
title: 'SyntaxError: Invalid or unexpected token',
severity: 'CRITICAL',
eventCount: 7,
+ service: 'https://gitlab.com',
+ // eslint-disable-next-line no-script-url
+ description: 'javascript:alert("XSS")',
createdAt: '2020-04-17T23:18:14.996Z',
startedAt: '2020-04-17T23:18:14.996Z',
endedAt: '2020-04-17T23:18:14.996Z',
@@ -43,7 +46,7 @@ describe('AlertDetails', () => {
wrapper = null;
});
- const findTableComponent = () => wrapper.find(GlTable);
+ const findTableComponent = () => wrapper.findComponent(GlTable);
const findTableKeys = () => findTableComponent().findAll('tbody td:first-child');
const findTableFieldValueByKey = (fieldKey) =>
findTableComponent()
@@ -52,6 +55,7 @@ describe('AlertDetails', () => {
.at(0)
.find('td:nth-child(2)');
const findTableField = (fields, fieldName) => fields.filter((row) => row.text() === fieldName);
+ const findTableLinks = () => wrapper.findAllComponents(GlLink);
describe('Alert details', () => {
describe('empty state', () => {
@@ -88,7 +92,16 @@ describe('AlertDetails', () => {
it('should show allowed alert fields', () => {
const fields = findTableKeys();
- ['Iid', 'Title', 'Severity', 'Status', 'Hosts', 'Environment'].forEach((field) => {
+ [
+ 'Iid',
+ 'Title',
+ 'Severity',
+ 'Status',
+ 'Hosts',
+ 'Environment',
+ 'Service',
+ 'Description',
+ ].forEach((field) => {
expect(findTableField(fields, field).exists()).toBe(true);
});
});
@@ -99,6 +112,12 @@ describe('AlertDetails', () => {
expect(findTableField(fields, field).exists()).toBe(false);
});
});
+
+ it('should render a clickable URL if safe', () => {
+ expect(findTableLinks().wrappers).toHaveLength(1);
+ expect(findTableLinks().at(0).props('isUnsafeLink')).toBe(false);
+ expect(findTableLinks().at(0).attributes('href')).toBe(mockAlert.service);
+ });
});
describe('environment', () => {
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index 550ac4a9d38..55f9eedc169 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -41,7 +41,14 @@ const TEST_AWARDS = [
];
const TEST_ADD_BUTTON_CLASS = 'js-test-add-button-class';
-const REACTION_CONTROL_CLASSES = ['btn', 'gl-mr-3', 'btn-default', 'btn-md', 'gl-button'];
+const REACTION_CONTROL_CLASSES = [
+ 'btn',
+ 'gl-mr-3',
+ 'gl-my-2',
+ 'btn-default',
+ 'btn-md',
+ 'gl-button',
+];
describe('vue_shared/components/awards_list', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
index b2ed79cd75a..93cddff8421 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -1,6 +1,9 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
+
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+
import {
stripQuotes,
uniqueTokens,
@@ -210,6 +213,19 @@ describe('filterToQueryObject', () => {
const res = filterToQueryObject({ [token]: value });
expect(res).toEqual(result);
});
+
+ it.each([
+ [FILTERED_SEARCH_TERM, [{ value: '' }], { search: '' }],
+ [FILTERED_SEARCH_TERM, [{ value: 'bar' }], { search: 'bar' }],
+ [FILTERED_SEARCH_TERM, [{ value: 'bar' }, { value: '' }], { search: 'bar' }],
+ [FILTERED_SEARCH_TERM, [{ value: 'bar' }, { value: 'baz' }], { search: 'bar baz' }],
+ ])(
+ 'when filteredSearchTermKey=search gathers filter values %s=%j into query object=%j',
+ (token, value, result) => {
+ const res = filterToQueryObject({ [token]: value }, { filteredSearchTermKey: 'search' });
+ expect(res).toEqual(result);
+ },
+ );
});
describe('urlQueryToFilter', () => {
@@ -255,10 +271,61 @@ describe('urlQueryToFilter', () => {
},
],
['not[foo][]=bar', { foo: [{ value: 'bar', operator: '!=' }] }],
- ])('gathers filter values %s into query object=%j', (query, result) => {
- const res = urlQueryToFilter(query);
- expect(res).toEqual(result);
- });
+ ['nop=1&not[nop]=2', {}, { filterNamesAllowList: ['foo'] }],
+ [
+ 'foo[]=bar&not[foo][]=baz&nop=xxx&not[nop]=yyy',
+ {
+ foo: [
+ { value: 'bar', operator: '=' },
+ { value: 'baz', operator: '!=' },
+ ],
+ },
+ { filterNamesAllowList: ['foo'] },
+ ],
+ [
+ 'search=term&foo=bar',
+ {
+ [FILTERED_SEARCH_TERM]: [{ value: 'term' }],
+ foo: { value: 'bar', operator: '=' },
+ },
+ { filteredSearchTermKey: 'search' },
+ ],
+ [
+ 'search=my terms',
+ {
+ [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ },
+ { filteredSearchTermKey: 'search' },
+ ],
+ [
+ 'search[]=my&search[]=terms',
+ {
+ [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ },
+ { filteredSearchTermKey: 'search' },
+ ],
+ [
+ 'search=my+terms',
+ {
+ [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ },
+ { filteredSearchTermKey: 'search', legacySpacesDecode: false },
+ ],
+ [
+ 'search=my terms&foo=bar&nop=xxx',
+ {
+ [FILTERED_SEARCH_TERM]: [{ value: 'my' }, { value: 'terms' }],
+ foo: { value: 'bar', operator: '=' },
+ },
+ { filteredSearchTermKey: 'search', filterNamesAllowList: ['foo'] },
+ ],
+ ])(
+ 'gathers filter values %s into query object=%j when options %j',
+ (query, result, options = undefined) => {
+ const res = urlQueryToFilter(query, options);
+ expect(res).toEqual(result);
+ },
+ );
});
describe('getRecentlyUsedTokenValues', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index 23e4deab9c1..134c6c8b929 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -65,8 +65,8 @@ export const mockMilestones = [
];
export const mockEpics = [
- { iid: 1, id: 1, title: 'Foo' },
- { iid: 2, id: 2, title: 'Bar' },
+ { iid: 1, id: 1, title: 'Foo', group_full_path: 'gitlab-org' },
+ { iid: 2, id: 2, title: 'Bar', group_full_path: 'gitlab-org/design' },
];
export const mockEmoji1 = {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 05bad572472..4140ec09b4e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 3b50927dcc6..f50eafdbc52 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -1,13 +1,13 @@
import {
- GlFilteredSearchToken,
GlFilteredSearchTokenSegment,
GlFilteredSearchSuggestion,
GlDropdownDivider,
+ GlAvatar,
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -15,6 +15,7 @@ import {
DEFAULT_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockAuthorToken, mockAuthors } from '../mock_data';
@@ -29,12 +30,22 @@ const defaultStubs = {
},
};
+const mockPreloadedAuthors = [
+ {
+ id: 13,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'avatar/url',
+ },
+];
+
function createComponent(options = {}) {
const {
config = mockAuthorToken,
value = { data: '' },
active = false,
stubs = defaultStubs,
+ data = {},
} = options;
return mount(AuthorToken, {
propsData: {
@@ -47,132 +58,172 @@ function createComponent(options = {}) {
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: 'custom-class',
},
+ data() {
+ return { ...data };
+ },
stubs,
});
}
describe('AuthorToken', () => {
+ const originalGon = window.gon;
+ const currentUserLength = 1;
let mock;
let wrapper;
+ const getBaseToken = () => wrapper.findComponent(BaseToken);
+
beforeEach(() => {
mock = new MockAdapter(axios);
- wrapper = createComponent();
});
afterEach(() => {
+ window.gon = originalGon;
mock.restore();
wrapper.destroy();
});
- describe('computed', () => {
- describe('currentValue', () => {
- it('returns lowercase string for `value.data`', () => {
- wrapper = createComponent({ value: { data: 'FOO' } });
-
- expect(wrapper.vm.currentValue).toBe('foo');
+ describe('methods', () => {
+ describe('fetchAuthorBySearchTerm', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
});
- });
- describe('activeAuthor', () => {
- it('returns object for currently present `value.data`', async () => {
- wrapper = createComponent({ value: { data: mockAuthors[0].username } });
-
- wrapper.setData({
- authors: mockAuthors,
- });
+ it('calls `config.fetchAuthors` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchAuthors');
- await wrapper.vm.$nextTick();
+ getBaseToken().vm.$emit('fetch-token-values', mockAuthors[0].username);
- expect(wrapper.vm.activeAuthor).toEqual(mockAuthors[0]);
+ expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith(
+ mockAuthorToken.fetchPath,
+ mockAuthors[0].username,
+ );
});
- });
- });
-
- describe('fetchAuthorBySearchTerm', () => {
- it('calls `config.fetchAuthors` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors');
-
- wrapper.vm.fetchAuthorBySearchTerm(mockAuthors[0].username);
- expect(wrapper.vm.config.fetchAuthors).toHaveBeenCalledWith(
- mockAuthorToken.fetchPath,
- mockAuthors[0].username,
- );
- });
-
- it('sets response to `authors` when request is succesful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors);
+ it('sets response to `authors` when request is succesful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockResolvedValue(mockAuthors);
- wrapper.vm.fetchAuthorBySearchTerm('root');
+ getBaseToken().vm.$emit('fetch-token-values', 'root');
- return waitForPromises().then(() => {
- expect(wrapper.vm.authors).toEqual(mockAuthors);
+ return waitForPromises().then(() => {
+ expect(getBaseToken().props('tokenValues')).toEqual(mockAuthors);
+ });
});
- });
- it('calls `createFlash` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
+ it('calls `createFlash` with flash error message when request fails', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
- wrapper.vm.fetchAuthorBySearchTerm('root');
+ getBaseToken().vm.$emit('fetch-token-values', 'root');
- return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith('There was a problem fetching users.');
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching users.',
+ });
+ });
});
- });
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
+ it('sets `loading` to false when request completes', async () => {
+ jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
- wrapper.vm.fetchAuthorBySearchTerm('root');
+ getBaseToken().vm.$emit('fetch-token-values', 'root');
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ await waitForPromises();
+
+ expect(getBaseToken().props('tokensListLoading')).toBe(false);
});
});
});
describe('template', () => {
- beforeEach(() => {
- wrapper.setData({
- authors: mockAuthors,
+ const activateTokenValuesList = async () => {
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const suggestionsSegment = tokenSegments.at(2);
+ suggestionsSegment.vm.$emit('activate');
+ await wrapper.vm.$nextTick();
+ };
+
+ it('renders base-token component', () => {
+ wrapper = createComponent({
+ value: { data: mockAuthors[0].username },
+ data: { authors: mockAuthors },
});
- return wrapper.vm.$nextTick();
- });
+ const baseTokenEl = getBaseToken();
- it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ expect(baseTokenEl.exists()).toBe(true);
+ expect(baseTokenEl.props()).toMatchObject({
+ tokenValues: mockAuthors,
+ fnActiveTokenValue: wrapper.vm.getActiveAuthor,
+ });
});
it('renders token item when value is selected', () => {
- wrapper.setProps({
+ wrapper = createComponent({
value: { data: mockAuthors[0].username },
+ data: { authors: mockAuthors },
+ stubs: { Portal: true },
});
return wrapper.vm.$nextTick(() => {
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // Author, =, "Administrator"
- expect(tokenSegments.at(2).text()).toBe(mockAuthors[0].name); // "Administrator"
+
+ const tokenValue = tokenSegments.at(2);
+
+ expect(tokenValue.findComponent(GlAvatar).props('src')).toBe(mockAuthors[0].avatar_url);
+ expect(tokenValue.text()).toBe(mockAuthors[0].name); // "Administrator"
});
});
+ it('renders token value with correct avatarUrl from author object', async () => {
+ const getAvatarEl = () =>
+ wrapper.findAll(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
+
+ wrapper = createComponent({
+ value: { data: mockAuthors[0].username },
+ data: {
+ authors: [
+ {
+ ...mockAuthors[0],
+ },
+ ],
+ },
+ stubs: { Portal: true },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+
+ wrapper.setData({
+ authors: [
+ {
+ ...mockAuthors[0],
+ avatarUrl: mockAuthors[0].avatar_url,
+ avatar_url: undefined,
+ },
+ ],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(getAvatarEl().props('src')).toBe(mockAuthors[0].avatar_url);
+ });
+
it('renders provided defaultAuthors as suggestions', async () => {
const defaultAuthors = DEFAULT_NONE_ANY;
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken, defaultAuthors },
+ config: { ...mockAuthorToken, defaultAuthors, preloadedAuthors: mockPreloadedAuthors },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
- const suggestionsSegment = tokenSegments.at(2);
- suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+
+ await activateTokenValuesList();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(defaultAuthors.length);
+ expect(suggestions).toHaveLength(defaultAuthors.length + currentUserLength);
defaultAuthors.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
@@ -189,25 +240,42 @@ describe('AuthorToken', () => {
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
- expect(wrapper.find(GlFilteredSearchSuggestion).exists()).toBe(false);
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => {
wrapper = createComponent({
active: true,
- config: { ...mockAuthorToken },
+ config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
- const suggestionsSegment = tokenSegments.at(2);
- suggestionsSegment.vm.$emit('activate');
- await wrapper.vm.$nextTick();
+
+ await activateTokenValuesList();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
- expect(suggestions).toHaveLength(1);
+ expect(suggestions).toHaveLength(1 + currentUserLength);
expect(suggestions.at(0).text()).toBe(DEFAULT_LABEL_ANY.text);
});
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ active: true,
+ config: {
+ ...mockAuthorToken,
+ preloadedAuthors: mockPreloadedAuthors,
+ defaultAuthors: [],
+ },
+ stubs: { Portal: true },
+ });
+ });
+
+ it('shows current user', () => {
+ const firstSuggestion = wrapper.findComponent(GlFilteredSearchSuggestion).text();
+ expect(firstSuggestion).toContain('Administrator');
+ expect(firstSuggestion).toContain('@root');
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 0db47f1f189..602864f4fa5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -175,6 +175,23 @@ describe('BaseToken', () => {
expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue);
});
+
+ it('does not add token from preloadedTokenValues', async () => {
+ const mockTokenValue = {
+ id: 1,
+ title: 'Foo',
+ };
+
+ wrapper.setProps({
+ preloadedTokenValues: [mockTokenValue],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.vm.handleTokenValueSelected(mockTokenValue);
+
+ expect(setTokenValueToRecentlyUsed).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index fb48aea8e4f..778a214f97e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -7,7 +7,7 @@ import {
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -121,7 +121,9 @@ describe('EmojiToken', () => {
wrapper.vm.fetchEmojiBySearchTerm('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith('There was a problem fetching emojis.');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching emojis.',
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
index addc058f658..68ed46fc3a2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/epic_token_spec.js
@@ -67,18 +67,6 @@ describe('EpicToken', () => {
await wrapper.vm.$nextTick();
});
-
- describe('activeEpic', () => {
- it('returns object for currently present `value.data`', async () => {
- wrapper.setProps({
- value: { data: `${mockEpics[0].iid}` },
- });
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.vm.activeEpic).toEqual(mockEpics[0]);
- });
- });
});
describe('methods', () => {
@@ -86,9 +74,12 @@ describe('EpicToken', () => {
it('calls `config.fetchEpics` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics');
- wrapper.vm.fetchEpicsBySearchTerm('foo');
+ wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
- expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith('foo');
+ expect(wrapper.vm.config.fetchEpics).toHaveBeenCalledWith({
+ epicPath: '',
+ search: 'foo',
+ });
});
it('sets response to `epics` when request is successful', async () => {
@@ -96,7 +87,7 @@ describe('EpicToken', () => {
data: mockEpics,
});
- wrapper.vm.fetchEpicsBySearchTerm();
+ wrapper.vm.fetchEpicsBySearchTerm({});
await waitForPromises();
@@ -106,7 +97,7 @@ describe('EpicToken', () => {
it('calls `createFlash` with flash error message when request fails', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
- wrapper.vm.fetchEpicsBySearchTerm('foo');
+ wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
await waitForPromises();
@@ -118,7 +109,7 @@ describe('EpicToken', () => {
it('sets `loading` to false when request completes', async () => {
jest.spyOn(wrapper.vm.config, 'fetchEpics').mockRejectedValue({});
- wrapper.vm.fetchEpicsBySearchTerm('foo');
+ wrapper.vm.fetchEpicsBySearchTerm({ search: 'foo' });
await waitForPromises();
@@ -128,9 +119,11 @@ describe('EpicToken', () => {
});
describe('template', () => {
+ const getTokenValueEl = () => wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2);
+
beforeEach(async () => {
wrapper = createComponent({
- value: { data: `${mockEpics[0].iid}` },
+ value: { data: `${mockEpics[0].group_full_path}::&${mockEpics[0].iid}` },
data: { epics: mockEpics },
});
@@ -147,5 +140,19 @@ describe('EpicToken', () => {
expect(tokenSegments).toHaveLength(3);
expect(tokenSegments.at(2).text()).toBe(`${mockEpics[0].title}::&${mockEpics[0].iid}`);
});
+
+ it.each`
+ value | valueType | tokenValueString
+ ${`${mockEpics[0].group_full_path}::&${mockEpics[0].iid}`} | ${'string'} | ${`${mockEpics[0].title}::&${mockEpics[0].iid}`}
+ ${`${mockEpics[1].group_full_path}::&${mockEpics[1].iid}`} | ${'number'} | ${`${mockEpics[1].title}::&${mockEpics[1].iid}`}
+ `('renders token item when selection is a $valueType', async ({ value, tokenValueString }) => {
+ wrapper.setProps({
+ value: { data: value },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(getTokenValueEl().text()).toBe(tokenValueString);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 57514a0c499..dd1c61b92b8 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -1,5 +1,4 @@
import {
- GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
@@ -11,13 +10,14 @@ import {
mockRegularLabel,
mockLabels,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
DEFAULT_LABELS,
DEFAULT_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { mockLabelToken } from '../mock_data';
@@ -25,6 +25,7 @@ import { mockLabelToken } from '../mock_data';
jest.mock('~/flash');
const defaultStubs = {
Portal: true,
+ BaseToken,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
@@ -68,55 +69,17 @@ describe('LabelToken', () => {
wrapper.destroy();
});
- describe('computed', () => {
- beforeEach(async () => {
- // Label title with spaces is always enclosed in quotations by component.
- wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
-
- wrapper.setData({
- labels: mockLabels,
- });
-
- await wrapper.vm.$nextTick();
- });
-
- describe('currentValue', () => {
- it('returns lowercase string for `value.data`', () => {
- expect(wrapper.vm.currentValue).toBe('"foo label"');
- });
- });
-
- describe('activeLabel', () => {
- it('returns object for currently present `value.data`', () => {
- expect(wrapper.vm.activeLabel).toEqual(mockRegularLabel);
- });
- });
-
- describe('containerStyle', () => {
- it('returns object containing `backgroundColor` and `color` properties based on `activeLabel` value', () => {
- expect(wrapper.vm.containerStyle).toEqual({
- backgroundColor: mockRegularLabel.color,
- color: mockRegularLabel.textColor,
- });
- });
-
- it('returns empty object when `activeLabel` is not set', async () => {
- wrapper.setData({
- labels: [],
- });
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.vm.containerStyle).toEqual({});
- });
- });
- });
-
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
+ describe('getActiveLabel', () => {
+ it('returns label object from labels array based on provided `currentValue` param', () => {
+ expect(wrapper.vm.getActiveLabel(mockLabels, 'foo label')).toEqual(mockRegularLabel);
+ });
+ });
+
describe('getLabelName', () => {
it('returns value of `name` or `title` property present in provided label param', () => {
let mockLabel = {
@@ -158,7 +121,9 @@ describe('LabelToken', () => {
wrapper.vm.fetchLabelBySearchTerm('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith('There was a problem fetching labels.');
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching labels.',
+ });
});
});
@@ -187,8 +152,14 @@ describe('LabelToken', () => {
await wrapper.vm.$nextTick();
});
- it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ it('renders base-token component', () => {
+ const baseTokenEl = wrapper.find(BaseToken);
+
+ expect(baseTokenEl.exists()).toBe(true);
+ expect(baseTokenEl.props()).toMatchObject({
+ tokenValues: mockLabels,
+ fnActiveTokenValue: wrapper.vm.getActiveLabel,
+ });
});
it('renders token item when value is selected', () => {
diff --git a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap
index e5035614196..ff1dad2de68 100644
--- a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap
+++ b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap
@@ -4,6 +4,7 @@ exports[`Title edit field matches the snapshot 1`] = `
<gl-form-group-stub
label="Title"
label-for="title-field-edit"
+ labeldescription=""
>
<gl-form-input-stub />
</gl-form-group-stub>
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 5c29c267c99..2658fa4a706 100644
--- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -91,7 +91,7 @@ describe('IssueAssigneesComponent', () => {
});
it('computes alt text for assignee avatar', () => {
- expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Avatar for Terrell Graham');
+ expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Assigned to Terrell Graham');
});
it('renders component root element with class `issue-assignees`', () => {
@@ -106,7 +106,7 @@ describe('IssueAssigneesComponent', () => {
const expected = mockAssigneesList.slice(0, TEST_MAX_VISIBLE - 1).map((x) =>
expect.objectContaining({
linkHref: x.web_url,
- imgAlt: `Avatar for ${x.name}`,
+ imgAlt: `Assigned to ${x.name}`,
imgCssClasses: TEST_CSS_CLASSES,
imgSrc: x.avatar_url,
imgSize: TEST_ICON_SIZE,
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index 30b7f0c2d28..23cf6ef9785 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -59,6 +59,7 @@ exports[`Package code instruction single line to match the default snapshot 1`]
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="copy-to-clipboard-icon"
+ role="img"
>
<use
href="#copy-to-clipboard"
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
new file mode 100644
index 00000000000..b2906973dbd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
@@ -0,0 +1,110 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
+<gl-modal-stub
+ actionsecondary="[object Object]"
+ dismisslabel="Close"
+ modalclass=""
+ modalid="runner-aws-deployments-modal"
+ size="sm"
+ title="Deploy GitLab Runner in AWS"
+ titletag="h4"
+>
+ <p>
+ For each solution, you will choose a capacity. 1 enables warm HA through Auto Scaling group re-spawn. 2 enables hot HA because the service is available even when a node is lost. 3 or more enables hot HA and manual scaling of runner fleet.
+ </p>
+
+ <ul
+ class="gl-list-style-none gl-p-0 gl-mb-0"
+ >
+ <li>
+ <gl-link-stub
+ class="gl-display-flex gl-font-weight-bold"
+ href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-amazon-linux-2-docker-manual-scaling-with-schedule-ondemandonly.cf.yml&stackName=linux-docker-nonspot&param_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host"
+ target="_blank"
+ >
+ <img
+ alt="linux-docker-nonspot"
+ class="gl-mt-2 gl-mr-5 gl-mb-6"
+ height="46"
+ src="/assets/aws-cloud-formation.png"
+ title="linux-docker-nonspot"
+ width="46"
+ />
+
+ Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot. Default choice for Linux Docker executor.
+
+ </gl-link-stub>
+ </li>
+ <li>
+ <gl-link-stub
+ class="gl-display-flex gl-font-weight-bold"
+ href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-amazon-linux-2-docker-manual-scaling-with-schedule-spotonly.cf.yml&stackName=linux-docker-spotonly&param_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host"
+ target="_blank"
+ >
+ <img
+ alt="linux-docker-spotonly"
+ class="gl-mt-2 gl-mr-5 gl-mb-6"
+ height="46"
+ src="/assets/aws-cloud-formation.png"
+ title="linux-docker-spotonly"
+ width="46"
+ />
+
+ Amazon Linux 2 Docker HA with manual scaling and optional scheduling. 100% spot.
+
+ </gl-link-stub>
+ </li>
+ <li>
+ <gl-link-stub
+ class="gl-display-flex gl-font-weight-bold"
+ href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-windows2019-shell-manual-scaling-with-scheduling-ondemandonly.cf.yml&stackName=win2019-shell-non-spot&param_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host"
+ target="_blank"
+ >
+ <img
+ alt="win2019-shell-non-spot"
+ class="gl-mt-2 gl-mr-5 gl-mb-6"
+ height="46"
+ src="/assets/aws-cloud-formation.png"
+ title="win2019-shell-non-spot"
+ width="46"
+ />
+
+ Windows 2019 Shell with manual scaling and optional scheduling. Non-spot. Default choice for Windows Shell executor.
+
+ </gl-link-stub>
+ </li>
+ <li>
+ <gl-link-stub
+ class="gl-display-flex gl-font-weight-bold"
+ href="https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https%3A%2F%2Fgl-public-templates.s3.amazonaws.com%2Fcfn%2Fexperimental%2Feasybutton-windows2019-shell-manual-scaling-with-scheduling-spotonly.cf.yml&stackName=win2019-shell-spot&param_3GITLABRunnerInstanceURL=http%3A%2F%2Ftest.host"
+ target="_blank"
+ >
+ <img
+ alt="win2019-shell-spot"
+ class="gl-mt-2 gl-mr-5 gl-mb-6"
+ height="46"
+ src="/assets/aws-cloud-formation.png"
+ title="win2019-shell-spot"
+ width="46"
+ />
+
+ Windows 2019 Shell with manual scaling and optional scheduling. 100% spot.
+
+ </gl-link-stub>
+ </li>
+ </ul>
+
+ <p>
+ <gl-sprintf-stub
+ message="Don't see what you are looking for? See the full list of options, including a fully customizable option, %{linkStart}here%{linkEnd}."
+ />
+ </p>
+
+ <p
+ class="gl-font-sm gl-mb-0"
+ >
+ If you do not select an AWS VPC, the runner will deploy to the Default VPC in the AWS Region you select. Please consult with your AWS administrator to understand if there are any security risks to deploying into the Default VPC in any given region in your AWS account.
+ </p>
+</gl-modal-stub>
+`;
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
new file mode 100644
index 00000000000..69db3ec7132
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
@@ -0,0 +1,75 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ExperimentTracking from '~/experimentation/experiment_tracking';
+import { getBaseURL } from '~/lib/utils/url_utility';
+import {
+ EXPERIMENT_NAME,
+ CF_BASE_URL,
+ TEMPLATES_BASE_URL,
+ EASY_BUTTONS,
+} from '~/vue_shared/components/runner_aws_deployments/constants';
+import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
+
+jest.mock('~/experimentation/experiment_tracking');
+
+describe('RunnerAwsDeploymentsModal', () => {
+ let wrapper;
+
+ const findEasyButtons = () => wrapper.findAllComponents(GlLink);
+
+ const createComponent = () => {
+ wrapper = shallowMount(RunnerAwsDeploymentsModal, {
+ propsData: {
+ modalId: 'runner-aws-deployments-modal',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the modal', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('should contain all easy buttons', () => {
+ expect(findEasyButtons()).toHaveLength(EASY_BUTTONS.length);
+ });
+
+ describe('first easy button', () => {
+ const findFirstButton = () => findEasyButtons().at(0);
+
+ it('should contain the correct description', () => {
+ expect(findFirstButton().text()).toBe(EASY_BUTTONS[0].description);
+ });
+
+ it('should contain the correct link', () => {
+ const link = findFirstButton().attributes('href');
+
+ expect(link.startsWith(CF_BASE_URL)).toBe(true);
+ expect(
+ link.includes(
+ `templateURL=${encodeURIComponent(TEMPLATES_BASE_URL + EASY_BUTTONS[0].templateName)}`,
+ ),
+ ).toBe(true);
+ expect(link.includes(`stackName=${EASY_BUTTONS[0].stackName}`)).toBe(true);
+ expect(
+ link.includes(`param_3GITLABRunnerInstanceURL=${encodeURIComponent(getBaseURL())}`),
+ ).toBe(true);
+ });
+
+ it('should track an event when clicked', () => {
+ findFirstButton().vm.$emit('click');
+
+ expect(ExperimentTracking).toHaveBeenCalledWith(EXPERIMENT_NAME);
+ expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
+ `template_clicked_${EASY_BUTTONS[0].stackName}`,
+ );
+ });
+ });
+});
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
new file mode 100644
index 00000000000..639668761ea
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js
@@ -0,0 +1,41 @@
+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/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
new file mode 100644
index 00000000000..d58c87d66cb
--- /dev/null
+++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
@@ -0,0 +1,108 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import {
+ expectedDownloadDropdownProps,
+ securityReportMergeRequestDownloadPathsQueryResponse,
+} from 'jest/vue_shared/security_reports/mock_data';
+import createFlash from '~/flash';
+import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
+import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_SECRET_DETECTION,
+} from '~/vue_shared/security_reports/constants';
+import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql';
+
+jest.mock('~/flash');
+
+describe('Merge request artifact Download', () => {
+ let wrapper;
+
+ const defaultProps = {
+ reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
+ targetProjectFullPath: '/path',
+ mrIid: 123,
+ };
+
+ const createWrapper = ({ propsData, options }) => {
+ wrapper = shallowMount(Component, {
+ stubs: {
+ SecurityReportDownloadDropdown,
+ },
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ ...options,
+ });
+ };
+
+ const pendingHandler = () => new Promise(() => {});
+ const successHandler = () =>
+ Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
+ const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+ const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('given the query is loading', () => {
+ beforeEach(() => {
+ createWrapper({
+ options: {
+ apolloProvider: createMockApolloProvider(pendingHandler),
+ },
+ });
+ });
+
+ it('loading is true', () => {
+ expect(findDownloadDropdown().props('loading')).toBe(true);
+ });
+ });
+
+ describe('given the query loads successfully', () => {
+ beforeEach(() => {
+ createWrapper({
+ options: {
+ apolloProvider: createMockApolloProvider(successHandler),
+ },
+ });
+ });
+
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ });
+ });
+
+ describe('given the query fails', () => {
+ beforeEach(() => {
+ createWrapper({
+ options: {
+ apolloProvider: createMockApolloProvider(failureHandler),
+ },
+ });
+ });
+
+ it('calls createFlash correctly', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: Component.i18n.apiError,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+
+ it('renders nothing', () => {
+ expect(findDownloadDropdown().props('artifacts')).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
deleted file mode 100644
index 68ea94e72ce..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-
-import LabelsSelect from '~/labels_select';
-import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-
-import { mockConfig, mockLabels } from './mock_data';
-
-const createComponent = (config = mockConfig) =>
- shallowMount(BaseComponent, {
- propsData: config,
- });
-
-describe('BaseComponent', () => {
- let wrapper;
- let vm;
-
- beforeEach((done) => {
- wrapper = createComponent();
-
- ({ vm } = wrapper);
-
- Vue.nextTick(done);
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('computed', () => {
- describe('hiddenInputName', () => {
- it('returns correct string when showCreate prop is `true`', () => {
- expect(vm.hiddenInputName).toBe('issue[label_names][]');
- });
-
- it('returns correct string when showCreate prop is `false`', async () => {
- await wrapper.setProps({ showCreate: false });
-
- expect(vm.hiddenInputName).toBe('label_id[]');
- });
- });
-
- describe('createLabelTitle', () => {
- it('returns `Create project label` when `isProject` prop is true', () => {
- expect(vm.createLabelTitle).toBe('Create project label');
- });
-
- it('return `Create group label` when `isProject` prop is false', async () => {
- await wrapper.setProps({ isProject: false });
-
- expect(vm.createLabelTitle).toBe('Create group label');
- });
- });
-
- describe('manageLabelsTitle', () => {
- it('returns `Manage project labels` when `isProject` prop is true', () => {
- expect(vm.manageLabelsTitle).toBe('Manage project labels');
- });
-
- it('return `Manage group labels` when `isProject` prop is false', async () => {
- await wrapper.setProps({ isProject: false });
-
- expect(vm.manageLabelsTitle).toBe('Manage group labels');
- });
- });
- });
-
- describe('methods', () => {
- describe('handleClick', () => {
- it('emits onLabelClick event with label and list of labels as params', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.handleClick(mockLabels[0]);
-
- expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
- });
- });
-
- describe('handleCollapsedValueClick', () => {
- it('emits toggleCollapse event on component', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.handleCollapsedValueClick();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
- });
- });
-
- describe('handleDropdownHidden', () => {
- it('emits onDropdownClose event on component', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.handleDropdownHidden();
-
- expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
- });
- });
- });
-
- describe('mounted', () => {
- it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => {
- expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true);
- });
- });
-
- describe('template', () => {
- it('renders component container element with classes `block labels`', () => {
- expect(vm.$el.classList.contains('block')).toBe(true);
- expect(vm.$el.classList.contains('labels')).toBe(true);
- });
-
- it('renders `.selectbox` element', () => {
- expect(vm.$el.querySelector('.selectbox')).not.toBeNull();
- expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;');
- });
-
- it('renders `.dropdown` element', () => {
- expect(vm.$el.querySelector('.dropdown')).not.toBeNull();
- });
-
- it('renders `.dropdown-menu` element', () => {
- const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
-
- expect(dropdownMenuEl).not.toBeNull();
- expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull();
- expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull();
- expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
deleted file mode 100644
index 79851e5db05..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-
-import { mockConfig, mockLabels } from './mock_data';
-
-const componentConfig = {
- ...mockConfig,
- fieldName: 'label_id[]',
- labels: mockLabels,
- showExtraOptions: false,
-};
-
-const createComponent = (config = componentConfig) => {
- const Component = Vue.extend(dropdownButtonComponent);
-
- return mountComponent(Component, config);
-};
-
-describe('DropdownButtonComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('dropdownToggleText', () => {
- it('returns text as `Label` when `labels` prop is empty array', () => {
- const mockEmptyLabels = { ...componentConfig, labels: [] };
- const vmEmptyLabels = createComponent(mockEmptyLabels);
-
- expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
- vmEmptyLabels.$destroy();
- });
-
- it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
- const mockMoreLabels = { ...componentConfig, labels: mockLabels.concat(mockLabels) };
- const vmMoreLabels = createComponent(mockMoreLabels);
-
- expect(vmMoreLabels.dropdownToggleText).toBe(
- `Foo Label +${mockMoreLabels.labels.length - 1} more`,
- );
- vmMoreLabels.$destroy();
- });
-
- it('returns first label name when `labels` prop has only one item present', () => {
- const singleLabel = { ...componentConfig, labels: [mockLabels[0]] };
- const vmSingleLabel = createComponent(singleLabel);
-
- expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
-
- vmSingleLabel.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element of type `button`', () => {
- expect(vm.$el.nodeName).toBe('BUTTON');
- });
-
- it('renders component container element with required data attributes', () => {
- expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
- expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
- expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
- expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
- expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
- expect(vm.$el.dataset.showAny).not.toBeDefined();
- });
-
- it('renders dropdown toggle text element', () => {
- const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
-
- expect(dropdownToggleTextEl).not.toBeNull();
- expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
- });
-
- it('renders dropdown button icon', () => {
- const dropdownIconEl = vm.$el.querySelector('.dropdown-menu-toggle .gl-icon');
-
- expect(dropdownIconEl).not.toBeNull();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
deleted file mode 100644
index 322e632da02..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-
-import { mockSuggestedColors } from './mock_data';
-
-const createComponent = (headerTitle) => {
- const Component = Vue.extend(dropdownCreateLabelComponent);
-
- return mountComponent(Component, {
- headerTitle,
- });
-};
-
-describe('DropdownCreateLabelComponent', () => {
- const colorsCount = Object.keys(mockSuggestedColors).length;
- let vm;
-
- beforeEach(() => {
- gon.suggested_label_colors = mockSuggestedColors;
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('created', () => {
- it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
- expect(vm.suggestedColors.length).toBe(colorsCount);
- });
- });
-
- describe('template', () => {
- it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => {
- expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true);
- });
-
- it('renders `Go back` button on component header', () => {
- const backButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-back');
-
- expect(backButtonEl).not.toBe(null);
- expect(backButtonEl.querySelector('[data-testid="arrow-left-icon"]')).not.toBe(null);
- });
-
- it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => {
- const headerEl = vm.$el.querySelector('.dropdown-title');
-
- expect(headerEl.innerText.trim()).toContain('Create new label');
- });
-
- it('renders component header element with value of `headerTitle` prop', () => {
- const headerTitle = 'Create project label';
- const vmWithHeaderTitle = createComponent(headerTitle);
- const headerEl = vmWithHeaderTitle.$el.querySelector('.dropdown-title');
-
- expect(headerEl.innerText.trim()).toContain(headerTitle);
- vmWithHeaderTitle.$destroy();
- });
-
- it('renders `Close` button on component header', () => {
- const closeButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-close');
-
- expect(closeButtonEl).not.toBe(null);
- });
-
- it('renders `Name new label` input element', () => {
- expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null);
- expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null);
- });
-
- it('renders suggested colors list elements', () => {
- const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
-
- expect(colorsListContainerEl).not.toBe(null);
- expect(colorsListContainerEl.querySelectorAll('a').length).toBe(colorsCount);
-
- const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
-
- expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
- expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 153, 102);');
- });
-
- it('renders color input element', () => {
- expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null);
- expect(
- vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview'),
- ).not.toBe(null);
-
- expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null);
- });
-
- it('renders component action buttons', () => {
- const createBtnEl = vm.$el.querySelector('button.js-new-label-btn');
- const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn');
-
- expect(createBtnEl).not.toBe(null);
- expect(createBtnEl.innerText.trim()).toBe('Create');
- expect(cancelBtnEl.innerText.trim()).toBe('Cancel');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
deleted file mode 100644
index 7e9e242a4f5..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-
-import { mockConfig } from './mock_data';
-
-const createComponent = (
- labelsWebUrl = mockConfig.labelsWebUrl,
- createLabelTitle,
- manageLabelsTitle,
-) => {
- const Component = Vue.extend(dropdownFooterComponent);
-
- return mountComponent(Component, {
- labelsWebUrl,
- createLabelTitle,
- manageLabelsTitle,
- });
-};
-
-describe('DropdownFooterComponent', () => {
- const createLabelTitle = 'Create project label';
- const manageLabelsTitle = 'Manage project labels';
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders link element with `Create new label` when `createLabelTitle` prop is not provided', () => {
- const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page');
-
- expect(createLabelEl).not.toBeNull();
- expect(createLabelEl.innerText.trim()).toBe('Create new label');
- });
-
- it('renders link element with value of `createLabelTitle` prop', () => {
- const vmWithCreateLabelTitle = createComponent(mockConfig.labelsWebUrl, createLabelTitle);
- const createLabelEl = vmWithCreateLabelTitle.$el.querySelector(
- '.dropdown-footer-list .dropdown-toggle-page',
- );
-
- expect(createLabelEl.innerText.trim()).toBe(createLabelTitle);
- vmWithCreateLabelTitle.$destroy();
- });
-
- it('renders link element with `Manage labels` when `manageLabelsTitle` prop is not provided', () => {
- const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link');
-
- expect(manageLabelsEl).not.toBeNull();
- expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl);
- expect(manageLabelsEl.innerText.trim()).toBe('Manage labels');
- });
-
- it('renders link element with value of `manageLabelsTitle` prop', () => {
- const vmWithManageLabelsTitle = createComponent(
- mockConfig.labelsWebUrl,
- createLabelTitle,
- manageLabelsTitle,
- );
- const manageLabelsEl = vmWithManageLabelsTitle.$el.querySelector(
- '.dropdown-footer-list .dropdown-external-link',
- );
-
- expect(manageLabelsEl.innerText.trim()).toBe(manageLabelsTitle);
- vmWithManageLabelsTitle.$destroy();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
deleted file mode 100644
index 0b9a7262e41..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
-
-const createComponent = () => {
- const Component = Vue.extend(dropdownHeaderComponent);
-
- return mountComponent(Component);
-};
-
-describe('DropdownHeaderComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders header text element', () => {
- const headerEl = vm.$el.querySelector('.dropdown-title span');
-
- expect(headerEl.innerText.trim()).toBe('Assign labels');
- });
-
- it('renders `Close` button element', () => {
- const closeBtnEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-close',
- );
-
- expect(closeBtnEl).not.toBeNull();
- expect(closeBtnEl.querySelector('.dropdown-menu-close-icon')).not.toBeNull();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
deleted file mode 100644
index 510e537b1cd..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
-
-const createComponent = () => {
- const Component = Vue.extend(dropdownSearchInputComponent);
-
- return mountComponent(Component);
-};
-
-describe('DropdownSearchInputComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders input element with type `search`', () => {
- const inputEl = vm.$el.querySelector('input.dropdown-input-field');
-
- expect(inputEl).not.toBeNull();
- expect(inputEl.getAttribute('type')).toBe('search');
- });
-
- it('renders search icon element', () => {
- expect(vm.$el.querySelector('.dropdown-input-search')).not.toBeNull();
- });
-
- it('renders clear search icon element', () => {
- expect(vm.$el.querySelector('.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
deleted file mode 100644
index 30dd92b72a4..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
-
-const createComponent = (canEdit = true) =>
- shallowMount(dropdownTitleComponent, {
- propsData: {
- canEdit,
- },
- });
-
-describe('DropdownTitleComponent', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('template', () => {
- it('renders title text', () => {
- expect(wrapper.vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
- expect(wrapper.vm.$el.innerText.trim()).toContain('Labels');
- });
-
- it('renders spinner icon element', () => {
- expect(wrapper.find(GlLoadingIcon)).not.toBeNull();
- });
-
- it('renders `Edit` button element', () => {
- const editBtnEl = wrapper.vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
-
- expect(editBtnEl).not.toBeNull();
- expect(editBtnEl.innerText.trim()).toBe('Edit');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
deleted file mode 100644
index 37f59c108df..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import { GlLabel } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-
-import { mockConfig, mockLabels } from './mock_data';
-
-const createComponent = (
- labels = mockLabels,
- labelFilterBasePath = mockConfig.labelFilterBasePath,
-) =>
- mount(DropdownValueComponent, {
- propsData: {
- labels,
- labelFilterBasePath,
- enableScopedLabels: true,
- },
- stubs: {
- GlLabel: true,
- },
- });
-
-describe('DropdownValueComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.destroy();
- });
-
- describe('computed', () => {
- describe('isEmpty', () => {
- it('returns true if `labels` prop is empty', () => {
- const vmEmptyLabels = createComponent([]);
-
- expect(vmEmptyLabels.classes()).not.toContain('has-labels');
- vmEmptyLabels.destroy();
- });
-
- it('returns false if `labels` prop is empty', () => {
- expect(vm.classes()).toContain('has-labels');
- });
- });
- });
-
- describe('methods', () => {
- describe('labelFilterUrl', () => {
- it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
- expect(vm.find(GlLabel).props('target')).toBe(
- '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
- );
- });
- });
-
- describe('showScopedLabels', () => {
- it('returns true if the label is scoped label', () => {
- const labels = vm.findAll(GlLabel);
- expect(labels.length).toEqual(2);
- expect(labels.at(1).props('scoped')).toBe(true);
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => {
- expect(vm.classes()).toContain('hide-collapsed', 'value', 'issuable-show-labels');
- });
-
- it('render slot content inside component when `labels` prop is empty', () => {
- const vmEmptyLabels = createComponent([]);
-
- expect(vmEmptyLabels.find('.text-secondary').text().trim()).toBe(mockConfig.emptyValueText);
- vmEmptyLabels.destroy();
- });
-
- it('renders DropdownValueComponent element', () => {
- const labelEl = vm.find(GlLabel);
-
- expect(labelEl.exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
deleted file mode 100644
index 73716d4edf3..00000000000
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
+++ /dev/null
@@ -1,57 +0,0 @@
-export const mockLabels = [
- {
- id: 26,
- title: 'Foo Label',
- description: 'Foobar',
- color: '#BADA55',
- text_color: '#FFFFFF',
- },
- {
- id: 27,
- title: 'Foo::Bar',
- description: 'Foobar',
- color: '#0033CC',
- text_color: '#FFFFFF',
- },
-];
-
-export const mockSuggestedColors = {
- '#009966': 'Green-cyan',
- '#8fbc8f': 'Dark sea green',
- '#3cb371': 'Medium sea green',
- '#00b140': 'Green screen',
- '#013220': 'Dark green',
- '#6699cc': 'Blue-gray',
- '#0000ff': 'Blue',
- '#e6e6fa': 'Lavendar',
- '#9400d3': 'Dark violet',
- '#330066': 'Deep violet',
- '#808080': 'Gray',
- '#36454f': 'Charcoal grey',
- '#f7e7ce': 'Champagne',
- '#c21e56': 'Rose red',
- '#cc338b': 'Magenta-pink',
- '#dc143c': 'Crimson',
- '#ff0000': 'Red',
- '#cd5b45': 'Dark coral',
- '#eee600': 'Titanium yellow',
- '#ed9121': 'Carrot orange',
- '#c39953': 'Aztec Gold',
-};
-
-export const mockConfig = {
- showCreate: true,
- isProject: true,
- abilityName: 'issue',
- context: {
- labels: mockLabels,
- },
- namespace: 'gitlab-org',
- updatePath: '/gitlab-org/my-project/issue/1',
- labelsPath: '/gitlab-org/my-project/-/labels.json',
- labelsWebUrl: '/gitlab-org/my-project/-/labels',
- labelFilterBasePath: '/gitlab-org/my-project/issues',
- canEdit: true,
- suggestedColors: mockSuggestedColors,
- emptyValueText: 'None',
-};
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
index 003f3d2b4e6..8c1693e8dcc 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
-import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
+import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
-import { mockLabels } from './mock_data';
+import { mockCollapsedLabels as mockLabels } from './mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 3f00eab17b7..be849789667 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -2,12 +2,12 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
-import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
+import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
index f293b8422e7..730afcbecab 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
@@ -33,6 +33,23 @@ export const mockLabels = [
},
];
+export const mockCollapsedLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+ {
+ id: 27,
+ title: 'Foo::Bar',
+ description: 'Foobar',
+ color: '#0033CC',
+ text_color: '#FFFFFF',
+ },
+];
+
export const mockConfig = {
allowLabelEdit: true,
allowLabelCreate: true,
diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_mock_data.js b/spec/frontend/vue_shared/components/user_callout_dismisser_mock_data.js
new file mode 100644
index 00000000000..7ca8c619ffc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_callout_dismisser_mock_data.js
@@ -0,0 +1,30 @@
+export const userCalloutsResponse = (callouts = []) => ({
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/46',
+ __typename: 'UserCore',
+ callouts: {
+ __typename: 'UserCalloutConnection',
+ nodes: callouts.map((callout) => ({
+ __typename: 'UserCallout',
+ featureName: callout.toUpperCase(),
+ dismissedAt: '2021-02-12T11:10:01Z',
+ })),
+ },
+ },
+ },
+});
+
+export const anonUserCalloutsResponse = () => ({ data: { currentUser: null } });
+
+export const userCalloutMutationResponse = (variables, errors = []) => ({
+ data: {
+ userCalloutCreate: {
+ errors,
+ userCallout: {
+ featureName: variables.input.featureName.toUpperCase(),
+ dismissedAt: '2021-02-12T11:10:01Z',
+ },
+ },
+ },
+});
diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
new file mode 100644
index 00000000000..70dec42ab32
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
@@ -0,0 +1,306 @@
+import { mount } from '@vue/test-utils';
+import { merge } from 'lodash';
+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 dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
+import getUserCalloutsQuery from '~/graphql_shared/queries/get_user_callouts.query.graphql';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import {
+ anonUserCalloutsResponse,
+ userCalloutMutationResponse,
+ userCalloutsResponse,
+} from './user_callout_dismisser_mock_data';
+
+Vue.use(VueApollo);
+
+const initialSlotProps = (changes = {}) => ({
+ dismiss: expect.any(Function),
+ isAnonUser: false,
+ isDismissed: false,
+ isLoadingQuery: true,
+ isLoadingMutation: false,
+ mutationError: null,
+ queryError: null,
+ shouldShowCallout: false,
+ ...changes,
+});
+
+describe('UserCalloutDismisser', () => {
+ let wrapper;
+
+ const MOCK_FEATURE_NAME = 'mock_feature_name';
+
+ // Query handlers
+ const successHandlerFactory = (dismissedCallouts = []) => async () =>
+ userCalloutsResponse(dismissedCallouts);
+ const anonUserHandler = async () => anonUserCalloutsResponse();
+ const errorHandler = () => Promise.reject(new Error('query error'));
+ const pendingHandler = () => new Promise(() => {});
+
+ // Mutation handlers
+ const mutationSuccessHandlerSpy = jest.fn(async (variables) =>
+ userCalloutMutationResponse(variables),
+ );
+ const mutationErrorHandlerSpy = jest.fn(async (variables) =>
+ userCalloutMutationResponse(variables, ['mutation error']),
+ );
+
+ const defaultScopedSlotSpy = jest.fn();
+
+ const callDismissSlotProp = () => defaultScopedSlotSpy.mock.calls[0][0].dismiss();
+
+ const createComponent = ({ queryHandler, mutationHandler, ...options }) => {
+ wrapper = mount(
+ UserCalloutDismisser,
+ merge(
+ {
+ propsData: {
+ featureName: MOCK_FEATURE_NAME,
+ },
+ scopedSlots: {
+ default: defaultScopedSlotSpy,
+ },
+ apolloProvider: createMockApollo([
+ [getUserCalloutsQuery, queryHandler],
+ [dismissUserCalloutMutation, mutationHandler],
+ ]),
+ },
+ options,
+ ),
+ );
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: pendingHandler,
+ });
+ });
+
+ it('passes expected slot props to child', () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(initialSlotProps());
+ });
+ });
+
+ describe('when loaded and dismissed', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: successHandlerFactory([MOCK_FEATURE_NAME]),
+ });
+
+ return waitForPromises();
+ });
+
+ it('passes expected slot props to child', () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isDismissed: true,
+ isLoadingQuery: false,
+ }),
+ );
+ });
+ });
+
+ describe('when loaded and not dismissed', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: successHandlerFactory(),
+ });
+
+ return waitForPromises();
+ });
+
+ it('passes expected slot props to child', () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isLoadingQuery: false,
+ shouldShowCallout: true,
+ }),
+ );
+ });
+ });
+
+ describe('when loaded with errors', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: errorHandler,
+ });
+
+ return waitForPromises();
+ });
+
+ it('passes expected slot props to child', () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isLoadingQuery: false,
+ queryError: expect.any(Error),
+ }),
+ );
+ });
+ });
+
+ describe('when loaded and the user is anonymous', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: anonUserHandler,
+ });
+
+ return waitForPromises();
+ });
+
+ it('passes expected slot props to child', () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isAnonUser: true,
+ isLoadingQuery: false,
+ }),
+ );
+ });
+ });
+
+ describe('when skipQuery is true', () => {
+ let queryHandler;
+ beforeEach(() => {
+ queryHandler = jest.fn();
+
+ createComponent({
+ queryHandler,
+ propsData: {
+ skipQuery: true,
+ },
+ });
+ });
+
+ it('does not run the query', async () => {
+ expect(queryHandler).not.toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(queryHandler).not.toHaveBeenCalled();
+ });
+
+ it('passes expected slot props to child', () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isLoadingQuery: false,
+ shouldShowCallout: true,
+ }),
+ );
+ });
+ });
+
+ describe('dismissing', () => {
+ describe('given it succeeds', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: successHandlerFactory(),
+ mutationHandler: mutationSuccessHandlerSpy,
+ });
+
+ return waitForPromises();
+ });
+
+ it('dismissing calls mutation', () => {
+ expect(mutationSuccessHandlerSpy).not.toHaveBeenCalled();
+
+ callDismissSlotProp();
+
+ expect(mutationSuccessHandlerSpy).toHaveBeenCalledWith({
+ input: { featureName: MOCK_FEATURE_NAME },
+ });
+ });
+
+ it('passes expected slot props to child', async () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isLoadingQuery: false,
+ shouldShowCallout: true,
+ }),
+ );
+
+ callDismissSlotProp();
+
+ // Wait for Vue re-render due to prop change
+ await nextTick();
+
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isDismissed: true,
+ isLoadingMutation: true,
+ isLoadingQuery: false,
+ }),
+ );
+
+ // Wait for mutation to resolve
+ await waitForPromises();
+
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isDismissed: true,
+ isLoadingQuery: false,
+ }),
+ );
+ });
+ });
+
+ describe('given it fails', () => {
+ beforeEach(() => {
+ createComponent({
+ queryHandler: successHandlerFactory(),
+ mutationHandler: mutationErrorHandlerSpy,
+ });
+
+ return waitForPromises();
+ });
+
+ it('calls mutation', () => {
+ expect(mutationErrorHandlerSpy).not.toHaveBeenCalled();
+
+ callDismissSlotProp();
+
+ expect(mutationErrorHandlerSpy).toHaveBeenCalledWith({
+ input: { featureName: MOCK_FEATURE_NAME },
+ });
+ });
+
+ it('passes expected slot props to child', async () => {
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isLoadingQuery: false,
+ shouldShowCallout: true,
+ }),
+ );
+
+ callDismissSlotProp();
+
+ // Wait for Vue re-render due to prop change
+ await nextTick();
+
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isDismissed: true,
+ isLoadingMutation: true,
+ isLoadingQuery: false,
+ }),
+ );
+
+ // Wait for mutation to resolve
+ await waitForPromises();
+
+ expect(defaultScopedSlotSpy).lastCalledWith(
+ initialSlotProps({
+ isDismissed: true,
+ isLoadingQuery: false,
+ mutationError: ['mutation error'],
+ }),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 5a609568220..0fabc6525ea 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -49,10 +49,13 @@ describe('User select dropdown', () => {
const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
+ const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
+ const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
+
const createComponent = ({
props = {},
- searchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse),
- participantsQueryHandler = jest.fn().mockResolvedValue(participantsQueryResponse),
+ searchQueryHandler = searchQueryHandlerSuccess,
+ participantsQueryHandler = participantsQueryHandlerSuccess,
} = {}) => {
fakeApollo = createMockApollo([
[searchUsersQuery, searchQueryHandler],
@@ -91,6 +94,14 @@ describe('User select dropdown', () => {
expect(findParticipantsLoading().exists()).toBe(true);
});
+ it('skips the queries if `isEditing` prop is false', () => {
+ createComponent({ props: { isEditing: false } });
+
+ expect(findParticipantsLoading().exists()).toBe(false);
+ expect(searchQueryHandlerSuccess).not.toHaveBeenCalled();
+ expect(participantsQueryHandlerSuccess).not.toHaveBeenCalled();
+ });
+
it('emits an `error` event if participants query was rejected', async () => {
createComponent({ participantsQueryHandler: mockError });
await waitForPromises();
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 eb23a8ef457..5a6c91bda9f 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -3,8 +3,8 @@ import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
-const TEST_EDIT_URL = '/gitlab-test/test/-/edit/master/';
-const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/master/-/';
+const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/';
+const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
const ACTION_EDIT = {
diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js
index 9e9cb59c0d6..8f4b4b08f50 100644
--- a/spec/frontend/whats_new/components/feature_spec.js
+++ b/spec/frontend/whats_new/components/feature_spec.js
@@ -8,7 +8,7 @@ describe("What's new single feature", () => {
const exampleFeature = {
title: 'Compliance pipeline configurations',
body:
- '<p>We are thrilled to announce that it is now possible to define enforceable pipelines that will run for any project assigned a corresponding compliance framework.</p>',
+ '<p data-testid="body-content">We are thrilled to announce that it is now possible to define enforceable pipelines that will run for any project assigned a corresponding <a href="https://en.wikipedia.org/wiki/Compliance_(psychology)" target="_blank" rel="noopener noreferrer" onload="alert(xss)">compliance</a> framework.</p>',
stage: 'Manage',
'self-managed': true,
'gitlab-com': true,
@@ -20,6 +20,7 @@ describe("What's new single feature", () => {
};
const findReleaseDate = () => wrapper.find('[data-testid="release-date"]');
+ const findBodyAnchor = () => wrapper.find('[data-testid="body-content"] a');
const createWrapper = ({ feature } = {}) => {
wrapper = shallowMount(Feature, {
@@ -43,4 +44,13 @@ describe("What's new single feature", () => {
expect(findReleaseDate().exists()).toBe(false);
});
});
+
+ it('safe-html config allows target attribute on elements', () => {
+ createWrapper({ feature: exampleFeature });
+ expect(findBodyAnchor().attributes()).toEqual({
+ href: expect.any(String),
+ rel: 'noopener noreferrer',
+ target: '_blank',
+ });
+ });
});
diff --git a/spec/generator_helper.rb b/spec/generator_helper.rb
deleted file mode 100644
index d35eaac45bd..00000000000
--- a/spec/generator_helper.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.configure do |config|
- # Redirect stdout so specs don't have so much noise
- config.before(:all) do
- $stdout = StringIO.new
- end
-
- # Reset stdout
- config.after(:all) do
- $stdout = STDOUT
- end
-end
diff --git a/spec/graphql/mutations/ci/runner/delete_spec.rb b/spec/graphql/mutations/ci/runner/delete_spec.rb
new file mode 100644
index 00000000000..82873c96c3e
--- /dev/null
+++ b/spec/graphql/mutations/ci/runner/delete_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Ci::Runner::Delete do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:runner) { create(:ci_runner) }
+
+ let(:current_ctx) { { current_user: user } }
+
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id
+ }
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:delete_runner) }
+
+ describe '#resolve' do
+ subject do
+ sync(resolve(described_class, args: mutation_params, ctx: current_ctx))
+ end
+
+ context 'when the user cannot admin the runner' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'raises an error' do
+ mutation_params[:id] = "invalid-id"
+
+ expect { subject }.to raise_error(::GraphQL::CoercionError)
+ end
+ end
+
+ context 'when required arguments are missing' do
+ let(:mutation_params) { {} }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, "missing keyword: :id")
+ end
+ end
+
+ context 'when user can delete owned runner' do
+ let_it_be(:project) { create(:project, creator_id: user.id) }
+ let_it_be(:project_runner, reload: true) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ context 'with one associated project' do
+ it 'deletes runner' do
+ mutation_params[:id] = project_runner.to_global_id
+
+ expect { subject }.to change { Ci::Runner.count }.by(-1)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'with more than one associated project' do
+ let_it_be(:project2) { create(:project, creator_id: user.id) }
+ let_it_be(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
+
+ before_all do
+ project2.add_maintainer(user)
+ end
+
+ it 'does not delete project runner' do
+ mutation_params[:id] = two_projects_runner.to_global_id
+
+ expect { subject }.not_to change { Ci::Runner.count }
+ expect(subject[:errors]).to contain_exactly("Runner #{two_projects_runner.to_global_id} associated with more than one project")
+ end
+ end
+ end
+
+ context 'when admin can delete runner', :enable_admin_mode do
+ let(:admin_user) { create(:user, :admin) }
+ let(:current_ctx) { { current_user: admin_user } }
+
+ it 'deletes runner' do
+ expect { subject }.to change { Ci::Runner.count }.by(-1)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
new file mode 100644
index 00000000000..3db0d552a05
--- /dev/null
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Ci::Runner::Update do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:runner) { create(:ci_runner, active: true, locked: false, run_untagged: true) }
+
+ let(:current_ctx) { { current_user: user } }
+ let(:mutated_runner) { subject[:runner] }
+
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id,
+ description: 'updated description'
+ }
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_runner) }
+
+ describe '#resolve' do
+ subject do
+ sync(resolve(described_class, args: mutation_params, ctx: current_ctx))
+ end
+
+ context 'when the user cannot admin the runner' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'raises an error' do
+ mutation_params[:id] = "invalid-id"
+
+ expect { subject }.to raise_error(::GraphQL::CoercionError)
+ end
+ end
+
+ context 'when required arguments are missing' do
+ let(:mutation_params) { {} }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, "missing keyword: :id")
+ end
+ end
+
+ context 'when user can update runner', :enable_admin_mode do
+ let(:admin_user) { create(:user, :admin) }
+ let(:current_ctx) { { current_user: admin_user } }
+
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id,
+ description: 'updated description',
+ maximum_timeout: 900,
+ access_level: 'ref_protected',
+ active: false,
+ locked: true,
+ run_untagged: false,
+ tag_list: %w(tag1 tag2)
+ }
+ end
+
+ context 'with valid arguments' do
+ it 'updates runner with correct values' do
+ expected_attributes = mutation_params.except(:id, :tag_list)
+
+ subject
+
+ expect(subject[:errors]).to be_empty
+ expect(subject[:runner]).to be_an_instance_of(Ci::Runner)
+ expect(subject[:runner]).to have_attributes(expected_attributes)
+ expect(subject[:runner].tag_list).to contain_exactly(*mutation_params[:tag_list])
+ expect(runner.reload).to have_attributes(expected_attributes)
+ expect(runner.tag_list).to contain_exactly(*mutation_params[:tag_list])
+ end
+ end
+
+ context 'with out-of-range maximum_timeout and missing tag_list' do
+ it 'returns a descriptive error' do
+ mutation_params[:maximum_timeout] = 100
+ mutation_params.delete(:tag_list)
+
+ expect(subject[:errors]).to contain_exactly(
+ 'Maximum timeout needs to be at least 10 minutes',
+ 'Tags list can not be empty when runner is not allowed to pick untagged jobs'
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/commits/create_spec.rb b/spec/graphql/mutations/commits/create_spec.rb
index 152b5d87da0..097e70bada6 100644
--- a/spec/graphql/mutations/commits/create_spec.rb
+++ b/spec/graphql/mutations/commits/create_spec.rb
@@ -74,6 +74,10 @@ RSpec.describe Mutations::Commits::Create do
expect(commit_pipeline_path).to match(%r(pipelines/sha/\w+))
end
+ it 'returns the content of the commit' do
+ expect(subject[:content]).to eq(actions.pluck(:content))
+ end
+
it 'returns a new commit' do
expect(mutated_commit).to have_attributes(message: message, project: project)
expect(subject[:errors]).to be_empty
@@ -166,6 +170,7 @@ RSpec.describe Mutations::Commits::Create do
it 'returns a new commit' do
expect(mutated_commit).to have_attributes(message: message, project: project)
expect(subject[:errors]).to be_empty
+ expect(subject[:content]).to eq(actions.pluck(:content))
expect_to_contain_deltas([
a_hash_including(a_mode: '0', b_mode: '100644', new_file: true, new_path: 'ANOTHER_FILE.md')
diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb
index ada88b7652c..1e585e55be4 100644
--- a/spec/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/graphql/mutations/design_management/upload_spec.rb
@@ -105,7 +105,7 @@ RSpec.describe Mutations::DesignManagement::Upload do
context "with a valid design" do
it "returns the updated designs" do
- expect(resolve[:errors]).to eq []
+ expect(resolve[:errors]).to be_empty
expect(resolve[:designs].map(&:filename)).to contain_exactly("dk.png")
end
end
diff --git a/spec/graphql/mutations/issues/set_subscription_spec.rb b/spec/graphql/mutations/issues/set_subscription_spec.rb
index 9e05a136c0b..7e2c3d93c51 100644
--- a/spec/graphql/mutations/issues/set_subscription_spec.rb
+++ b/spec/graphql/mutations/issues/set_subscription_spec.rb
@@ -3,8 +3,38 @@
require 'spec_helper'
RSpec.describe Mutations::Issues::SetSubscription do
- it_behaves_like 'a subscribeable graphql resource' do
- let_it_be(:resource) { create(:issue) }
- let(:permission_name) { :update_issue }
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be_with_reload(:resource) { create(:issue, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_subscription) }
+
+ context 'when user does not have access to the project' do
+ it_behaves_like 'a subscribeable not accessible graphql resource'
+ end
+
+ context 'when user is developer member of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'a subscribeable graphql resource'
+ end
+
+ context 'when the project is public' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it_behaves_like 'a subscribeable graphql resource'
+ end
+
+ context 'when the project is public but the issue is confidential' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ resource.update!(confidential: true)
+ end
+
+ it_behaves_like 'a subscribeable not accessible graphql resource'
end
end
diff --git a/spec/graphql/mutations/issues/update_spec.rb b/spec/graphql/mutations/issues/update_spec.rb
index 6d6a5b94219..bd780477658 100644
--- a/spec/graphql/mutations/issues/update_spec.rb
+++ b/spec/graphql/mutations/issues/update_spec.rb
@@ -69,33 +69,17 @@ RSpec.describe Mutations::Issues::Update do
context 'when changing state' do
let_it_be_with_refind(:issue) { create(:issue, project: project, state: :opened) }
- before do
- mutation_params[:state_event] = state_event
- end
-
- context 'when state_event is close' do
- let_it_be(:removable_label) { create(:label, project: project, remove_on_close: true, issues: [issue]) }
+ it 'closes issue' do
+ mutation_params[:state_event] = 'close'
- let(:state_event) { 'close' }
-
- it 'closes issue' do
- expect do
- subject
- issue.reload
- end.to change(issue, :state).from('opened').to('closed').and(
- change { issue.label_ids }.from([removable_label.id]).to([])
- )
- end
+ expect { subject }.to change { issue.reload.state }.from('opened').to('closed')
end
- context 'when state_event is reopen' do
- let(:state_event) { 'reopen' }
-
- it 'reopens issue' do
- issue.close
+ it 'reopens issue' do
+ issue.close
+ mutation_params[:state_event] = 'reopen'
- expect { subject }.to change { issue.reload.state }.from('closed').to('opened')
- end
+ expect { subject }.to change { issue.reload.state }.from('closed').to('opened')
end
end
diff --git a/spec/graphql/mutations/merge_requests/set_subscription_spec.rb b/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
index 600053637c9..377042f068c 100644
--- a/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
@@ -3,8 +3,30 @@
require 'spec_helper'
RSpec.describe Mutations::MergeRequests::SetSubscription do
- it_behaves_like 'a subscribeable graphql resource' do
- let_it_be(:resource) { create(:merge_request) }
- let(:permission_name) { :update_merge_request }
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:resource) { create(:merge_request, source_project: project, target_project: project) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_subscription) }
+
+ context 'when user does not have access to the project' do
+ it_behaves_like 'a subscribeable not accessible graphql resource'
+ end
+
+ context 'when user is developer member of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'a subscribeable graphql resource'
+ end
+
+ context 'when the project is public' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it_behaves_like 'a subscribeable graphql resource'
end
end
diff --git a/spec/graphql/mutations/todos/mark_all_done_spec.rb b/spec/graphql/mutations/todos/mark_all_done_spec.rb
index f3b6bf52ef7..1e12d86aa18 100644
--- a/spec/graphql/mutations/todos/mark_all_done_spec.rb
+++ b/spec/graphql/mutations/todos/mark_all_done_spec.rb
@@ -21,26 +21,25 @@ RSpec.describe Mutations::Todos::MarkAllDone do
describe '#resolve' do
it 'marks all pending todos as done' do
- updated_todo_ids, todos = mutation_for(current_user).resolve.values_at(:updated_ids, :todos)
+ todos = mutation_for(current_user).resolve[:todos]
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('done')
expect(todo3.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
- expect(updated_todo_ids).to contain_exactly(todo1.id, todo3.id)
expect(todos).to contain_exactly(todo1, todo3)
end
it 'behaves as expected if there are no todos for the requesting user' do
- updated_todo_ids = mutation_for(user3).resolve.dig(:updated_ids)
+ todos = mutation_for(user3).resolve[:todos]
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(todo3.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('pending')
- expect(updated_todo_ids).to be_empty
+ expect(todos).to be_empty
end
context 'when user is not logged in' do
diff --git a/spec/graphql/resolvers/boards_resolver_spec.rb b/spec/graphql/resolvers/boards_resolver_spec.rb
index 221e905f441..07d0902d3ba 100644
--- a/spec/graphql/resolvers/boards_resolver_spec.rb
+++ b/spec/graphql/resolvers/boards_resolver_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Resolvers::BoardsResolver do
shared_examples_for 'group and project boards resolver' do
it 'does not create a default board' do
- expect(resolve_boards).to eq []
+ expect(resolve_boards).to be_empty
end
it 'calls Boards::BoardsFinder' do
diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
index 006d6785506..5ac15d5729f 100644
--- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb
@@ -10,15 +10,15 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:inactive_project_runner) do
- create(:ci_runner, :project, projects: [project], active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
+ create(:ci_runner, :project, projects: [project], description: 'inactive project runner', token: 'abcdef', active: false, contacted_at: 1.minute.ago, tag_list: %w(project_runner))
end
let_it_be(:offline_project_runner) do
- create(:ci_runner, :project, projects: [project], contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
+ create(:ci_runner, :project, projects: [project], description: 'offline project runner', token: 'defghi', contacted_at: 1.day.ago, tag_list: %w(project_runner active_runner))
end
- let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], contacted_at: 1.second.ago) }
- let_it_be(:instance_runner) { create(:ci_runner, :instance, contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 1.second.ago) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
describe '#resolve' do
subject { resolve(described_class, ctx: { current_user: user }, args: args).items.to_a }
@@ -27,6 +27,14 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
{}
end
+ context 'when the user cannot see runners' do
+ let(:user) { create(:user) }
+
+ it 'returns no runners' do
+ is_expected.to be_empty
+ end
+ end
+
context 'without sort' do
it 'returns all the runners' do
is_expected.to contain_exactly(inactive_project_runner, offline_project_runner, group_runner, instance_runner)
@@ -42,13 +50,29 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner]) }
end
- context "set to :created_date" do
+ context "set to :contacted_desc" do
+ let(:args) do
+ { sort: :contacted_desc }
+ end
+
+ it { is_expected.to eq([offline_project_runner, instance_runner, inactive_project_runner, group_runner].reverse) }
+ end
+
+ context "set to :created_at_desc" do
let(:args) do
- { sort: :created_date }
+ { sort: :created_at_desc }
end
it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner]) }
end
+
+ context "set to :created_at_asc" do
+ let(:args) do
+ { sort: :created_at_asc }
+ end
+
+ it { is_expected.to eq([instance_runner, group_runner, offline_project_runner, inactive_project_runner].reverse) }
+ end
end
context 'when type is filtered' do
@@ -132,5 +156,35 @@ RSpec.describe Resolvers::Ci::RunnersResolver do
end
end
end
+
+ context 'when text is filtered' do
+ let(:args) do
+ { search: search_term }
+ end
+
+ context 'to "project"' do
+ let(:search_term) { 'project' }
+
+ it 'returns both project runners' do
+ is_expected.to contain_exactly(inactive_project_runner, offline_project_runner)
+ end
+ end
+
+ context 'to "group"' do
+ let(:search_term) { 'group' }
+
+ it 'returns group runner' do
+ is_expected.to contain_exactly(group_runner)
+ end
+ end
+
+ context 'to "defghi"' do
+ let(:search_term) { 'defghi' }
+
+ it 'returns runners containing term in token' do
+ is_expected.to contain_exactly(offline_project_runner)
+ end
+ end
+ end
end
end
diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
index e9e7fff6e6e..8d15d7eda1b 100644
--- a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
+++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe ::CachingArrayResolver do
include GraphqlHelpers
- include Gitlab::Graphql::Laziness
let_it_be(:admins) { create_list(:user, 4, admin: true) }
let(:query_context) { { current_user: admins.first } }
diff --git a/spec/graphql/resolvers/group_packages_resolver_spec.rb b/spec/graphql/resolvers/group_packages_resolver_spec.rb
index 48f4c8ec4ca..eba3a5f2de8 100644
--- a/spec/graphql/resolvers/group_packages_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_packages_resolver_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Resolvers::GroupPackagesResolver do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public) }
- let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:project) { create(:project, :public, group: group, path: 'a') }
let(:args) do
{ sort: :created_desc }
@@ -17,5 +17,25 @@ RSpec.describe Resolvers::GroupPackagesResolver do
subject { resolve(described_class, ctx: { current_user: user }, obj: group, args: args).to_a }
it_behaves_like 'group and projects packages resolver'
+
+ describe 'project_path sorting' do
+ let_it_be(:project2) { create(:project, :public, group: group, path: 'b') }
+ let_it_be(:package) { create(:package, project: project ) }
+ let_it_be(:package2) { create(:package, project: project2 ) }
+ let_it_be(:package3) { create(:package, project: project ) }
+ let_it_be(:package4) { create(:package, project: project2 ) }
+
+ context 'filter by package_name' do
+ let(:args) { { sort: :project_path_desc } }
+
+ it { is_expected.to eq([package4, package2, package3, package]) }
+ end
+
+ context 'filter by package_type' do
+ let(:args) { { sort: :project_path_asc } }
+
+ it { is_expected.to eq([package, package3, package2, package4]) }
+ end
+ end
end
end
diff --git a/spec/graphql/resolvers/package_details_resolver_spec.rb b/spec/graphql/resolvers/package_details_resolver_spec.rb
index 1bdc069b3bb..d6acb31d4e3 100644
--- a/spec/graphql/resolvers/package_details_resolver_spec.rb
+++ b/spec/graphql/resolvers/package_details_resolver_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Resolvers::PackageDetailsResolver do
include GraphqlHelpers
- include ::Gitlab::Graphql::Laziness
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:user) { project.owner }
diff --git a/spec/graphql/resolvers/projects_resolver_spec.rb b/spec/graphql/resolvers/projects_resolver_spec.rb
index 34ddc9cd8cb..2f2aacb9ad5 100644
--- a/spec/graphql/resolvers/projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects_resolver_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Resolvers::ProjectsResolver do
let_it_be(:group) { create(:group, name: 'public-group') }
let_it_be(:private_group) { create(:group, name: 'private-group') }
- let_it_be(:project) { create(:project, :public) }
+ let_it_be(:project) { create(:project, :public, topic_list: %w(ruby javascript)) }
let_it_be(:other_project) { create(:project, :public) }
let_it_be(:group_project) { create(:project, :public, group: group) }
let_it_be(:private_project) { create(:project, :private) }
@@ -70,6 +70,14 @@ RSpec.describe Resolvers::ProjectsResolver do
is_expected.to be_empty
end
end
+
+ context 'when topics filter is provided' do
+ let(:filters) { { topics: %w(ruby) } }
+
+ it 'returns matching project' do
+ is_expected.to contain_exactly(project)
+ end
+ end
end
end
@@ -138,6 +146,14 @@ RSpec.describe Resolvers::ProjectsResolver do
is_expected.to match_array([named_project3, named_project1, named_project2])
end
end
+
+ context 'when topics filter is provided' do
+ let(:filters) { { topics: %w(ruby) } }
+
+ it 'returns matching project' do
+ is_expected.to contain_exactly(project)
+ end
+ end
end
end
end
diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb
index 585cd657e35..bb4938c751f 100644
--- a/spec/graphql/resolvers/timelog_resolver_spec.rb
+++ b/spec/graphql/resolvers/timelog_resolver_spec.rb
@@ -11,26 +11,27 @@ RSpec.describe Resolvers::TimelogResolver do
context "with a group" do
let_it_be(:current_user) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, :public, group: group) }
-
- before_all do
- group.add_developer(current_user)
- project.add_developer(current_user)
- end
-
- before do
- group.clear_memoization(:timelogs)
- end
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :empty_repo, :public, group: group) }
describe '#resolve' do
+ let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day }
+ let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day }
+
let_it_be(:issue) { create(:issue, project: project) }
- let_it_be(:issue2) { create(:issue, project: project) }
- let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
- let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
- let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.beginning_of_day) }
+ let_it_be(:timelog2) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.end_of_day) }
+ let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, spent_at: medium_time_ago) }
+
+ let(:args) { { start_time: short_time_ago, end_time: short_time_ago.noon } }
+
+ it 'finds all timelogs' do
+ timelogs = resolve_timelogs
- let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
+ expect(timelogs).to contain_exactly(timelog1, timelog2, timelog3)
+ end
it 'finds all timelogs within given dates' do
timelogs = resolve_timelogs(**args)
@@ -38,15 +39,28 @@ RSpec.describe Resolvers::TimelogResolver do
expect(timelogs).to contain_exactly(timelog1)
end
- it 'return nothing when user has insufficient permissions' do
- user = create(:user)
- group.add_guest(current_user)
+ context 'when only start_date is present' do
+ let(:args) { { start_date: short_time_ago } }
+
+ it 'finds timelogs until the end of day of end_date' do
+ timelogs = resolve_timelogs(**args)
+
+ expect(timelogs).to contain_exactly(timelog1, timelog2)
+ end
+ end
+
+ context 'when only end_date is present' do
+ let(:args) { { end_date: medium_time_ago } }
+
+ it 'finds timelogs until the end of day of end_date' do
+ timelogs = resolve_timelogs(**args)
- expect(resolve_timelogs(user: user, **args)).to be_empty
+ expect(timelogs).to contain_exactly(timelog3)
+ end
end
context 'when start_time and end_date are present' do
- let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
+ let(:args) { { start_time: short_time_ago, end_date: short_time_ago } }
it 'finds timelogs until the end of day of end_date' do
timelogs = resolve_timelogs(**args)
@@ -56,7 +70,7 @@ RSpec.describe Resolvers::TimelogResolver do
end
context 'when start_date and end_time are present' do
- let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
+ let(:args) { { start_date: short_time_ago, end_time: short_time_ago.noon } }
it 'finds all timelogs within start_date and end_time' do
timelogs = resolve_timelogs(**args)
@@ -68,95 +82,32 @@ RSpec.describe Resolvers::TimelogResolver do
context 'when arguments are invalid' do
let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError }
- context 'when no time or date arguments are present' do
- let(:args) { {} }
-
- it 'returns correct error' do
- expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Start and End arguments must be present/)
- end
- end
-
- context 'when only start_time is present' do
- let(:args) { { start_time: 6.days.ago } }
-
- it 'returns correct error' do
- expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Both Start and End arguments must be present/)
- end
- end
-
- context 'when only end_time is present' do
- let(:args) { { end_time: 2.days.ago } }
-
- it 'returns correct error' do
- expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Both Start and End arguments must be present/)
- end
- end
-
- context 'when only start_date is present' do
- let(:args) { { start_date: 6.days.ago } }
-
- it 'returns correct error' do
- expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Both Start and End arguments must be present/)
- end
- end
-
- context 'when only end_date is present' do
- let(:args) { { end_date: 2.days.ago } }
-
- it 'returns correct error' do
- expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Both Start and End arguments must be present/)
- end
- end
-
context 'when start_time and start_date are present' do
- let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
+ let(:args) { { start_time: short_time_ago, start_date: short_time_ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Both Start and End arguments must be present/)
+ .to raise_error(error_class, /Provide either a start date or time, but not both/)
end
end
context 'when end_time and end_date are present' do
- let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
+ let(:args) { { end_time: short_time_ago, end_date: short_time_ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Both Start and End arguments must be present/)
- end
- end
-
- context 'when three arguments are present' do
- let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
-
- it 'returns correct error' do
- expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /Only Time or Date arguments must be present/)
+ .to raise_error(error_class, /Provide either an end date or time, but not both/)
end
end
context 'when start argument is after end argument' do
- let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
+ let(:args) { { start_time: short_time_ago, end_time: medium_time_ago } }
it 'returns correct error' do
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Start argument must be before End argument/)
end
end
-
- context 'when time range is more than 60 days' do
- let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
-
- it 'returns correct error' do
- expect { resolve_timelogs(**args) }
- .to raise_error(error_class, /The time range period cannot contain more than 60 days/)
- end
- end
end
end
end
diff --git a/spec/graphql/types/ci/runner_type_spec.rb b/spec/graphql/types/ci/runner_type_spec.rb
index dfe4a30c5b7..f27216f4d39 100644
--- a/spec/graphql/types/ci/runner_type_spec.rb
+++ b/spec/graphql/types/ci/runner_type_spec.rb
@@ -2,15 +2,17 @@
require 'spec_helper'
-RSpec.describe Types::Ci::RunnerType do
+RSpec.describe GitlabSchema.types['CiRunner'] do
specify { expect(described_class.graphql_name).to eq('CiRunner') }
+ specify { expect(described_class).to require_graphql_authorizations(:read_runner) }
+
it 'contains attributes related to a runner' do
expected_fields = %w[
id description contacted_at maximum_timeout access_level active status
version short_sha revision locked run_untagged ip_address runner_type tag_list
]
- expect(described_class).to have_graphql_fields(*expected_fields)
+ expect(described_class).to include_graphql_fields(*expected_fields)
end
end
diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb
index 4df51dc8d1b..37f59770817 100644
--- a/spec/graphql/types/global_id_type_spec.rb
+++ b/spec/graphql/types/global_id_type_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
RSpec.describe Types::GlobalIDType do
+ include ::Gitlab::Graphql::Laziness
+ include GraphqlHelpers
+ include GlobalIDDeprecationHelpers
+
let_it_be(:project) { create(:project) }
let(:gid) { project.to_global_id }
@@ -97,6 +101,142 @@ RSpec.describe Types::GlobalIDType do
expect { type.coerce_isolated_input(invalid_gid) }
.to raise_error(GraphQL::CoercionError, /does not represent an instance of Project/)
end
+
+ context 'with a deprecation' do
+ around(:all) do |example|
+ # Unset all previously memoized GlobalIDTypes to allow us to define one
+ # that will use the constants stubbed in the `before` block.
+ previous_id_types = Types::GlobalIDType.instance_variable_get(:@id_types)
+ Types::GlobalIDType.instance_variable_set(:@id_types, {})
+
+ example.run
+ ensure
+ Types::GlobalIDType.instance_variable_set(:@id_types, previous_id_types)
+ end
+
+ before do
+ deprecation = Gitlab::GlobalId::Deprecations::Deprecation.new(old_model_name: 'OldIssue', new_model_name: 'Issue', milestone: '10.0')
+
+ stub_global_id_deprecations(deprecation)
+ end
+
+ let_it_be(:issue) { create(:issue) }
+
+ let!(:type) { ::Types::GlobalIDType[::Issue] }
+ let(:deprecated_gid) { Gitlab::GlobalId.build(model_name: 'OldIssue', id: issue.id) }
+ let(:deprecating_gid) { Gitlab::GlobalId.build(model_name: 'Issue', id: issue.id) }
+
+ it 'appends the description with a deprecation notice for the old Global ID' do
+ expect(type.to_graphql.description).to include('The older format `"gid://gitlab/OldIssue/1"` was deprecated in 10.0')
+ end
+
+ describe 'coercing input against the type (parsing the Global ID string when supplied as an argument)' do
+ subject(:result) { type.coerce_isolated_input(gid.to_s) }
+
+ context 'when passed the deprecated Global ID' do
+ let(:gid) { deprecated_gid }
+
+ it 'changes the model_name to the new model name' do
+ expect(result.model_name).to eq('Issue')
+ end
+
+ it 'changes the model_class to the new model class' do
+ expect(result.model_class).to eq(Issue)
+ end
+
+ it 'can find the correct resource' do
+ expect(result.find).to eq(issue)
+ end
+
+ it 'can find the correct resource loaded through GitlabSchema' do
+ expect(force(GitlabSchema.object_from_id(result, expected_class: Issue))).to eq(issue)
+ end
+ end
+
+ context 'when passed the Global ID that is deprecating another' do
+ let(:gid) { deprecating_gid }
+
+ it 'works as normal' do
+ expect(result).to have_attributes(
+ model_class: Issue,
+ model_name: 'Issue',
+ find: issue,
+ to_s: gid.to_s
+ )
+ end
+ end
+ end
+
+ describe 'coercing the result against the type (producing the Global ID string when used in a field)' do
+ context 'when passed the deprecated Global ID' do
+ let(:gid) { deprecated_gid }
+
+ it 'works, but does not result in matching the new Global ID', :aggregate_failures do
+ # Note, this would normally never happen in real life as the object being parsed
+ # by the field would not produce the GlobalID of the deprecated model. This test
+ # proves that it is technically possible for the deprecated GlobalID to be
+ # considered parsable for the type, as opposed to raising a `GraphQL::CoercionError`.
+ expect(type.coerce_isolated_result(gid)).not_to eq(issue.to_global_id.to_s)
+ expect(type.coerce_isolated_result(gid)).to eq(gid.to_s)
+ end
+ end
+
+ context 'when passed the Global ID that is deprecating another' do
+ let(:gid) { deprecating_gid }
+
+ it 'works as normal' do
+ expect(type.coerce_isolated_result(gid)).to eq(issue.to_global_id.to_s)
+ end
+ end
+ end
+
+ describe 'executing against the schema' do
+ let(:query_result) do
+ context = { current_user: issue.project.owner }
+ variables = { 'id' => gid }
+
+ run_with_clean_state(query, context: context, variables: variables).to_h
+ end
+
+ shared_examples 'a query that works with old and new GIDs' do
+ let(:query) do
+ <<-GQL
+ query($id: #{argument_name}!) {
+ issue(id: $id) {
+ id
+ }
+ }
+ GQL
+ end
+
+ subject { query_result.dig('data', 'issue', 'id') }
+
+ context 'when the argument value is the new GID' do
+ let(:gid) { Gitlab::GlobalId.build(model_name: 'Issue', id: issue.id) }
+
+ it { is_expected.to be_present }
+ end
+
+ context 'when the argument value is the old GID' do
+ let(:gid) { Gitlab::GlobalId.build(model_name: 'OldIssue', id: issue.id) }
+
+ it { is_expected.to be_present }
+ end
+ end
+
+ context 'when the query signature includes the old type name' do
+ let(:argument_name) { 'OldIssueID' }
+
+ it_behaves_like 'a query that works with old and new GIDs'
+ end
+
+ context 'when the query signature includes the new type name' do
+ let(:argument_name) { 'IssueID' }
+
+ it_behaves_like 'a query that works with old and new GIDs'
+ end
+ end
+ end
end
describe 'a parameterized type with a namespace' do
@@ -231,4 +371,10 @@ RSpec.describe Types::GlobalIDType do
end
end
end
+
+ describe '.model_name_to_graphql_name' do
+ it 'returns a graphql name for the given model name' do
+ expect(described_class.model_name_to_graphql_name('DesignManagement::Design')).to eq('DesignManagementDesignID')
+ end
+ end
end
diff --git a/spec/graphql/types/label_type_spec.rb b/spec/graphql/types/label_type_spec.rb
index 475b2a2ad34..427b5d2dcef 100644
--- a/spec/graphql/types/label_type_spec.rb
+++ b/spec/graphql/types/label_type_spec.rb
@@ -11,7 +11,6 @@ RSpec.describe GitlabSchema.types['Label'] do
:color,
:text_color,
:created_at,
- :remove_on_close,
:updated_at
]
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index fa33b32c6c8..875a16a79e5 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['MergeRequest'] do
+ include GraphqlHelpers
+
specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
specify { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
@@ -19,15 +21,17 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
target_branch work_in_progress draft merge_when_pipeline_succeeds diff_head_sha
merge_commit_sha user_notes_count user_discussions_count should_remove_source_branch
diff_refs diff_stats diff_stats_summary
- force_remove_source_branch merge_status in_progress_merge_commit_sha
+ force_remove_source_branch
+ merge_status merge_status_enum
+ in_progress_merge_commit_sha
merge_error allow_collaboration should_be_rebased rebase_commit_sha
rebase_in_progress default_merge_commit_message
merge_ongoing mergeable_discussions_state web_url
source_branch_exists target_branch_exists diverged_from_target_branch
upvotes downvotes head_pipeline pipelines task_completion_status
milestone assignees reviewers participants subscribed labels discussion_locked time_estimate
- total_time_spent reference author merged_at commit_count current_user_todos
- conflicts auto_merge_enabled approved_by source_branch_protected
+ total_time_spent human_time_estimate human_total_time_spent reference author merged_at
+ commit_count current_user_todos conflicts auto_merge_enabled approved_by source_branch_protected
default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies
has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message
auto_merge_strategy merge_user
@@ -106,4 +110,27 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
execute_query
end
end
+
+ describe 'merge_status_enum' do
+ let(:type) { GitlabSchema.types['MergeStatus'] }
+
+ it 'has the type MergeStatus' do
+ expect(described_class.fields['mergeStatusEnum']).to have_graphql_type(type)
+ end
+
+ let_it_be(:project) { create(:project, :public) }
+
+ %i[preparing unchecked cannot_be_merged_recheck checking cannot_be_merged_rechecking can_be_merged cannot_be_merged].each do |state|
+ context "when the the DB value is #{state}" do
+ let(:merge_request) { create(:merge_request, :unique_branches, source_project: project, merge_status: state.to_s) }
+
+ it 'serializes correctly' do
+ value = resolve_field(:merge_status_enum, merge_request)
+ value = type.coerce_isolated_result(value)
+
+ expect(value).to eq(merge_request.public_merge_status.upcase)
+ end
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/mutation_type_spec.rb b/spec/graphql/types/mutation_type_spec.rb
index e4144e4fa97..c1a5c93c85b 100644
--- a/spec/graphql/types/mutation_type_spec.rb
+++ b/spec/graphql/types/mutation_type_spec.rb
@@ -15,28 +15,6 @@ RSpec.describe Types::MutationType do
expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft)
end
- describe 'deprecated and aliased mutations' do
- using RSpec::Parameterized::TableSyntax
-
- where(:alias_name, :canonical_name) do
- 'AddAwardEmoji' | 'AwardEmojiAdd'
- 'RemoveAwardEmoji' | 'AwardEmojiRemove'
- 'ToggleAwardEmoji' | 'AwardEmojiToggle'
- end
-
- with_them do
- let(:alias_field) { get_field(alias_name) }
- let(:canonical_field) { get_field(canonical_name) }
-
- it { expect(alias_field).to be_present }
- it { expect(canonical_field).to be_present }
- it { expect(alias_field.deprecation_reason).to be_present }
- it { expect(canonical_field.deprecation_reason).not_to be_present }
- it { expect(alias_field.resolver.fields).to eq(canonical_field.resolver.fields) }
- it { expect(alias_field.resolver.arguments).to eq(canonical_field.resolver.arguments) }
- end
- end
-
def get_field(name)
described_class.fields[GraphqlHelpers.fieldnamerize(name)]
end
diff --git a/spec/graphql/types/packages/package_group_sort_enum_spec.rb b/spec/graphql/types/packages/package_group_sort_enum_spec.rb
new file mode 100644
index 00000000000..f2ed8f66fb3
--- /dev/null
+++ b/spec/graphql/types/packages/package_group_sort_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageGroupSort'] do
+ it 'exposes all package group sort values' do
+ expect(described_class.values.keys).to contain_exactly(*%w[CREATED_DESC CREATED_ASC NAME_DESC NAME_ASC PROJECT_PATH_DESC PROJECT_PATH_ASC VERSION_DESC VERSION_ASC TYPE_DESC TYPE_ASC])
+ end
+end
diff --git a/spec/graphql/types/packages/package_sort_enum_spec.rb b/spec/graphql/types/packages/package_sort_enum_spec.rb
new file mode 100644
index 00000000000..fe9ce120c73
--- /dev/null
+++ b/spec/graphql/types/packages/package_sort_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageSort'] do
+ it 'exposes all package sort values' do
+ expect(described_class.values.keys).to contain_exactly(*%w[CREATED_DESC CREATED_ASC NAME_DESC NAME_ASC VERSION_DESC VERSION_ASC TYPE_DESC TYPE_ASC])
+ end
+end
diff --git a/spec/graphql/types/packages/pypi/metadatum_type_spec.rb b/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
new file mode 100644
index 00000000000..16fb3ef2098
--- /dev/null
+++ b/spec/graphql/types/packages/pypi/metadatum_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PypiMetadata'] do
+ it 'includes pypi metadatum fields' do
+ expected_fields = %w[
+ id required_python
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/projects/service_type_spec.rb b/spec/graphql/types/projects/service_type_spec.rb
index cca7c49e132..567bdfaec24 100644
--- a/spec/graphql/types/projects/service_type_spec.rb
+++ b/spec/graphql/types/projects/service_type_spec.rb
@@ -9,8 +9,8 @@ RSpec.describe Types::Projects::ServiceType do
it 'resolves the corresponding type for objects' do
expect(described_class.resolve_type(build(:jira_service), {})).to eq(Types::Projects::Services::JiraServiceType)
expect(described_class.resolve_type(build(:service), {})).to eq(Types::Projects::Services::BaseServiceType)
- expect(described_class.resolve_type(build(:drone_ci_service), {})).to eq(Types::Projects::Services::BaseServiceType)
- expect(described_class.resolve_type(build(:custom_issue_tracker_service), {})).to eq(Types::Projects::Services::BaseServiceType)
+ expect(described_class.resolve_type(build(:drone_ci_integration), {})).to eq(Types::Projects::Services::BaseServiceType)
+ expect(described_class.resolve_type(build(:custom_issue_tracker_integration), {})).to eq(Types::Projects::Services::BaseServiceType)
end
end
end
diff --git a/spec/graphql/types/projects/services_enum_spec.rb b/spec/graphql/types/projects/services_enum_spec.rb
index c23c652a378..39c2dcd07f6 100644
--- a/spec/graphql/types/projects/services_enum_spec.rb
+++ b/spec/graphql/types/projects/services_enum_spec.rb
@@ -3,13 +3,11 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ServiceType'] do
- specify { expect(described_class.graphql_name).to eq('ServiceType') }
-
it 'exposes all the existing project services' do
expect(described_class.values.keys).to match_array(available_services_enum)
end
-end
-def available_services_enum
- ::Integration.available_services_types(include_dev: false).map(&:underscore).map(&:upcase)
+ def available_services_enum
+ ::Integration.available_services_types(include_dev: false).map(&:underscore).map(&:upcase)
+ end
end
diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb
index b87770ebe8d..f284d88180c 100644
--- a/spec/graphql/types/snippet_type_spec.rb
+++ b/spec/graphql/types/snippet_type_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do
:visibility_level, :created_at, :updated_at,
:web_url, :raw_url, :ssh_url_to_repo, :http_url_to_repo,
:notes, :discussions, :user_permissions,
- :description_html, :blob, :blobs]
+ :description_html, :blobs]
expect(described_class).to have_graphql_fields(*expected_fields)
end
@@ -133,32 +133,6 @@ RSpec.describe GitlabSchema.types['Snippet'] do
end
end
- describe '#blob' do
- let(:query_blob) { subject.dig('data', 'snippets', 'nodes')[0]['blob'] }
-
- subject { GitlabSchema.execute(snippet_query_for(field: 'blob'), context: { current_user: user }).as_json }
-
- context 'when snippet has repository' do
- let!(:snippet) { create(:personal_snippet, :repository, :public, author: user) }
- let(:blob) { snippet.blobs.first }
-
- it 'returns the first blob from the repository' do
- expect(query_blob['name']).to eq blob.name
- expect(query_blob['path']).to eq blob.path
- end
- end
-
- context 'when snippet does not have a repository' do
- let!(:snippet) { create(:personal_snippet, :public, author: user) }
- let(:blob) { snippet.blob }
-
- it 'returns SnippetBlob type' do
- expect(query_blob['name']).to eq blob.name
- expect(query_blob['path']).to eq blob.path
- end
- end
- end
-
describe '#blobs' do
let_it_be(:snippet) { create(:personal_snippet, :public, author: user) }
diff --git a/spec/graphql/types/snippets/blob_viewer_type_spec.rb b/spec/graphql/types/snippets/blob_viewer_type_spec.rb
index 295df992c67..c3b98236f2b 100644
--- a/spec/graphql/types/snippets/blob_viewer_type_spec.rb
+++ b/spec/graphql/types/snippets/blob_viewer_type_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe GitlabSchema.types['SnippetBlobViewer'] do
end
it 'returns false' do
- snippet_blob = subject.dig('data', 'snippets', 'edges')[0].dig('node', 'blob')
+ snippet_blob = subject.dig('data', 'snippets', 'edges').first.dig('node', 'blobs', 'nodes').find { |b| b['path'] == blob.path }
expect(snippet_blob['path']).to eq blob.path
expect(blob_attribute).to be_nil
@@ -47,10 +47,12 @@ RSpec.describe GitlabSchema.types['SnippetBlobViewer'] do
snippets(ids: "#{snippet.to_global_id}") {
edges {
node {
- blob {
- path
- simpleViewer {
- collapsed
+ blobs {
+ nodes {
+ path
+ simpleViewer {
+ collapsed
+ }
}
}
}
@@ -73,10 +75,12 @@ RSpec.describe GitlabSchema.types['SnippetBlobViewer'] do
snippets(ids: "#{snippet.to_global_id}") {
edges {
node {
- blob {
- path
- simpleViewer {
- tooLarge
+ blobs {
+ nodes {
+ path
+ simpleViewer {
+ tooLarge
+ }
}
}
}
diff --git a/spec/graphql/types/timelog_type_spec.rb b/spec/graphql/types/timelog_type_spec.rb
index 791c2fdb046..1344af89fb6 100644
--- a/spec/graphql/types/timelog_type_spec.rb
+++ b/spec/graphql/types/timelog_type_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Timelog'] do
it { expect(described_class.graphql_name).to eq('Timelog') }
it { expect(described_class).to have_graphql_fields(fields) }
- it { expect(described_class).to require_graphql_authorizations(:read_group_timelogs) }
+ it { expect(described_class).to require_graphql_authorizations(:read_issue) }
describe 'user field' do
subject { described_class.fields['user'] }
diff --git a/spec/helpers/admin/background_migrations_helper_spec.rb b/spec/helpers/admin/background_migrations_helper_spec.rb
new file mode 100644
index 00000000000..8880a00755b
--- /dev/null
+++ b/spec/helpers/admin/background_migrations_helper_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Admin::BackgroundMigrationsHelper do
+ describe '#batched_migration_status_badge_class_name' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :class_name) do
+ :active | 'badge-info'
+ :paused | 'badge-warning'
+ :failed | 'badge-danger'
+ :finished | 'badge-success'
+ end
+
+ subject { helper.batched_migration_status_badge_class_name(migration) }
+
+ with_them do
+ let(:migration) { build(:batched_background_migration, status: status) }
+
+ it { is_expected.to eq(class_name) }
+ end
+ end
+
+ describe '#batched_migration_progress' do
+ subject { helper.batched_migration_progress(migration, completed_rows) }
+
+ let(:migration) { build(:batched_background_migration, status: :active, total_tuple_count: 100) }
+ let(:completed_rows) { 25 }
+
+ it 'returns completion percentage' do
+ expect(subject).to eq(25)
+ end
+
+ context 'when migration is finished' do
+ let(:migration) { build(:batched_background_migration, status: :finished, total_tuple_count: nil) }
+
+ it 'returns 100 percent' do
+ expect(subject).to eq(100)
+ end
+ end
+
+ context 'when total_tuple_count is nil' do
+ let(:migration) { build(:batched_background_migration, status: :active, total_tuple_count: nil) }
+
+ it 'returns nil' do
+ expect(subject).to eq(nil)
+ end
+
+ context 'when there are no completed rows' do
+ let(:completed_rows) { 0 }
+
+ it 'returns 0 percent' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+
+ context 'when completed rows are greater than total count' do
+ let(:completed_rows) { 150 }
+
+ it 'returns 99 percent' do
+ expect(subject).to eq(99)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index c74ee3ce0ec..4c62b3e12c1 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -37,8 +37,24 @@ RSpec.describe ApplicationSettingsHelper do
it_behaves_like 'when HTTP protocol is in use', 'https'
it_behaves_like 'when HTTP protocol is in use', 'http'
- context 'with tracking parameters' do
- it { expect(visible_attributes).to include(*%i(snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id)) }
+ describe '.visible_attributes' do
+ it 'contains tracking parameters' do
+ expect(helper.visible_attributes).to include(*%i(snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id))
+ end
+
+ it 'contains :deactivate_dormant_users' do
+ expect(helper.visible_attributes).to include(:deactivate_dormant_users)
+ end
+
+ context 'when GitLab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'does not contain :deactivate_dormant_users' do
+ expect(helper.visible_attributes).not_to include(:deactivate_dormant_users)
+ end
+ end
end
describe '.integration_expanded?' do
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 9a1ecb22edb..4e94636ba45 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -205,16 +205,6 @@ RSpec.describe CommitsHelper do
{ id: forked_project.id.to_s, name: forked_project.full_path, refsUrl: refs_project_path(forked_project) }
])
end
-
- context 'pick_into_project is disabled' do
- before do
- stub_feature_flags(pick_into_project: false)
- end
-
- it 'does not calculate target projects' do
- expect(helper.cherry_pick_projects_data(project)).to eq([])
- end
- end
end
describe "#commit_options_dropdown_data" do
@@ -298,7 +288,7 @@ RSpec.describe CommitsHelper do
let(:pipeline) { create(:ci_pipeline, :running) }
let(:user) { create(:user) }
let(:ref) { "master" }
- let(:merge_request) { nil }
+ let(:merge_request) { create(:merge_request) }
let(:request) { double(xhr?: true) }
let(:current_path) { "test" }
@@ -315,8 +305,8 @@ RSpec.describe CommitsHelper do
it do
is_expected.to include(
{
- merge_request: merge_request,
- pipeline_status: commit_status,
+ merge_request: merge_request.cache_key,
+ pipeline_status: pipeline.cache_key,
xhr: true,
controller: "commits",
path: current_path
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 89cb0f72277..96869fcc777 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -34,7 +34,6 @@ RSpec.describe EnvironmentsHelper do
'project_path' => project_path(project),
'tags_path' => project_tags_path(project),
'has_metrics' => "#{environment.has_metrics?}",
- 'prometheus_status' => "#{environment.prometheus_status}",
'external_dashboard_url' => nil,
'environment_state' => environment.state,
'custom_metrics_path' => project_prometheus_metrics_path(project),
@@ -158,7 +157,7 @@ RSpec.describe EnvironmentsHelper do
let(:cluster) { create(:cluster, projects: [project]) }
it 'has managed prometheus' do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
expect(metrics_data).to include(
'has_managed_prometheus' => 'true'
@@ -193,7 +192,7 @@ RSpec.describe EnvironmentsHelper do
"environment_name": environment.name,
"environments_path": api_v4_projects_environments_path(id: project.id),
"environment_id": environment.id,
- "cluster_applications_documentation_path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
+ "cluster_applications_documentation_path" => help_page_path('user/clusters/integrations.md', anchor: 'elastic-stack-cluster-integration'),
"clusters_path": project_clusters_path(project, format: :json)
}
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 264bad92d56..073cebeecd5 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -88,6 +88,13 @@ RSpec.describe EventsHelper do
expect(helper.event_feed_url(event)).to eq(push_event_feed_url(event))
end
+
+ it 'returns nil for push event with multiple refs' do
+ event = create(:push_event)
+ create(:push_event_payload, event: event, ref_count: 2, ref: nil, ref_type: :tag, commit_count: 0, action: :pushed)
+
+ expect(helper.event_feed_url(event)).to eq(nil)
+ end
end
describe '#event_preposition' do
diff --git a/spec/helpers/gitlab_script_tag_helper_spec.rb b/spec/helpers/gitlab_script_tag_helper_spec.rb
index 37413b9b1c2..35f2c0795be 100644
--- a/spec/helpers/gitlab_script_tag_helper_spec.rb
+++ b/spec/helpers/gitlab_script_tag_helper_spec.rb
@@ -41,4 +41,11 @@ RSpec.describe GitlabScriptTagHelper do
expect(helper.javascript_tag( '// ignored', type: 'application/javascript') { 'alert(1)' }.to_s).to eq tag_with_nonce_and_type
end
end
+
+ describe '#preload_link_tag' do
+ it 'returns a link tag with a nonce' do
+ expect(helper.preload_link_tag('https://example.com/script.js').to_s)
+ .to eq "<link rel=\"preload\" href=\"https://example.com/script.js\" as=\"script\" type=\"text/javascript\" nonce=\"noncevalue\">"
+ end
+ end
end
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index c3f1509fbc8..b409bebaac3 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -23,58 +23,79 @@ RSpec.describe Groups::GroupMembersHelper do
end
end
- describe '#group_members_list_data_json' do
- let(:group_members) { create_list(:group_member, 2, group: group, created_by: current_user) }
-
- let(:pagination) { {} }
- let(:collection) { group_members }
- let(:presented_members) { present_members(collection) }
+ describe '#group_members_app_data_json' do
+ include_context 'group_group_link'
- subject { Gitlab::Json.parse(helper.group_members_list_data_json(group, presented_members, pagination)) }
+ let(:members) { create_list(:group_member, 2, group: shared_group, created_by: current_user) }
+ let(:invited) { create_list(:group_member, 2, :invited, group: shared_group, created_by: current_user) }
+ let!(:access_requests) { create_list(:group_member, 2, :access_request, group: shared_group, created_by: current_user) }
+
+ let(:members_collection) { members }
+
+ subject do
+ Gitlab::Json.parse(
+ helper.group_members_app_data_json(
+ shared_group,
+ members: present_members(members_collection),
+ invited: present_members(invited),
+ access_requests: present_members(access_requests)
+ )
+ )
+ end
- shared_examples 'members.json' do
+ shared_examples 'members.json' do |member_type|
it 'returns `members` property that matches json schema' do
- expect(subject['members'].to_json).to match_schema('members')
+ expect(subject[member_type]['members'].to_json).to match_schema('members')
+ end
+
+ it 'sets `member_path` property' do
+ expect(subject[member_type]['member_path']).to eq('/groups/foo-bar/-/group_members/:id')
end
end
before do
- allow(helper).to receive(:group_group_member_path).with(group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
- allow(helper).to receive(:can?).with(current_user, :admin_group_member, group).and_return(true)
+ allow(helper).to receive(:group_group_member_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
+ allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
+ allow(helper).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
end
it 'returns expected json' do
expected = {
- member_path: '/groups/foo-bar/-/group_members/:id',
- source_id: group.id,
+ source_id: shared_group.id,
can_manage_members: true
}.as_json
expect(subject).to include(expected)
end
- context 'for a group member' do
- it_behaves_like 'members.json'
+ context 'group members' do
+ it_behaves_like 'members.json', 'user'
context 'with user status set' do
let(:user) { create(:user) }
let!(:status) { create(:user_status, user: user) }
- let(:group_members) { [create(:group_member, group: group, user: user, created_by: current_user)] }
+ let(:members) { [create(:group_member, group: shared_group, user: user, created_by: current_user)] }
- it_behaves_like 'members.json'
+ it_behaves_like 'members.json', 'user'
end
end
- context 'for an invited group member' do
- let(:group_members) { create_list(:group_member, 2, :invited, group: group, created_by: current_user) }
+ context 'invited group members' do
+ it_behaves_like 'members.json', 'invite'
+ end
- it_behaves_like 'members.json'
+ context 'access requests' do
+ it_behaves_like 'members.json', 'access_request'
end
- context 'for an access request' do
- let(:group_members) { create_list(:group_member, 2, :access_request, group: group, created_by: current_user) }
+ context 'group links' do
+ it 'sets `group.members` property that matches json schema' do
+ expect(subject['group']['members'].to_json).to match_schema('group_link/group_group_links')
+ end
- it_behaves_like 'members.json'
+ it 'sets `member_path` property' do
+ expect(subject['group']['member_path']).to eq('/groups/foo-bar/-/group_links/:id')
+ end
end
context 'when pagination is not available' do
@@ -87,13 +108,12 @@ RSpec.describe Groups::GroupMembersHelper do
params: {}
}.as_json
- expect(subject['pagination']).to include(expected)
+ expect(subject['access_request']['pagination']).to include(expected)
end
end
context 'when pagination is available' do
- let(:collection) { Kaminari.paginate_array(group_members).page(1).per(1) }
- let(:pagination) { { param_name: :page, params: { search_groups: nil } } }
+ let(:members_collection) { Kaminari.paginate_array(members).page(1).per(1) }
it 'sets `pagination` attribute to expected json' do
expected = {
@@ -101,41 +121,11 @@ RSpec.describe Groups::GroupMembersHelper do
per_page: 1,
total_items: 2,
param_name: :page,
- params: { search_groups: nil }
+ params: { invited_members_page: nil, search_invited: nil }
}.as_json
- expect(subject['pagination']).to include(expected)
+ expect(subject['user']['pagination']).to include(expected)
end
end
end
-
- describe '#group_group_links_list_data_json' do
- include_context 'group_group_link'
-
- subject { Gitlab::Json.parse(helper.group_group_links_list_data_json(shared_group)) }
-
- before do
- allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
- end
-
- it 'returns expected json' do
- expected = {
- pagination: {
- current_page: nil,
- per_page: nil,
- total_items: 1,
- param_name: nil,
- params: {}
- },
- member_path: '/groups/foo-bar/-/group_links/:id',
- source_id: shared_group.id
- }.as_json
-
- expect(subject).to include(expected)
- end
-
- it 'returns `members` property that matches json schema' do
- expect(subject['members'].to_json).to match_schema('group_link/group_group_links')
- end
- end
end
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index 122f2339b28..3d2adaa5b5d 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -114,69 +114,4 @@ RSpec.describe InviteMembersHelper do
end
end
end
-
- describe '#dropdown_invite_members_link' do
- shared_examples_for 'dropdown invite members link' do
- let(:link_regex) do
- /data-track-event="click_link".*data-track-property="_track_property_".*Invite members/
- end
-
- before do
- allow(helper).to receive(:experiment_tracking_category_and_group) { '_track_property_' }
- allow(helper).to receive(:current_user) { owner }
- end
-
- it 'records the experiment' do
- allow(helper).to receive(:experiment_enabled?)
-
- helper.dropdown_invite_members_link(form_model)
-
- expect(helper).to have_received(:experiment_tracking_category_and_group).with(:invite_members_new_dropdown)
- end
-
- context 'with experiment enabled' do
- before do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_new_dropdown) { true }
- end
-
- it 'returns link' do
- link = helper.dropdown_invite_members_link(form_model)
-
- expect(link).to match(link_regex)
- expect(link).to include(link_href)
- expect(link).to include('gl-emoji')
- end
- end
-
- context 'with no experiment enabled' do
- before do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_new_dropdown) { false }
- end
-
- it 'returns link' do
- link = helper.dropdown_invite_members_link(form_model)
-
- expect(link).to match(link_regex)
- expect(link).to include(link_href)
- expect(link).not_to include('gl-emoji')
- end
- end
- end
-
- context 'with a project' do
- let_it_be(:form_model) { project }
-
- let(:link_href) { "href=\"#{project_project_members_path(form_model)}\"" }
-
- it_behaves_like 'dropdown invite members link'
- end
-
- context 'with a group' do
- let_it_be(:form_model) { create(:group) }
-
- let(:link_href) { "href=\"#{group_group_members_path(form_model)}\"" }
-
- it_behaves_like 'dropdown invite members link'
- end
- end
end
diff --git a/spec/helpers/issuables_description_templates_helper_spec.rb b/spec/helpers/issuables_description_templates_helper_spec.rb
index e8961ccb535..95460174266 100644
--- a/spec/helpers/issuables_description_templates_helper_spec.rb
+++ b/spec/helpers/issuables_description_templates_helper_spec.rb
@@ -13,24 +13,8 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
let_it_be(:group_member) { create(:group_member, :developer, group: parent_group, user: user) }
let_it_be(:project_member) { create(:project_member, :developer, user: user, project: project) }
- context 'when feature flag disabled' do
- before do
- stub_feature_flags(inherited_issuable_templates: false)
- end
-
- it 'returns empty array when template type does not exist' do
- expect(helper.issuable_templates(project, 'non-existent-template-type')).to eq([])
- end
- end
-
- context 'when feature flag enabled' do
- before do
- stub_feature_flags(inherited_issuable_templates: true)
- end
-
- it 'returns empty hash when template type does not exist' do
- expect(helper.issuable_templates(build(:project), 'non-existent-template-type')).to eq({})
- end
+ it 'returns empty hash when template type does not exist' do
+ expect(helper.issuable_templates(build(:project), 'non-existent-template-type')).to eq({})
end
context 'with cached issuable templates' do
@@ -81,16 +65,18 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
allow(helper).to receive(:issuable_templates).and_return(templates)
end
- context 'when feature flag disabled' do
+ context 'with matching project templates' do
let(:templates) do
- [
- { name: "another_issue_template", id: "another_issue_template", project_id: project.id },
- { name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
- ]
- end
-
- before do
- stub_feature_flags(inherited_issuable_templates: false)
+ {
+ "" => [
+ { name: "another_issue_template", id: "another_issue_template", project_id: project.id },
+ { name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
+ ],
+ "Instance" => [
+ { name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
+ { name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
+ ]
+ }
end
it 'returns project templates only' do
@@ -98,47 +84,22 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
end
- context 'when feature flag enabled' do
- before do
- stub_feature_flags(inherited_issuable_templates: true)
- end
-
- context 'with matching project templates' do
- let(:templates) do
- {
- "" => [
- { name: "another_issue_template", id: "another_issue_template", project_id: project.id },
- { name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
- ],
- "Instance" => [
- { name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
- { name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
- ]
- }
- end
-
- it 'returns project templates only' do
- expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
- end
+ context 'without matching project templates' do
+ let(:templates) do
+ {
+ "Project Templates" => [
+ { name: "another_issue_template", id: "another_issue_template", project_id: non_existing_record_id },
+ { name: "custom_issue_template", id: "custom_issue_template", project_id: non_existing_record_id }
+ ],
+ "Instance" => [
+ { name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
+ { name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
+ ]
+ }
end
- context 'without matching project templates' do
- let(:templates) do
- {
- "Project Templates" => [
- { name: "another_issue_template", id: "another_issue_template", project_id: non_existing_record_id },
- { name: "custom_issue_template", id: "custom_issue_template", project_id: non_existing_record_id }
- ],
- "Instance" => [
- { name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
- { name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
- ]
- }
- end
-
- it 'returns empty array' do
- expect(helper.issuable_templates_names(Issue.new)).to eq([])
- end
+ it 'returns empty array' do
+ expect(helper.issuable_templates_names(Issue.new)).to eq([])
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index b0338d80ee7..ecaee03eeea 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -202,6 +202,20 @@ RSpec.describe IssuablesHelper do
end
end
+ describe '#issuable_project_reference' do
+ it 'display project name and simple reference with `#` to an issue' do
+ issue = build_stubbed(:issue)
+
+ expect(helper.issuable_project_reference(issue)).to eq("#{issue.project.full_name} ##{issue.iid}")
+ end
+
+ it 'display project name and simple reference with `!` to an MR' do
+ merge_request = build_stubbed(:merge_request)
+
+ expect(helper.issuable_project_reference(merge_request)).to eq("#{merge_request.project.full_name} !#{merge_request.iid}")
+ end
+ end
+
describe '#updated_at_by' do
let(:user) { create(:user) }
let(:unedited_issuable) { create(:issue) }
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 17e6c75ca27..59b42dfca20 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -304,15 +304,15 @@ RSpec.describe IssuesHelper do
empty_state_svg_path: '#',
endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project),
- has_issues: project_issues(project).exists?.to_s,
+ has_project_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: '#',
initial_email: project.new_issuable_address(current_user, 'issue'),
is_signed_in: current_user.present?.to_s,
issues_path: project_issues_path(project),
- jira_integration_path: help_page_url('user/project/integrations/jira', anchor: 'view-jira-issues'),
+ jira_integration_path: help_page_url('integration/jira/', anchor: 'view-jira-issues'),
markdown_help_path: help_page_path('user/markdown'),
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
- new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.id, milestone_id: finder.milestones.first.id }),
+ new_issue_path: new_project_issue_path(project, issue: { milestone_id: finder.milestones.first.id }),
project_import_jira_path: project_import_jira_path(project),
project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json),
project_milestones_path: project_milestones_path(project, format: :json),
diff --git a/spec/helpers/keyset_helper_spec.rb b/spec/helpers/keyset_helper_spec.rb
new file mode 100644
index 00000000000..2e4bf537e2f
--- /dev/null
+++ b/spec/helpers/keyset_helper_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe KeysetHelper, type: :controller do
+ controller(Admin::UsersController) do
+ def index
+ @users = User
+ .where(admin: false)
+ .order(id: :desc)
+ .keyset_paginate(cursor: params[:cursor], per_page: 2)
+
+ render inline: "<%= keyset_paginate @users %>", layout: false # rubocop: disable Rails/RenderInline
+ end
+ end
+
+ render_views
+
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ context 'with admin mode', :enable_admin_mode do
+ context 'when no users are present' do
+ it 'does not render pagination links' do
+ get :index
+
+ expect(response.body).not_to include(s_('Pagination|First'))
+ expect(response.body).not_to include(s_('Pagination|Prev'))
+ expect(response.body).not_to include(s_('Pagination|Next'))
+ expect(response.body).not_to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when one user is present' do
+ before do
+ create(:user)
+ end
+
+ it 'does not render pagination links' do
+ get :index
+
+ expect(response.body).not_to include(s_('Pagination|First'))
+ expect(response.body).not_to include(s_('Pagination|Prev'))
+ expect(response.body).not_to include(s_('Pagination|Next'))
+ expect(response.body).not_to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when more users are present' do
+ let_it_be(:users) { create_list(:user, 5) }
+
+ let(:paginator) { User.where(admin: false).order(id: :desc).keyset_paginate(per_page: 2) }
+
+ context 'when on the first page' do
+ it 'renders the next and last links' do
+ get :index
+
+ expect(response.body).not_to include(s_('Pagination|First'))
+ expect(response.body).not_to include(s_('Pagination|Prev'))
+ expect(response.body).to include(s_('Pagination|Next'))
+ expect(response.body).to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when at the last page' do
+ it 'renders the prev and first links' do
+ cursor = paginator.cursor_for_last_page
+
+ get :index, params: { cursor: cursor }
+
+ expect(response.body).to include(s_('Pagination|First'))
+ expect(response.body).to include(s_('Pagination|Prev'))
+ expect(response.body).not_to include(s_('Pagination|Next'))
+ expect(response.body).not_to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when at the second page' do
+ it 'renders all links' do
+ cursor = paginator.cursor_for_next_page
+
+ get :index, params: { cursor: cursor }
+
+ expect(response.body).to include(s_('Pagination|First'))
+ expect(response.body).to include(s_('Pagination|Prev'))
+ expect(response.body).to include(s_('Pagination|Next'))
+ expect(response.body).to include(s_('Pagination|Last'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
new file mode 100644
index 00000000000..dd860ce3180
--- /dev/null
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Nav::NewDropdownHelper do
+ describe '#new_dropdown_view_model' do
+ let_it_be(: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(:with_new_repo_experiment) { :control }
+ let(:with_invite_members_experiment) { false }
+ let(:with_invite_members_experiment_enabled) { 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
+
+ before do
+ stub_experiments(new_repo: with_new_repo_experiment)
+ allow(::Gitlab::Experimentation).to receive(:active?).with(:invite_members_new_dropdown) { with_invite_members_experiment }
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_new_dropdown) { with_invite_members_experiment_enabled }
+ allow(helper).to receive(:tracking_label) { 'test_tracking_label' }
+ allow(helper).to receive(:experiment_tracking_category_and_group) { |x| x }
+
+ allow(helper).to receive(:current_user) { current_user }
+ allow(helper).to receive(:can?) { 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 'new repo experiment shared example' do |title|
+ let(:with_new_repo_experiment) { :candidate }
+
+ it 'has experiment project title' do
+ expect(subject[:menu_sections]).to match(
+ expected_menu_section(
+ title: title,
+ menu_item: a_hash_including(title: 'New project/repository')
+ )
+ )
+ end
+ end
+
+ shared_examples 'invite member link shared example' do
+ it 'shows invite member link' do
+ expect(subject[:menu_sections]).to eq(
+ expected_menu_section(
+ title: expected_title,
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'invite',
+ title: 'Invite members',
+ href: expected_href,
+ data: {
+ track_event: 'click_link',
+ track_label: 'test_tracking_label',
+ track_property: :invite_members_new_dropdown
+ }
+ )
+ )
+ )
+ end
+
+ context 'with experiment enabled' do
+ let(:with_invite_members_experiment_enabled) { true }
+
+ it 'shows emoji with invite member link' do
+ expect(subject[:menu_sections]).to match(
+ expected_menu_section(
+ title: expected_title,
+ menu_item: a_hash_including(
+ emoji: 'shaking_hands'
+ )
+ )
+ )
+ end
+ end
+ end
+
+ it 'has title' do
+ expect(subject[:title]).to eq('New...')
+ end
+
+ context 'when current_user is nil (anonymous)' do
+ let(:current_user) { nil }
+
+ it 'is nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when group and project are nil' do
+ it 'has no menu sections' do
+ expect(subject[: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(
+ expected_menu_section(
+ title: 'GitLab',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'general_new_project',
+ title: 'New project',
+ href: '/projects/new',
+ data: { track_experiment: 'new_repo', track_event: 'click_link_new_project', track_label: 'plus_menu_dropdown', qa_selector: 'global_new_project_link' }
+ )
+ )
+ )
+ end
+
+ it_behaves_like 'new repo experiment shared example', 'GitLab'
+ end
+
+ context 'when can create group' do
+ let(:with_can_create_group) { true }
+
+ it 'has group menu item' do
+ expect(subject[:menu_sections]).to eq(
+ expected_menu_section(
+ title: 'GitLab',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'general_new_group',
+ title: 'New group',
+ href: '/groups/new',
+ data: { track_event: 'click_link_new_group', track_label: 'plus_menu_dropdown' }
+ )
+ )
+ )
+ end
+ end
+
+ context 'when can create snippet' do
+ let(:with_can_create_snippet) { true }
+
+ it 'has new snippet menu item' do
+ expect(subject[:menu_sections]).to eq(
+ expected_menu_section(
+ title: 'GitLab',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'general_new_snippet',
+ title: 'New snippet',
+ href: '/-/snippets/new',
+ data: { track_event: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown', qa_selector: 'global_new_snippet_link' }
+ )
+ )
+ )
+ end
+ end
+ end
+
+ context 'with persisted group' do
+ let_it_be(: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 }
+ end
+
+ it 'has no menu sections' do
+ expect(subject[: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(
+ expected_menu_section(
+ title: 'This group',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'new_project',
+ title: 'New project',
+ href: "/projects/new?namespace_id=#{group.id}",
+ data: { track_experiment: 'new_repo', track_event: 'click_link_new_project_group', track_label: 'plus_menu_dropdown' }
+ )
+ )
+ )
+ end
+
+ it_behaves_like 'new repo experiment shared example', 'This group'
+ end
+
+ context 'when can create subgroup' do
+ let(:with_can_create_subgroup_in_group) { true }
+
+ it 'has new subgroup menu item' do
+ expect(subject[:menu_sections]).to eq(
+ expected_menu_section(
+ title: 'This group',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'new_subgroup',
+ title: 'New subgroup',
+ href: "/groups/new?parent_id=#{group.id}",
+ data: { track_event: 'click_link_new_subgroup', track_label: 'plus_menu_dropdown' }
+ )
+ )
+ )
+ end
+ end
+
+ 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_href) { "/groups/#{group.full_path}/-/group_members" }
+
+ it_behaves_like 'invite member link shared example'
+ end
+ end
+
+ context 'with persisted project' do
+ let_it_be(:project) { build_stubbed(:project) }
+ let_it_be(:merge_project) { build_stubbed(:project) }
+
+ let(:current_project) { project }
+ let(:with_show_new_issue_link) { false }
+ let(:with_merge_project) { nil }
+ let(:with_can_create_snippet_in_project) { false }
+ let(:with_can_import_members) { false }
+
+ before do
+ allow(helper).to receive(:show_new_issue_link?).with(project) { with_show_new_issue_link }
+ allow(helper).to receive(:merge_request_source_project_for_project).with(project) { with_merge_project }
+ allow(helper).to receive(:can?).with(user, :create_snippet, project) { with_can_create_snippet_in_project }
+ allow(helper).to receive(:can_import_members?) { with_can_import_members }
+ end
+
+ it 'has no menu sections' do
+ expect(subject[: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(
+ expected_menu_section(
+ title: 'This project',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'new_issue',
+ title: 'New issue',
+ href: "/#{project.path_with_namespace}/-/issues/new",
+ data: { track_event: 'click_link_new_issue', track_label: 'plus_menu_dropdown', qa_selector: 'new_issue_link' }
+ )
+ )
+ )
+ end
+ end
+
+ context 'with merge project' do
+ let(:with_merge_project) { merge_project }
+
+ it 'shows merge project' do
+ expect(subject[:menu_sections]).to eq(
+ expected_menu_section(
+ title: '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_event: 'click_link_new_mr', track_label: 'plus_menu_dropdown' }
+ )
+ )
+ )
+ end
+ end
+
+ context 'when can create snippet' do
+ let(:with_can_create_snippet_in_project) { true }
+
+ it 'shows new snippet' do
+ expect(subject[:menu_sections]).to eq(
+ expected_menu_section(
+ title: 'This project',
+ menu_item: ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'new_snippet',
+ title: 'New snippet',
+ href: "/#{project.path_with_namespace}/-/snippets/new",
+ data: { track_event: 'click_link_new_snippet_project', track_label: 'plus_menu_dropdown' }
+ )
+ )
+ )
+ end
+ end
+
+ context 'when invite members experiment' do
+ let(:with_invite_members_experiment) { true }
+ let(:with_can_import_members) { true }
+ let(:expected_title) { 'This project' }
+ let(:expected_href) { "/#{project.path_with_namespace}/-/project_members" }
+
+ it_behaves_like 'invite member link shared example'
+ end
+ end
+ end
+end
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index 5c9e1e82b01..d87c751c62f 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -5,11 +5,17 @@ require 'spec_helper'
RSpec.describe Nav::TopNavHelper do
include ActionView::Helpers::UrlHelper
- describe '#top_nav_view_model' do
- let_it_be(:user) { build_stubbed(:user) }
- let_it_be(:admin) { build_stubbed(:user, :admin) }
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:admin) { build_stubbed(:user, :admin) }
+ let_it_be(:external_user) { build_stubbed(:user, :external, can_create_group: false) }
+
+ let(:current_user) { nil }
+
+ before do
+ allow(helper).to receive(:current_user) { current_user }
+ end
- let(:current_user) { nil }
+ describe '#top_nav_view_model' do
let(:current_project) { nil }
let(:current_group) { nil }
let(:with_current_settings_admin_mode) { false }
@@ -26,7 +32,6 @@ RSpec.describe Nav::TopNavHelper do
let(:active_title) { 'Menu' }
before do
- allow(helper).to receive(:current_user) { current_user }
allow(Gitlab::CurrentSettings).to receive(:admin_mode) { with_current_settings_admin_mode }
allow(helper).to receive(:header_link?).with(:admin_mode) { with_header_link_admin_mode }
allow(Gitlab::Sherlock).to receive(:enabled?) { with_sherlock_enabled }
@@ -48,30 +53,73 @@ RSpec.describe Nav::TopNavHelper do
context 'when current_user is nil (anonymous)' do
it 'has expected :primary' do
- expected_projects_item = ::Gitlab::Nav::TopNavMenuItem.build(
- href: '/explore',
- icon: 'project',
- id: 'project',
- title: 'Projects'
- )
- expected_groups_item = ::Gitlab::Nav::TopNavMenuItem.build(
- href: '/explore/groups',
- icon: 'group',
- id: 'groups',
- title: 'Groups'
- )
- expected_snippets_item = ::Gitlab::Nav::TopNavMenuItem.build(
- href: '/explore/snippets',
- icon: 'snippet',
- id: 'snippets',
- title: 'Snippets'
- )
- expect(subject[:primary])
- .to eq([
- expected_projects_item,
- expected_groups_item,
- expected_snippets_item
- ])
+ expected_primary = [
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore',
+ icon: 'project',
+ id: 'project',
+ title: 'Projects'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore/groups',
+ icon: 'group',
+ id: 'groups',
+ title: 'Groups'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore/snippets',
+ icon: 'snippet',
+ id: 'snippets',
+ title: 'Snippets'
+ )
+ ]
+ expect(subject[:primary]).to eq(expected_primary)
+ end
+
+ it 'has expected :shortcuts' do
+ expected_shortcuts = [
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore',
+ id: 'project-shortcut',
+ title: 'Projects',
+ css_class: 'dashboard-shortcuts-projects'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore/groups',
+ id: 'groups-shortcut',
+ title: 'Groups',
+ css_class: 'dashboard-shortcuts-groups'
+ ),
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/explore/snippets',
+ id: 'snippets-shortcut',
+ title: 'Snippets',
+ css_class: 'dashboard-shortcuts-snippets'
+ )
+ ]
+ expect(subject[:shortcuts]).to eq(expected_shortcuts)
+ end
+
+ it 'has expected :secondary' do
+ expected_secondary = [
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ href: '/help',
+ id: 'help',
+ title: 'Help',
+ icon: 'question-o'
+ )
+ ]
+ expect(subject[:secondary]).to eq(expected_secondary)
+ end
+
+ context 'with current nav as project' do
+ before do
+ helper.nav('project')
+ end
+
+ it 'has expected :active' do
+ expect(subject[:primary].detect { |entry| entry[:id] == 'project' }[:active]).to eq(true)
+ end
end
end
@@ -82,6 +130,7 @@ RSpec.describe Nav::TopNavHelper do
expect(subject).to eq({ activeTitle: active_title,
primary: [],
secondary: [],
+ shortcuts: [],
views: {} })
end
@@ -105,6 +154,16 @@ RSpec.describe Nav::TopNavHelper do
expect(subject[:primary]).to eq([expected_primary])
end
+ it 'has expected :shortcuts' do
+ expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'project-shortcut',
+ title: 'Projects',
+ href: '/dashboard/projects',
+ css_class: 'dashboard-shortcuts-projects'
+ )
+ expect(subject[:shortcuts]).to eq([expected_shortcuts])
+ end
+
context 'projects' do
it 'has expected :currentUserName' do
expect(projects_view[:currentUserName]).to eq(current_user.username)
@@ -146,6 +205,16 @@ RSpec.describe Nav::TopNavHelper do
expect(projects_view[:linksSecondary]).to eq(expected_links_secondary)
end
+ context 'with current nav as project' do
+ before do
+ helper.nav('project')
+ end
+
+ it 'has expected :active' do
+ expect(subject[:primary].detect { |entry| entry[:id] == 'project' }[:active]).to eq(true)
+ end
+ end
+
context 'with persisted project' do
let_it_be(:project) { build_stubbed(:project) }
@@ -191,6 +260,16 @@ RSpec.describe Nav::TopNavHelper do
expect(subject[:primary]).to eq([expected_primary])
end
+ it 'has expected :shortcuts' do
+ expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'groups-shortcut',
+ title: 'Groups',
+ href: '/dashboard/groups',
+ css_class: 'dashboard-shortcuts-groups'
+ )
+ expect(subject[:shortcuts]).to eq([expected_shortcuts])
+ end
+
context 'groups' do
it 'has expected :currentUserName' do
expect(groups_view[:currentUserName]).to eq(current_user.username)
@@ -219,7 +298,7 @@ RSpec.describe Nav::TopNavHelper do
it 'has expected :linksSecondary' do
expected_links_secondary = [
::Gitlab::Nav::TopNavMenuItem.build(
- href: '/groups/new#create-group-pane',
+ href: '/groups/new',
id: 'create',
title: 'Create group'
)
@@ -227,6 +306,24 @@ RSpec.describe Nav::TopNavHelper do
expect(groups_view[:linksSecondary]).to eq(expected_links_secondary)
end
+ context 'with external user' do
+ let(:current_user) { external_user }
+
+ it 'does not have create group link' do
+ expect(groups_view[:linksSecondary]).to eq([])
+ end
+ end
+
+ context 'with current nav as group' do
+ before do
+ helper.nav('group')
+ end
+
+ it 'has expected :active' do
+ expect(subject[:primary].detect { |entry| entry[:id] == 'groups' }[:active]).to eq(true)
+ end
+ end
+
context 'with persisted group' do
let_it_be(:group) { build_stubbed(:group) }
@@ -268,6 +365,16 @@ RSpec.describe Nav::TopNavHelper do
)
expect(subject[:primary]).to eq([expected_primary])
end
+
+ it 'has expected :shortcuts' do
+ expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'milestones-shortcut',
+ title: 'Milestones',
+ href: '/dashboard/milestones',
+ css_class: 'dashboard-shortcuts-milestones'
+ )
+ expect(subject[:shortcuts]).to eq([expected_shortcuts])
+ end
end
context 'with snippets' do
@@ -285,6 +392,16 @@ RSpec.describe Nav::TopNavHelper do
)
expect(subject[:primary]).to eq([expected_primary])
end
+
+ it 'has expected :shortcuts' do
+ expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'snippets-shortcut',
+ title: 'Snippets',
+ href: '/dashboard/snippets',
+ css_class: 'dashboard-shortcuts-snippets'
+ )
+ expect(subject[:shortcuts]).to eq([expected_shortcuts])
+ end
end
context 'with activity' do
@@ -302,6 +419,16 @@ RSpec.describe Nav::TopNavHelper do
)
expect(subject[:primary]).to eq([expected_primary])
end
+
+ it 'has expected :shortcuts' do
+ expected_shortcuts = ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'activity-shortcut',
+ title: 'Activity',
+ href: '/dashboard/activity',
+ css_class: 'dashboard-shortcuts-activity'
+ )
+ expect(subject[:shortcuts]).to eq([expected_shortcuts])
+ end
end
context 'when sherlock is enabled' do
@@ -352,7 +479,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Leave Admin Mode',
icon: 'lock-open',
href: '/admin/session/destroy',
- method: :post
+ data: { method: 'post' }
)
expect(subject[:secondary].last).to eq(expected_leave_admin_mode_item)
end
@@ -373,4 +500,50 @@ RSpec.describe Nav::TopNavHelper do
end
end
end
+
+ describe '#top_nav_responsive_view_model' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+
+ let(:with_search) { false }
+ let(:with_new_view_model) { nil }
+
+ let(:subject) { helper.top_nav_responsive_view_model(project: project, group: group) }
+
+ before do
+ allow(helper).to receive(:header_link?).with(:search) { with_search }
+ allow(helper).to receive(:new_dropdown_view_model).with(project: project, group: group) { with_new_view_model }
+ end
+
+ it 'has nil new subview' do
+ expect(subject[:views][:new]).to be_nil
+ end
+
+ it 'has nil search subview' do
+ expect(subject[:views][:search]).to be_nil
+ end
+
+ context 'with search' do
+ let(:with_search) { true }
+
+ it 'has search subview' do
+ expect(subject[:views][:search]).to eq(
+ ::Gitlab::Nav::TopNavMenuItem.build(
+ id: 'search',
+ title: 'Search',
+ icon: 'search',
+ href: search_path
+ )
+ )
+ end
+ end
+
+ context 'with new' do
+ let(:with_new_view_model) { { id: 'test-new-view-model' } }
+
+ it 'has new subview' do
+ expect(subject[:views][:new]).to eq({ id: 'test-new-view-model' })
+ end
+ end
+ end
end
diff --git a/spec/helpers/notify_helper_spec.rb b/spec/helpers/notify_helper_spec.rb
index eb0f796038c..e2a7a212b1b 100644
--- a/spec/helpers/notify_helper_spec.rb
+++ b/spec/helpers/notify_helper_spec.rb
@@ -28,27 +28,12 @@ RSpec.describe NotifyHelper do
end
end
- describe '#invited_role_description' do
- where(:role, :description) do
- "Guest" | /As a guest/
- "Reporter" | /As a reporter/
- "Developer" | /As a developer/
- "Maintainer" | /As a maintainer/
- "Owner" | /As an owner/
- "Minimal Access" | /As a user with minimal access/
- end
-
- with_them do
- specify do
- expect(helper.invited_role_description(role)).to match description
- end
- end
- end
-
describe '#invited_to_description' do
where(:source, :description) do
- "project" | /Projects can/
- "group" | /Groups assemble/
+ build(:project, description: nil) | /Projects are/
+ build(:group, description: nil) | /Groups assemble/
+ build(:project, description: '_description_') | '_description_'
+ build(:group, description: '_description_') | '_description_'
end
with_them do
@@ -56,6 +41,15 @@ RSpec.describe NotifyHelper do
expect(helper.invited_to_description(source)).to match description
end
end
+
+ it 'truncates long descriptions', :aggregate_failures do
+ description = '_description_ ' * 30
+ project = build(:project, description: description)
+
+ result = helper.invited_to_description(project)
+ expect(result).not_to match description
+ expect(result.length).to be <= 200
+ end
end
def reference_link(entity, url)
diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb
index 5b0ce00063f..e1bd477bc75 100644
--- a/spec/helpers/operations_helper_spec.rb
+++ b/spec/helpers/operations_helper_spec.rb
@@ -40,7 +40,14 @@ RSpec.describe OperationsHelper do
'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json),
'disabled' => 'false',
'project_path' => project.full_path,
- 'multi_integrations' => 'false'
+ 'multi_integrations' => 'false',
+ 'templates' => '[]',
+ 'create_issue' => 'false',
+ 'issue_template_key' => '',
+ 'send_email' => 'false',
+ 'auto_close_incident' => 'true',
+ 'operations_settings_endpoint' => project_settings_operations_path(project),
+ 'pagerduty_reset_key_path' => reset_pagerduty_token_project_settings_operations_path(project)
)
end
end
@@ -106,9 +113,7 @@ RSpec.describe OperationsHelper do
create(
:project_incident_management_setting,
project: project,
- issue_template_key: 'template-key',
- pagerduty_active: true,
- auto_close_incident: false
+ pagerduty_active: true
)
end
@@ -117,11 +122,6 @@ RSpec.describe OperationsHelper do
it 'returns the correct set of data' do
is_expected.to include(
operations_settings_endpoint: project_settings_operations_path(project),
- templates: '[]',
- create_issue: 'false',
- issue_template_key: 'template-key',
- send_email: 'false',
- auto_close_incident: 'false',
pagerduty_active: 'true',
pagerduty_token: operations_settings.pagerduty_token,
pagerduty_webhook_url: project_incidents_integrations_pagerduty_url(project, token: operations_settings.pagerduty_token),
diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb
index dacd386d01c..93d32cb8418 100644
--- a/spec/helpers/packages_helper_spec.rb
+++ b/spec/helpers/packages_helper_spec.rb
@@ -3,11 +3,13 @@
require 'spec_helper'
RSpec.describe PackagesHelper do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:project) { create(:project) }
let_it_be(:base_url) { "#{Gitlab.config.gitlab.url}/api/v4/" }
- let_it_be(:project) { create(:project) }
- describe 'package_registry_instance_url' do
- it 'returns conant instance url when registry_type is conant' do
+ describe '#package_registry_instance_url' do
+ it 'returns conan instance url when registry_type is conant' do
url = helper.package_registry_instance_url(:conan)
expect(url).to eq("#{base_url}packages/conan")
@@ -20,7 +22,7 @@ RSpec.describe PackagesHelper do
end
end
- describe 'package_registry_project_url' do
+ describe '#package_registry_project_url' do
it 'returns maven registry url when registry_type is not provided' do
url = helper.package_registry_project_url(1)
@@ -34,7 +36,7 @@ RSpec.describe PackagesHelper do
end
end
- describe 'pypi_registry_url' do
+ describe '#pypi_registry_url' do
let_it_be(:base_url_with_token) { base_url.sub('://', '://__token__:<your_personal_token>@') }
it 'returns the pypi registry url' do
@@ -44,7 +46,7 @@ RSpec.describe PackagesHelper do
end
end
- describe 'composer_registry_url' do
+ describe '#composer_registry_url' do
it 'return the composer registry url' do
url = helper.composer_registry_url(1)
@@ -52,7 +54,7 @@ RSpec.describe PackagesHelper do
end
end
- describe 'composer_config_repository_name' do
+ describe '#composer_config_repository_name' do
let(:host) { Gitlab.config.gitlab.host }
let(:group_id) { 1 }
@@ -62,4 +64,157 @@ RSpec.describe PackagesHelper do
expect(id).to eq("#{host}/#{group_id}")
end
end
+
+ describe '#show_cleanup_policy_on_alert' do
+ let_it_be_with_reload(:container_repository) { create(:container_repository) }
+
+ subject { helper.show_cleanup_policy_on_alert(project.reload) }
+
+ where(:com, :config_registry, :project_registry, :historic_entries, :historic_entry, :nil_policy, :container_repositories_exist, :expected_result) do
+ false | false | false | false | false | false | false | false
+ false | false | false | false | false | false | true | false
+ false | false | false | false | false | true | false | false
+ false | false | false | false | false | true | true | false
+ false | false | false | false | true | false | false | false
+ false | false | false | false | true | false | true | false
+ false | false | false | false | true | true | false | false
+ false | false | false | false | true | true | true | false
+ false | false | false | true | false | false | false | false
+ false | false | false | true | false | false | true | false
+ false | false | false | true | false | true | false | false
+ false | false | false | true | false | true | true | false
+ false | false | false | true | true | false | false | false
+ false | false | false | true | true | false | true | false
+ false | false | false | true | true | true | false | false
+ false | false | false | true | true | true | true | false
+ false | false | true | false | false | false | false | false
+ false | false | true | false | false | false | true | false
+ false | false | true | false | false | true | false | false
+ false | false | true | false | false | true | true | false
+ false | false | true | false | true | false | false | false
+ false | false | true | false | true | false | true | false
+ false | false | true | false | true | true | false | false
+ false | false | true | false | true | true | true | false
+ false | false | true | true | false | false | false | false
+ false | false | true | true | false | false | true | false
+ false | false | true | true | false | true | false | false
+ false | false | true | true | false | true | true | false
+ false | false | true | true | true | false | false | false
+ false | false | true | true | true | false | true | false
+ false | false | true | true | true | true | false | false
+ false | false | true | true | true | true | true | false
+ false | true | false | false | false | false | false | false
+ false | true | false | false | false | false | true | false
+ false | true | false | false | false | true | false | false
+ false | true | false | false | false | true | true | false
+ false | true | false | false | true | false | false | false
+ false | true | false | false | true | false | true | false
+ false | true | false | false | true | true | false | false
+ false | true | false | false | true | true | true | false
+ false | true | false | true | false | false | false | false
+ false | true | false | true | false | false | true | false
+ false | true | false | true | false | true | false | false
+ false | true | false | true | false | true | true | false
+ false | true | false | true | true | false | false | false
+ false | true | false | true | true | false | true | false
+ false | true | false | true | true | true | false | false
+ false | true | false | true | true | true | true | false
+ false | true | true | false | false | false | false | false
+ false | true | true | false | false | false | true | false
+ false | true | true | false | false | true | false | false
+ false | true | true | false | false | true | true | false
+ false | true | true | false | true | false | false | false
+ false | true | true | false | true | false | true | false
+ false | true | true | false | true | true | false | false
+ false | true | true | false | true | true | true | false
+ false | true | true | true | false | false | false | false
+ false | true | true | true | false | false | true | false
+ false | true | true | true | false | true | false | false
+ false | true | true | true | false | true | true | false
+ false | true | true | true | true | false | false | false
+ false | true | true | true | true | false | true | false
+ false | true | true | true | true | true | false | false
+ false | true | true | true | true | true | true | false
+ true | false | false | false | false | false | false | false
+ true | false | false | false | false | false | true | false
+ true | false | false | false | false | true | false | false
+ true | false | false | false | false | true | true | false
+ true | false | false | false | true | false | false | false
+ true | false | false | false | true | false | true | false
+ true | false | false | false | true | true | false | false
+ true | false | false | false | true | true | true | false
+ true | false | false | true | false | false | false | false
+ true | false | false | true | false | false | true | false
+ true | false | false | true | false | true | false | false
+ true | false | false | true | false | true | true | false
+ true | false | false | true | true | false | false | false
+ true | false | false | true | true | false | true | false
+ true | false | false | true | true | true | false | false
+ true | false | false | true | true | true | true | false
+ true | false | true | false | false | false | false | false
+ true | false | true | false | false | false | true | false
+ true | false | true | false | false | true | false | false
+ true | false | true | false | false | true | true | false
+ true | false | true | false | true | false | false | false
+ true | false | true | false | true | false | true | false
+ true | false | true | false | true | true | false | false
+ true | false | true | false | true | true | true | false
+ true | false | true | true | false | false | false | false
+ true | false | true | true | false | false | true | false
+ true | false | true | true | false | true | false | false
+ true | false | true | true | false | true | true | false
+ true | false | true | true | true | false | false | false
+ true | false | true | true | true | false | true | false
+ true | false | true | true | true | true | false | false
+ true | false | true | true | true | true | true | false
+ true | true | false | false | false | false | false | false
+ true | true | false | false | false | false | true | false
+ true | true | false | false | false | true | false | false
+ true | true | false | false | false | true | true | false
+ true | true | false | false | true | false | false | false
+ true | true | false | false | true | false | true | false
+ true | true | false | false | true | true | false | false
+ true | true | false | false | true | true | true | false
+ true | true | false | true | false | false | false | false
+ true | true | false | true | false | false | true | false
+ true | true | false | true | false | true | false | false
+ true | true | false | true | false | true | true | false
+ true | true | false | true | true | false | false | false
+ true | true | false | true | true | false | true | false
+ true | true | false | true | true | true | false | false
+ true | true | false | true | true | true | true | false
+ true | true | true | false | false | false | false | false
+ true | true | true | false | false | false | true | false
+ true | true | true | false | false | true | false | false
+ true | true | true | false | false | true | true | false
+ true | true | true | false | true | false | false | false
+ true | true | true | false | true | false | true | false
+ true | true | true | false | true | true | false | false
+ true | true | true | false | true | true | true | true
+ true | true | true | true | false | false | false | false
+ true | true | true | true | false | false | true | false
+ true | true | true | true | false | true | false | false
+ true | true | true | true | false | true | true | false
+ true | true | true | true | true | false | false | false
+ true | true | true | true | true | false | true | false
+ true | true | true | true | true | true | false | false
+ true | true | true | true | true | true | true | false
+ end
+
+ with_them do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(com)
+ stub_config(registry: { enabled: config_registry })
+ allow(project).to receive(:container_registry_enabled).and_return(project_registry)
+ stub_application_setting(container_expiration_policies_enable_historic_entries: historic_entries)
+ stub_feature_flags(container_expiration_policies_historic_entry: false)
+ stub_feature_flags(container_expiration_policies_historic_entry: project) if historic_entry
+
+ project.container_expiration_policy.destroy! if nil_policy
+ container_repository.update!(project_id: project.id) if container_repositories_exist
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+ end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 6be6d3670d4..ad2f142e3ff 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -143,4 +143,41 @@ RSpec.describe PreferencesHelper do
.and_return(double('user', messages))
end
end
+
+ describe '#integration_views' do
+ let(:gitpod_url) { 'http://gitpod.test' }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:gitpod_enabled).and_return(gitpod_enabled)
+ allow(Gitlab::CurrentSettings).to receive(:gitpod_url).and_return(gitpod_url)
+ end
+
+ context 'when Gitpod is not enabled' do
+ let(:gitpod_enabled) { false }
+
+ it 'does not include Gitpod integration' do
+ expect(helper.integration_views).to be_empty
+ end
+ end
+
+ context 'when Gitpod is enabled' do
+ let(:gitpod_enabled) { true }
+
+ it 'includes Gitpod integration' do
+ expect(helper.integration_views[0][:name]).to eq 'gitpod'
+ end
+
+ it 'returns the Gitpod url configured in settings' do
+ expect(helper.integration_views[0][:message_url]).to eq gitpod_url
+ end
+
+ context 'when Gitpod url is not set' do
+ let(:gitpod_url) { '' }
+
+ it 'returns the Gitpod default url' do
+ expect(helper.integration_views[0][:message_url]).to eq 'https://gitpod.io/'
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index 6f66a93b9ec..9895d06f93a 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Projects::AlertManagementHelper do
let(:cluster) { create(:cluster, projects: [project]) }
it 'has managed prometheus' do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
expect(data).to include(
'has-managed-prometheus' => 'true'
diff --git a/spec/helpers/projects/project_members_helper_spec.rb b/spec/helpers/projects/project_members_helper_spec.rb
index 90035f3e1c5..b180b5ec06f 100644
--- a/spec/helpers/projects/project_members_helper_spec.rb
+++ b/spec/helpers/projects/project_members_helper_spec.rb
@@ -147,16 +147,27 @@ RSpec.describe Projects::ProjectMembersHelper do
end
describe 'project members' do
- let_it_be(:project_members) { create_list(:project_member, 2, project: project) }
+ let_it_be(:members) { create_list(:project_member, 2, project: project) }
+ let_it_be(:group_links) { create_list(:project_group_link, 1, project: project) }
+ let_it_be(:invited) { create_list(:project_member, 2, :invited, project: project) }
+ let_it_be(:access_requests) { create_list(:project_member, 2, :access_request, project: project) }
- let(:collection) { project_members }
- let(:presented_members) { present_members(collection) }
+ let(:members_collection) { members }
- describe '#project_members_list_data_json' do
+ describe '#project_members_app_data_json' do
let(:allow_admin_project) { true }
- let(:pagination) { {} }
- subject { Gitlab::Json.parse(helper.project_members_list_data_json(project, presented_members, pagination)) }
+ subject do
+ Gitlab::Json.parse(
+ helper.project_members_app_data_json(
+ project,
+ members: present_members(members_collection),
+ group_links: group_links,
+ invited: present_members(invited),
+ access_requests: present_members(access_requests)
+ )
+ )
+ end
before do
allow(helper).to receive(:project_project_member_path).with(project, ':id').and_return('/foo-bar/-/project_members/:id')
@@ -164,7 +175,6 @@ RSpec.describe Projects::ProjectMembersHelper do
it 'returns expected json' do
expected = {
- member_path: '/foo-bar/-/project_members/:id',
source_id: project.id,
can_manage_members: true
}.as_json
@@ -172,8 +182,12 @@ RSpec.describe Projects::ProjectMembersHelper do
expect(subject).to include(expected)
end
- it 'returns `members` property that matches json schema' do
- expect(subject['members'].to_json).to match_schema('members')
+ it 'sets `members` property that matches json schema' do
+ expect(subject['user']['members'].to_json).to match_schema('members')
+ end
+
+ it 'sets `member_path` property' do
+ expect(subject['user']['member_path']).to eq('/foo-bar/-/project_members/:id')
end
context 'when pagination is not available' do
@@ -186,13 +200,12 @@ RSpec.describe Projects::ProjectMembersHelper do
params: {}
}.as_json
- expect(subject['pagination']).to include(expected)
+ expect(subject['invite']['pagination']).to include(expected)
end
end
context 'when pagination is available' do
- let(:collection) { Kaminari.paginate_array(project_members).page(1).per(1) }
- let(:pagination) { { param_name: :page, params: { search_groups: nil } } }
+ let(:members_collection) { Kaminari.paginate_array(members).page(1).per(1) }
it 'sets `pagination` attribute to expected json' do
expected = {
@@ -203,45 +216,9 @@ RSpec.describe Projects::ProjectMembersHelper do
params: { search_groups: nil }
}.as_json
- expect(subject['pagination']).to match(expected)
+ expect(subject['user']['pagination']).to match(expected)
end
end
end
end
-
- describe 'project group links' do
- let_it_be(:project_group_links) { create_list(:project_group_link, 1, project: project) }
-
- let(:allow_admin_project) { true }
-
- describe '#project_group_links_list_data_json' do
- subject { Gitlab::Json.parse(helper.project_group_links_list_data_json(project, project_group_links)) }
-
- before do
- allow(helper).to receive(:project_group_link_path).with(project, ':id').and_return('/foo-bar/-/group_links/:id')
- allow(helper).to receive(:can?).with(current_user, :admin_project_member, project).and_return(true)
- end
-
- it 'returns expected json' do
- expected = {
- pagination: {
- current_page: nil,
- per_page: nil,
- total_items: 1,
- param_name: nil,
- params: {}
- },
- member_path: '/foo-bar/-/group_links/:id',
- source_id: project.id,
- can_manage_members: true
- }.as_json
-
- expect(subject).to include(expected)
- end
-
- it 'returns `members` property that matches json schema' do
- expect(subject['members'].to_json).to match_schema('group_link/project_group_links')
- end
- end
- end
end
diff --git a/spec/helpers/registrations_helper_spec.rb b/spec/helpers/registrations_helper_spec.rb
index 00d0a0850cd..fa647548b3c 100644
--- a/spec/helpers/registrations_helper_spec.rb
+++ b/spec/helpers/registrations_helper_spec.rb
@@ -26,4 +26,10 @@ RSpec.describe RegistrationsHelper do
it { is_expected.to eq(result) }
end
end
+
+ describe '#signup_username_data_attributes' do
+ it 'has expected attributes' do
+ expect(helper.signup_username_data_attributes.keys).to include(:min_length, :min_length_message, :max_length, :max_length_message, :qa_selector)
+ end
+ end
end
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
index 3dbaa655aeb..f68da45bb9a 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -61,26 +61,6 @@ RSpec.describe UserCalloutsHelper do
end
end
- describe '.show_admin_integrations_moved?' do
- subject { helper.show_admin_integrations_moved? }
-
- context 'when user has not dismissed' do
- before do
- allow(helper).to receive(:user_dismissed?).with(described_class::ADMIN_INTEGRATIONS_MOVED) { false }
- end
-
- it { is_expected.to be true }
- end
-
- context 'when user dismissed' do
- before do
- allow(helper).to receive(:user_dismissed?).with(described_class::ADMIN_INTEGRATIONS_MOVED) { true }
- end
-
- it { is_expected.to be false }
- end
- end
-
describe '.show_service_templates_deprecated_callout?' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/helpers/webpack_helper_spec.rb b/spec/helpers/webpack_helper_spec.rb
index f9386c99dc3..f9e2d265153 100644
--- a/spec/helpers/webpack_helper_spec.rb
+++ b/spec/helpers/webpack_helper_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe WebpackHelper do
describe '#webpack_preload_asset_tag' do
before do
allow(Gitlab::Webpack::Manifest).to receive(:asset_paths).and_return([asset_path])
+ allow(helper).to receive(:content_security_policy_nonce).and_return('noncevalue')
end
it 'preloads the resource by default' do
@@ -22,7 +23,7 @@ RSpec.describe WebpackHelper do
output = helper.webpack_preload_asset_tag(source)
- expect(output).to eq("<link rel=\"preload\" href=\"#{asset_path}\" as=\"script\" type=\"text/javascript\">")
+ expect(output).to eq("<link rel=\"preload\" href=\"#{asset_path}\" as=\"script\" type=\"text/javascript\" nonce=\"noncevalue\">")
end
it 'prefetches the resource if explicitly asked' do
diff --git a/spec/initializers/database_config_spec.rb b/spec/initializers/database_config_spec.rb
index ccd69de0b3a..f1b353d4012 100644
--- a/spec/initializers/database_config_spec.rb
+++ b/spec/initializers/database_config_spec.rb
@@ -7,8 +7,15 @@ RSpec.describe 'Database config initializer' do
load Rails.root.join('config/initializers/database_config.rb')
end
+ around do |example|
+ original_config = ActiveRecord::Base.connection_db_config
+
+ example.run
+
+ ActiveRecord::Base.establish_connection(original_config)
+ end
+
before do
- allow(ActiveRecord::Base).to receive(:establish_connection)
allow(Gitlab::Runtime).to receive(:max_threads).and_return(max_threads)
end
@@ -21,6 +28,8 @@ RSpec.describe 'Database config initializer' do
it "sets it based on the max number of worker threads" do
expect { subject }.to change { Gitlab::Database.config['pool'] }.from(nil).to(18)
+
+ expect(ActiveRecord::Base.connection_db_config.pool).to eq(18)
end
end
@@ -31,6 +40,8 @@ RSpec.describe 'Database config initializer' do
it "sets it based on the max number of worker threads" do
expect { subject }.to change { Gitlab::Database.config['pool'] }.from(1).to(18)
+
+ expect(ActiveRecord::Base.connection_db_config.pool).to eq(18)
end
end
@@ -41,6 +52,8 @@ RSpec.describe 'Database config initializer' do
it "sets it based on the max number of worker threads" do
expect { subject }.to change { Gitlab::Database.config['pool'] }.from(100).to(18)
+
+ expect(ActiveRecord::Base.connection_db_config.pool).to eq(18)
end
end
@@ -56,15 +69,16 @@ RSpec.describe 'Database config initializer' do
expect { subject }.to change { Gitlab::Database.config['pool'] }
.from(1)
.to(max_threads + headroom)
+
+ expect(ActiveRecord::Base.connection_db_config.pool).to eq(max_threads + headroom)
end
end
def stub_database_config(pool_size:)
- config = {
- 'adapter' => 'postgresql',
- 'host' => 'db.host.com',
- 'pool' => pool_size
- }.compact
+ original_config = Gitlab::Database.config
+
+ config = original_config.dup
+ config['pool'] = pool_size
allow(Gitlab::Database).to receive(:config).and_return(config)
end
diff --git a/spec/initializers/global_id_spec.rb b/spec/initializers/global_id_spec.rb
new file mode 100644
index 00000000000..63bfa32d74f
--- /dev/null
+++ b/spec/initializers/global_id_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'global_id' do
+ it 'prepends `Gitlab::Patch::GlobalID`' do
+ expect(GlobalID.ancestors).to include(Gitlab::Patch::GlobalID)
+ end
+
+ it 'patches GlobalID to find aliased models when a deprecation exists' do
+ allow(Gitlab::GlobalId::Deprecations).to receive(:deprecation_for).and_call_original
+ allow(Gitlab::GlobalId::Deprecations).to receive(:deprecation_for).with('Issue').and_return(double(new_model_name: 'Project'))
+ project = create(:project)
+ gid_string = Gitlab::GlobalId.build(model_name: Issue.name, id: project.id).to_s
+
+ expect(GlobalID.new(gid_string)).to have_attributes(
+ to_s: gid_string,
+ model_name: 'Project',
+ model_class: Project,
+ find: project
+ )
+ end
+
+ it 'works as normal when no deprecation exists' do
+ issue = create(:issue)
+ gid_string = Gitlab::GlobalId.build(model_name: Issue.name, id: issue.id).to_s
+
+ expect(GlobalID.new(gid_string)).to have_attributes(
+ to_s: gid_string,
+ model_name: 'Issue',
+ model_class: Issue,
+ find: issue
+ )
+ end
+end
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index 421f6373eff..651b0c8a9b8 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -120,6 +120,7 @@ RSpec.describe 'lograge', type: :request do
context 'with a log subscriber' do
include_context 'parsed logs'
+ include_context 'clear DB Load Balancing configuration'
let(:subscriber) { Lograge::LogSubscribers::ActionController.new }
@@ -195,9 +196,25 @@ RSpec.describe 'lograge', type: :request do
end
context 'with db payload' do
+ let(:db_load_balancing_logging_keys) do
+ %w[
+ db_primary_wal_count
+ db_replica_wal_count
+ db_replica_count
+ db_replica_cached_count
+ db_primary_count
+ db_primary_cached_count
+ db_primary_duration_s
+ db_replica_duration_s
+ ]
+ end
+
+ before do
+ ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ end
+
context 'when RequestStore is enabled', :request_store do
it 'includes db counters' do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
subscriber.process_action(event)
expect(log_data).to include("db_count" => a_value >= 1, "db_write_count" => 0, "db_cached_count" => 0)
@@ -206,12 +223,47 @@ RSpec.describe 'lograge', type: :request do
context 'when RequestStore is disabled' do
it 'does not include db counters' do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
subscriber.process_action(event)
expect(log_data).not_to include("db_count", "db_write_count", "db_cached_count")
end
end
+
+ context 'when load balancing is enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ end
+
+ context 'with db payload' do
+ context 'when RequestStore is enabled', :request_store do
+ it 'includes db counters for load balancing' do
+ subscriber.process_action(event)
+
+ expect(log_data).to include(*db_load_balancing_logging_keys)
+ end
+ end
+
+ context 'when RequestStore is disabled' do
+ it 'does not include db counters for load balancing' do
+ subscriber.process_action(event)
+
+ expect(log_data).not_to include(*db_load_balancing_logging_keys)
+ end
+ end
+ end
+ end
+
+ context 'when load balancing is disabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+ end
+
+ it 'does not include db counters for load balancing' do
+ subscriber.process_action(event)
+
+ expect(log_data).not_to include(*db_load_balancing_logging_keys)
+ end
+ end
end
end
end
diff --git a/spec/initializers/mailer_retries_spec.rb b/spec/initializers/mailer_retries_spec.rb
new file mode 100644
index 00000000000..c1e56784ad9
--- /dev/null
+++ b/spec/initializers/mailer_retries_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Mailer retries' do
+ # We need to ensure that this runs through Sidekiq to take
+ # advantage of the middleware. There is a Rails bug that means we
+ # have to do some extra steps to make this happen:
+ # https://github.com/rails/rails/issues/37270#issuecomment-553927324
+ around do |example|
+ descendants = ActiveJob::Base.descendants + [ActiveJob::Base]
+ descendants.each(&:disable_test_adapter)
+ ActiveJob::Base.queue_adapter = :sidekiq
+
+ example.run
+
+ descendants.each { |a| a.queue_adapter = :test }
+ end
+
+ it 'sets retries for mailers to 3', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332645' do
+ DeviseMailer.user_admin_approval(create(:user)).deliver_later
+
+ expect(Sidekiq::Queues['mailers'].first).to include('retry' => 3)
+ 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 8572b067984..b9d6ab7a652 100644
--- a/spec/lib/api/entities/merge_request_basic_spec.rb
+++ b/spec/lib/api/entities/merge_request_basic_spec.rb
@@ -9,11 +9,22 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
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
def present(obj)
described_class.new(obj).presented
end
+ subject { entity.as_json }
+
+ it 'includes basic fields' do
+ is_expected.to include(
+ draft: merge_request.draft?,
+ work_in_progress: merge_request.draft?
+ )
+ end
+
context "with :with_api_entity_associations scope" do
let(:scope) { MergeRequest.with_api_entity_associations }
diff --git a/spec/lib/api/helpers/runner_helpers_spec.rb b/spec/lib/api/helpers/runner_helpers_spec.rb
new file mode 100644
index 00000000000..65b35845aab
--- /dev/null
+++ b/spec/lib/api/helpers/runner_helpers_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Helpers::Runner do
+ let(:ip_address) { '1.2.3.4' }
+ let(:runner_class) do
+ Class.new do
+ include API::Helpers
+ include API::Helpers::Runner
+
+ attr_accessor :params
+
+ def initialize(params)
+ @params = params
+ end
+
+ def ip_address
+ '1.2.3.4'
+ end
+ end
+ end
+
+ let(:runner_helper) { runner_class.new(runner_params) }
+
+ describe '#get_runner_details_from_request' do
+ context 'when no runner info is present' do
+ let(:runner_params) { {} }
+
+ it 'returns the runner IP' do
+ expect(runner_helper.get_runner_details_from_request).to eq({ ip_address: ip_address })
+ end
+ end
+
+ context 'when runner info is present' do
+ let(:name) { 'runner' }
+ let(:version) { '1.2.3' }
+ let(:revision) { '10.0' }
+ let(:platform) { 'test' }
+ let(:architecture) { 'arm' }
+ let(:config) { { 'gpus' => 'all' } }
+ let(:runner_params) do
+ {
+ 'info' =>
+ {
+ 'name' => name,
+ 'version' => version,
+ 'revision' => revision,
+ 'platform' => platform,
+ 'architecture' => architecture,
+ 'config' => config,
+ 'ignored' => 1
+ }
+ }
+ end
+
+ 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 config ip_address))
+ expect(details['name']).to eq(name)
+ expect(details['version']).to eq(version)
+ expect(details['revision']).to eq(revision)
+ expect(details['platform']).to eq(platform)
+ expect(details['architecture']).to eq(architecture)
+ expect(details['config']).to eq(config)
+ expect(details['ip_address']).to eq(ip_address)
+ end
+ end
+ end
+end
diff --git a/spec/lib/api/helpers/runner_spec.rb b/spec/lib/api/helpers/runner_spec.rb
new file mode 100644
index 00000000000..e55c20b7ab6
--- /dev/null
+++ b/spec/lib/api/helpers/runner_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Helpers::Runner do
+ let(:helper) { Class.new { include API::Helpers::Runner }.new }
+
+ before do
+ allow(helper).to receive(:env).and_return({})
+ end
+
+ describe '#current_job' do
+ let(:build) { create(:ci_build, :running) }
+
+ it 'handles sticking of a build when a build ID is specified' do
+ allow(helper).to receive(:params).and_return(id: build.id)
+
+ expect(Gitlab::Database::LoadBalancing::RackMiddleware)
+ .to receive(:stick_or_unstick)
+ .with({}, :build, build.id)
+
+ helper.current_job
+ end
+
+ it 'does not handle sticking if no build ID was specified' do
+ allow(helper).to receive(:params).and_return({})
+
+ expect(Gitlab::Database::LoadBalancing::RackMiddleware)
+ .not_to receive(:stick_or_unstick)
+
+ helper.current_job
+ end
+
+ it 'returns the build if one could be found' do
+ allow(helper).to receive(:params).and_return(id: build.id)
+
+ expect(helper.current_job).to eq(build)
+ end
+ end
+
+ describe '#current_runner' do
+ let(:runner) { create(:ci_runner, token: 'foo') }
+
+ it 'handles sticking of a runner if a token is specified' do
+ allow(helper).to receive(:params).and_return(token: runner.token)
+
+ expect(Gitlab::Database::LoadBalancing::RackMiddleware)
+ .to receive(:stick_or_unstick)
+ .with({}, :runner, runner.token)
+
+ helper.current_runner
+ end
+
+ it 'does not handle sticking if no token was specified' do
+ allow(helper).to receive(:params).and_return({})
+
+ expect(Gitlab::Database::LoadBalancing::RackMiddleware)
+ .not_to receive(:stick_or_unstick)
+
+ helper.current_runner
+ end
+
+ it 'returns the runner if one could be found' do
+ allow(helper).to receive(:params).and_return(token: runner.token)
+
+ expect(helper.current_runner).to eq(runner)
+ end
+ end
+end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 87cd0d4388c..6e48ee4c315 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -7,6 +7,66 @@ RSpec.describe API::Helpers do
subject { Class.new.include(described_class).new }
+ describe '#current_user' do
+ include Rack::Test::Methods
+
+ let(:user) { build(:user, id: 42) }
+
+ let(:helper) do
+ Class.new(Grape::API::Instance) do
+ helpers API::APIGuard::HelperMethods
+ helpers API::Helpers
+ format :json
+
+ get 'user' do
+ current_user ? { id: current_user.id } : { found: false }
+ end
+
+ get 'protected' do
+ authenticate_by_gitlab_geo_node_token!
+ end
+ end
+ end
+
+ def app
+ helper
+ end
+
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ end
+
+ it 'handles sticking when a user could be found' do
+ allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(user)
+
+ expect(Gitlab::Database::LoadBalancing::RackMiddleware)
+ .to receive(:stick_or_unstick).with(any_args, :user, 42)
+
+ get 'user'
+
+ expect(Gitlab::Json.parse(last_response.body)).to eq({ 'id' => user.id })
+ end
+
+ it 'does not handle sticking if no user could be found' do
+ allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(nil)
+
+ expect(Gitlab::Database::LoadBalancing::RackMiddleware)
+ .not_to receive(:stick_or_unstick)
+
+ get 'user'
+
+ expect(Gitlab::Json.parse(last_response.body)).to eq({ 'found' => false })
+ end
+
+ it 'returns the user if one could be found' do
+ allow_any_instance_of(API::Helpers).to receive(:initial_current_user).and_return(user)
+
+ get 'user'
+
+ expect(Gitlab::Json.parse(last_response.body)).to eq({ 'id' => user.id })
+ end
+ end
+
describe '#find_project' do
let(:project) { create(:project) }
diff --git a/spec/lib/backup/gitaly_backup_spec.rb b/spec/lib/backup/gitaly_backup_spec.rb
new file mode 100644
index 00000000000..13567ead842
--- /dev/null
+++ b/spec/lib/backup/gitaly_backup_spec.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Backup::GitalyBackup do
+ let(:progress) do
+ Tempfile.new('progress').tap do |progress|
+ progress.unlink
+ end
+ end
+
+ after do
+ progress.close
+ end
+
+ subject { described_class.new(progress) }
+
+ context 'unknown' do
+ it 'fails to start unknown' do
+ expect { subject.start(:unknown) }.to raise_error(::Backup::Error, 'unknown backup type: unknown')
+ end
+ end
+
+ context 'create' do
+ RSpec.shared_examples 'creates a repository backup' do
+ it 'creates repository bundles', :aggregate_failures do
+ # Add data to the wiki, design repositories, and snippets, so they will be included in the dump.
+ create(:wiki_page, container: project)
+ create(:design, :with_file, issue: create(:issue, project: project))
+ project_snippet = create(:project_snippet, :repository, project: project)
+ personal_snippet = create(:personal_snippet, :repository, author: project.owner)
+
+ subject.start(:create)
+ subject.enqueue(project, Gitlab::GlRepository::PROJECT)
+ subject.enqueue(project, Gitlab::GlRepository::WIKI)
+ subject.enqueue(project, Gitlab::GlRepository::DESIGN)
+ subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.wait
+
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.design.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', personal_snippet.disk_path + '.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle'))
+ end
+
+ it 'raises when the exit code not zero' do
+ expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false'))
+
+ subject.start(:create)
+ expect { subject.wait }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1')
+ end
+ end
+
+ context 'hashed storage' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ it_behaves_like 'creates a repository backup'
+ end
+
+ context 'legacy storage' do
+ let_it_be(:project) { create(:project, :repository, :legacy_storage) }
+
+ it_behaves_like 'creates a repository backup'
+ end
+ end
+
+ context 'restore' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
+ let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
+
+ def copy_bundle_to_backup_path(bundle_name, destination)
+ FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(destination)))
+ FileUtils.cp(Rails.root.join('spec/fixtures/lib/backup', bundle_name), File.join(Gitlab.config.backup.path, 'repositories', destination))
+ end
+
+ it 'restores from repository bundles', :aggregate_failures do
+ copy_bundle_to_backup_path('project_repo.bundle', project.disk_path + '.bundle')
+ copy_bundle_to_backup_path('wiki_repo.bundle', project.disk_path + '.wiki.bundle')
+ copy_bundle_to_backup_path('design_repo.bundle', project.disk_path + '.design.bundle')
+ copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle')
+ copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle')
+
+ subject.start(:restore)
+ subject.enqueue(project, Gitlab::GlRepository::PROJECT)
+ subject.enqueue(project, Gitlab::GlRepository::WIKI)
+ subject.enqueue(project, Gitlab::GlRepository::DESIGN)
+ subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.wait
+
+ collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) }
+
+ expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec'])
+ expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea'])
+ expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d'])
+ expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e'])
+ expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1'])
+ end
+
+ it 'raises when the exit code not zero' do
+ expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false'))
+
+ subject.start(:restore)
+ expect { subject.wait }.to raise_error(::Backup::Error, 'gitaly-backup exit status 1')
+ end
+ end
+end
diff --git a/spec/lib/backup/gitaly_rpc_backup_spec.rb b/spec/lib/backup/gitaly_rpc_backup_spec.rb
new file mode 100644
index 00000000000..fb442f4a86f
--- /dev/null
+++ b/spec/lib/backup/gitaly_rpc_backup_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Backup::GitalyRpcBackup do
+ let(:progress) { spy(:stdout) }
+
+ subject { described_class.new(progress) }
+
+ after do
+ # make sure we do not leave behind any backup files
+ FileUtils.rm_rf(File.join(Gitlab.config.backup.path, 'repositories'))
+ end
+
+ context 'unknown' do
+ it 'fails to start unknown' do
+ expect { subject.start(:unknown) }.to raise_error(::Backup::Error, 'unknown backup type: unknown')
+ end
+ end
+
+ context 'create' do
+ RSpec.shared_examples 'creates a repository backup' do
+ it 'creates repository bundles', :aggregate_failures do
+ # Add data to the wiki, design repositories, and snippets, so they will be included in the dump.
+ create(:wiki_page, container: project)
+ create(:design, :with_file, issue: create(:issue, project: project))
+ project_snippet = create(:project_snippet, :repository, project: project)
+ personal_snippet = create(:personal_snippet, :repository, author: project.owner)
+
+ subject.start(:create)
+ subject.enqueue(project, Gitlab::GlRepository::PROJECT)
+ subject.enqueue(project, Gitlab::GlRepository::WIKI)
+ subject.enqueue(project, Gitlab::GlRepository::DESIGN)
+ subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.wait
+
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.design.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', personal_snippet.disk_path + '.bundle'))
+ expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle'))
+ end
+
+ context 'failure' do
+ before do
+ allow_next_instance_of(Repository) do |repository|
+ allow(repository).to receive(:bundle_to_disk) { raise 'Fail in tests' }
+ end
+ end
+
+ it 'logs an appropriate message', :aggregate_failures do
+ subject.start(:create)
+ subject.enqueue(project, Gitlab::GlRepository::PROJECT)
+ subject.wait
+
+ expect(progress).to have_received(:puts).with("[Failed] backing up #{project.full_path} (#{project.disk_path})")
+ expect(progress).to have_received(:puts).with("Error Fail in tests")
+ end
+ end
+ end
+
+ context 'hashed storage' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ it_behaves_like 'creates a repository backup'
+ end
+
+ context 'legacy storage' do
+ let_it_be(:project) { create(:project, :repository, :legacy_storage) }
+
+ it_behaves_like 'creates a repository backup'
+ end
+ end
+
+ context 'restore' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
+ let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
+
+ def copy_bundle_to_backup_path(bundle_name, destination)
+ FileUtils.mkdir_p(File.join(Gitlab.config.backup.path, 'repositories', File.dirname(destination)))
+ FileUtils.cp(Rails.root.join('spec/fixtures/lib/backup', bundle_name), File.join(Gitlab.config.backup.path, 'repositories', destination))
+ end
+
+ it 'restores from repository bundles', :aggregate_failures do
+ copy_bundle_to_backup_path('project_repo.bundle', project.disk_path + '.bundle')
+ copy_bundle_to_backup_path('wiki_repo.bundle', project.disk_path + '.wiki.bundle')
+ copy_bundle_to_backup_path('design_repo.bundle', project.disk_path + '.design.bundle')
+ copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle')
+ copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle')
+
+ subject.start(:restore)
+ subject.enqueue(project, Gitlab::GlRepository::PROJECT)
+ subject.enqueue(project, Gitlab::GlRepository::WIKI)
+ subject.enqueue(project, Gitlab::GlRepository::DESIGN)
+ subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.wait
+
+ collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) }
+
+ expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec'])
+ expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea'])
+ expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d'])
+ expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e'])
+ expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1'])
+ end
+
+ it 'cleans existing repositories', :aggregate_failures do
+ expect_next_instance_of(DesignManagement::Repository) do |repository|
+ expect(repository).to receive(:remove)
+ end
+
+ # 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo
+ expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args|
+ full_path, container, kwargs = original_args
+
+ repository = method.call(full_path, container, **kwargs)
+
+ expect(repository).to receive(:remove)
+
+ repository
+ end
+
+ subject.start(:restore)
+ subject.enqueue(project, Gitlab::GlRepository::PROJECT)
+ subject.enqueue(project, Gitlab::GlRepository::WIKI)
+ subject.enqueue(project, Gitlab::GlRepository::DESIGN)
+ subject.enqueue(personal_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.enqueue(project_snippet, Gitlab::GlRepository::SNIPPET)
+ subject.wait
+ end
+
+ context 'failure' do
+ before do
+ allow_next_instance_of(Repository) do |repository|
+ allow(repository).to receive(:create_repository) { raise 'Fail in tests' }
+ allow(repository).to receive(:create_from_bundle) { raise 'Fail in tests' }
+ end
+ end
+
+ it 'logs an appropriate message', :aggregate_failures do
+ subject.start(:restore)
+ subject.enqueue(project, Gitlab::GlRepository::PROJECT)
+ subject.wait
+
+ expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})")
+ expect(progress).to have_received(:puts).with("Error Fail in tests")
+ end
+ end
+ end
+end
diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb
index 7a8cc713e4f..d77b1e0f276 100644
--- a/spec/lib/backup/repositories_spec.rb
+++ b/spec/lib/backup/repositories_spec.rb
@@ -3,37 +3,28 @@
require 'spec_helper'
RSpec.describe Backup::Repositories do
- let(:progress) { StringIO.new }
+ let(:progress) { spy(:stdout) }
+ let(:strategy) { spy(:strategy) }
- subject { described_class.new(progress) }
-
- before do
- allow(progress).to receive(:puts)
- allow(progress).to receive(:print)
-
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:progress).and_return(progress)
- end
- end
+ subject { described_class.new(progress, strategy: strategy) }
describe '#dump' do
let_it_be(:projects) { create_list(:project, 5, :repository) }
RSpec.shared_examples 'creates repository bundles' do
- specify :aggregate_failures do
- # Add data to the wiki, design repositories, and snippets, so they will be included in the dump.
- create(:wiki_page, container: project)
- create(:design, :with_file, issue: create(:issue, project: project))
+ it 'calls enqueue for each repository type', :aggregate_failures do
project_snippet = create(:project_snippet, :repository, project: project)
personal_snippet = create(:personal_snippet, :repository, author: project.owner)
subject.dump(max_concurrency: 1, max_storage_concurrency: 1)
- expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle'))
- expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki' + '.bundle'))
- expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.design' + '.bundle'))
- expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', personal_snippet.disk_path + '.bundle'))
- expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle'))
+ expect(strategy).to have_received(:start).with(:create)
+ expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
+ expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::WIKI)
+ expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN)
+ expect(strategy).to have_received(:enqueue).with(project_snippet, Gitlab::GlRepository::SNIPPET)
+ expect(strategy).to have_received(:enqueue).with(personal_snippet, Gitlab::GlRepository::SNIPPET)
+ expect(strategy).to have_received(:wait)
end
end
@@ -53,16 +44,18 @@ RSpec.describe Backup::Repositories do
it 'creates the expected number of threads' do
expect(Thread).not_to receive(:new)
+ expect(strategy).to receive(:start).with(:create)
projects.each do |project|
- expect(subject).to receive(:dump_project).with(project).and_call_original
+ expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
+ expect(strategy).to receive(:wait)
subject.dump(max_concurrency: 1, max_storage_concurrency: 1)
end
describe 'command failure' do
- it 'dump_project raises an error' do
- allow(subject).to receive(:dump_project).and_raise(IOError)
+ it 'enqueue_project raises an error' do
+ allow(strategy).to receive(:enqueue).with(anything, Gitlab::GlRepository::PROJECT).and_raise(IOError)
expect { subject.dump(max_concurrency: 1, max_storage_concurrency: 1) }.to raise_error(IOError)
end
@@ -100,9 +93,11 @@ RSpec.describe Backup::Repositories do
.exactly(storage_keys.length * (max_storage_concurrency + 1)).times
.and_call_original
+ expect(strategy).to receive(:start).with(:create)
projects.each do |project|
- expect(subject).to receive(:dump_project).with(project).and_call_original
+ expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
+ expect(strategy).to receive(:wait)
subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency)
end
@@ -112,17 +107,18 @@ RSpec.describe Backup::Repositories do
.exactly(storage_keys.length * (max_storage_concurrency + 1)).times
.and_call_original
+ expect(strategy).to receive(:start).with(:create)
projects.each do |project|
- expect(subject).to receive(:dump_project).with(project).and_call_original
+ expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
+ expect(strategy).to receive(:wait)
subject.dump(max_concurrency: 3, max_storage_concurrency: max_storage_concurrency)
end
describe 'command failure' do
- it 'dump_project raises an error' do
- allow(subject).to receive(:dump_project)
- .and_raise(IOError)
+ it 'enqueue_project raises an error' do
+ allow(strategy).to receive(:enqueue).and_raise(IOError)
expect { subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) }.to raise_error(IOError)
end
@@ -162,61 +158,16 @@ RSpec.describe Backup::Repositories do
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
- let(:next_path_to_bundle) do
- [
- Rails.root.join('spec/fixtures/lib/backup/project_repo.bundle'),
- Rails.root.join('spec/fixtures/lib/backup/wiki_repo.bundle'),
- Rails.root.join('spec/fixtures/lib/backup/design_repo.bundle'),
- Rails.root.join('spec/fixtures/lib/backup/personal_snippet_repo.bundle'),
- Rails.root.join('spec/fixtures/lib/backup/project_snippet_repo.bundle')
- ].to_enum
- end
-
- it 'restores repositories from bundles', :aggregate_failures do
- allow_next_instance_of(described_class::BackupRestore) do |backup_restore|
- allow(backup_restore).to receive(:path_to_bundle).and_return(next_path_to_bundle.next)
- end
-
+ it 'calls enqueue for each repository type', :aggregate_failures do
subject.restore
- collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) }
-
- expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec'])
- expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea'])
- expect(collect_commit_shas.call(project.design_repository)).to eq(['c3cd4d7bd73a51a0f22045c3a4c871c435dc959d'])
- expect(collect_commit_shas.call(personal_snippet.repository)).to eq(['3b3c067a3bc1d1b695b51e2be30c0f8cf698a06e'])
- expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1'])
- end
-
- describe 'command failure' do
- before do
- expect(Project).to receive(:find_each).and_yield(project)
-
- allow_next_instance_of(DesignManagement::Repository) do |repository|
- allow(repository).to receive(:create_repository) { raise 'Fail in tests' }
- end
- allow_next_instance_of(Repository) do |repository|
- allow(repository).to receive(:create_repository) { raise 'Fail in tests' }
- end
- end
-
- context 'hashed storage' do
- it 'shows the appropriate error' do
- subject.restore
-
- expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})")
- end
- end
-
- context 'legacy storage' do
- let_it_be(:project) { create(:project, :legacy_storage) }
-
- it 'shows the appropriate error' do
- subject.restore
-
- expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} (#{project.disk_path})")
- end
- end
+ expect(strategy).to have_received(:start).with(:restore)
+ expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
+ expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::WIKI)
+ expect(strategy).to have_received(:enqueue).with(project, Gitlab::GlRepository::DESIGN)
+ expect(strategy).to have_received(:enqueue).with(project_snippet, Gitlab::GlRepository::SNIPPET)
+ expect(strategy).to have_received(:enqueue).with(personal_snippet, Gitlab::GlRepository::SNIPPET)
+ expect(strategy).to have_received(:wait)
end
context 'restoring object pools' do
@@ -242,78 +193,36 @@ RSpec.describe Backup::Repositories do
end
end
- it 'cleans existing repositories' do
- success_response = ServiceResponse.success(message: "Valid Snippet Repo")
- allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(success_response)
-
- expect_next_instance_of(DesignManagement::Repository) do |repository|
- expect(repository).to receive(:remove)
- end
-
- # 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo
- expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args|
- full_path, container, kwargs = original_args
-
- repository = method.call(full_path, container, **kwargs)
-
- expect(repository).to receive(:remove)
-
- repository
- end
-
- subject.restore
- end
-
- context 'restoring snippets' do
+ context 'cleanup snippets' do
before do
create(:snippet_repository, snippet: personal_snippet)
create(:snippet_repository, snippet: project_snippet)
- allow_next_instance_of(described_class::BackupRestore) do |backup_restore|
- allow(backup_restore).to receive(:path_to_bundle).and_return(next_path_to_bundle.next)
- end
+ error_response = ServiceResponse.error(message: "Repository has more than one branch")
+ allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(error_response)
end
- context 'when the repository is valid' do
- it 'restores the snippet repositories' do
- subject.restore
-
- expect(personal_snippet.snippet_repository.persisted?).to be true
- expect(personal_snippet.repository).to exist
+ it 'shows the appropriate error' do
+ subject.restore
- expect(project_snippet.snippet_repository.persisted?).to be true
- expect(project_snippet.repository).to exist
- end
+ expect(progress).to have_received(:puts).with("Snippet #{personal_snippet.full_path} can't be restored: Repository has more than one branch")
+ expect(progress).to have_received(:puts).with("Snippet #{project_snippet.full_path} can't be restored: Repository has more than one branch")
end
- context 'when repository is invalid' do
- before do
- error_response = ServiceResponse.error(message: "Repository has more than one branch")
- allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(error_response)
- end
-
- it 'shows the appropriate error' do
- subject.restore
-
- expect(progress).to have_received(:puts).with("Snippet #{personal_snippet.full_path} can't be restored: Repository has more than one branch")
- expect(progress).to have_received(:puts).with("Snippet #{project_snippet.full_path} can't be restored: Repository has more than one branch")
- end
-
- it 'removes the snippets from the DB' do
- expect { subject.restore }.to change(PersonalSnippet, :count).by(-1)
- .and change(ProjectSnippet, :count).by(-1)
- .and change(SnippetRepository, :count).by(-2)
- end
+ it 'removes the snippets from the DB' do
+ expect { subject.restore }.to change(PersonalSnippet, :count).by(-1)
+ .and change(ProjectSnippet, :count).by(-1)
+ .and change(SnippetRepository, :count).by(-2)
+ end
- it 'removes the repository from disk' do
- gitlab_shell = Gitlab::Shell.new
- shard_name = personal_snippet.repository.shard
- path = personal_snippet.disk_path + '.git'
+ it 'removes the repository from disk' do
+ gitlab_shell = Gitlab::Shell.new
+ shard_name = personal_snippet.repository.shard
+ path = personal_snippet.disk_path + '.git'
- subject.restore
+ subject.restore
- expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false
- end
+ expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false
end
end
end
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 db7dda96cad..b18d68c8dd4 100644
--- a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
@@ -702,4 +702,72 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
expect(result.css('a').first.text).to eq "#{label.name} in #{project.full_name}"
end
end
+
+ context 'checking N+1' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:project) { create(:project, :public, namespace: group) }
+ let_it_be(:project2) { create(:project, :public, namespace: group2) }
+ let_it_be(:project3) { create(:project, :public) }
+ let_it_be(:project_label) { create(:label, project: project) }
+ let_it_be(:project_label2) { create(:label, project: project) }
+ let_it_be(:project2_label) { create(:label, project: project2) }
+ let_it_be(:group2_label) { create(:group_label, group: group2, color: '#00ff00') }
+ let_it_be(:project_reference) { "#{project_label.to_reference}" }
+ let_it_be(:project_reference2) { "#{project_label2.to_reference}" }
+ let_it_be(:project2_reference) { "#{project2_label.to_reference}" }
+ let_it_be(:group2_reference) { "#{project2.full_path}~#{group2_label.name}" }
+
+ it 'does not have N+1 per multiple references per project', :use_sql_query_cache do
+ markdown = "#{project_reference}"
+ control_count = 1
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(control_count)
+
+ markdown = "#{project_reference} ~qwert ~werty ~ertyu ~rtyui #{project_reference2}"
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+
+ it 'has N+1 for multiple unique project/group references', :use_sql_query_cache do
+ # reference to already loaded project, only one query
+ markdown = "#{project_reference}"
+ control_count = 1
+
+ expect do
+ reference_filter(markdown, project: project)
+ end.not_to exceed_all_query_limit(control_count)
+
+ # Since we're not batching label queries across projects/groups,
+ # queries increase when a new project/group is added.
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359
+ # first reference to already loaded project (1),
+ # second reference requires project and namespace (2), and label (1)
+ markdown = "#{project_reference} #{group2_reference}"
+ max_count = control_count + 3
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(max_count)
+
+ # third reference to already queried project/namespace, nothing extra (no N+1 here)
+ markdown = "#{project_reference} #{group2_reference} #{project2_reference}"
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(max_count)
+
+ # last reference needs another namespace and label query (2)
+ markdown = "#{project_reference} #{group2_reference} #{project2_reference} #{project3.full_path}~test_label"
+ max_count += 2
+
+ expect do
+ reference_filter(markdown)
+ end.not_to exceed_all_query_limit(max_count)
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb
index 9e2a6f35910..c9404c381d3 100644
--- a/spec/lib/banzai/filter/references/reference_cache_spec.rb
+++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb
@@ -55,11 +55,12 @@ RSpec.describe Banzai::Filter::References::ReferenceCache do
cache_single.load_records_per_parent
end.count
+ expect(control_count).to eq 1
+
# Since this is an issue filter that is not batching issue queries
# across projects, we have to account for that.
- # 1 for both projects, 1 for issues in each project == 3
- # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359
- max_count = control_count + 1
+ # 1 for original issue, 2 for second route/project, 1 for other issue
+ max_count = control_count + 3
expect do
cache.load_references_per_parent(filter.nodes)
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 7ab3b24b1c2..2e324669870 100644
--- a/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb
@@ -233,13 +233,15 @@ RSpec.describe Banzai::Filter::References::SnippetReferenceFilter do
reference_filter(markdown)
end.count
+ expect(control_count).to eq 1
+
markdown = "#{reference} $9999990 $9999991 $9999992 $9999993 #{reference2} something/cool$12"
# Since we're not batching snippet queries across projects,
# we have to account for that.
# 1 for both projects, 1 for snippets in each project == 3
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359
- max_count = control_count + 1
+ max_count = control_count + 2
expect do
reference_filter(markdown)
diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
index 5f31ad0c8f6..4903f624469 100644
--- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb
@@ -17,20 +17,6 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do
result
end
- context 'when feature flag honor_escaped_markdown is disabled' do
- before do
- stub_feature_flags(honor_escaped_markdown: false)
- end
-
- it 'does not escape the markdown' do
- result = described_class.call(%q(\!), project: project)
- output = result[:output].to_html
-
- expect(output).to eq('<p data-sourcepos="1:1-1:2">!</p>')
- expect(result[:escaped_literals]).to be_falsey
- end
- end
-
describe 'CommonMark tests', :aggregate_failures do
it 'converts all reference punctuation to literals' do
reference_chars = Banzai::Filter::MarkdownPreEscapeFilter::REFERENCE_CHARACTERS
diff --git a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
index 55038d58f22..e8df395564a 100644
--- a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
@@ -44,18 +44,5 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline do
subject
end
-
- context 'when "optimize_linkable_attributes" is disabled' do
- before do
- stub_feature_flags(optimize_linkable_attributes: false)
- end
-
- it 'searches for attributes twice' do
- expect(doc).to receive(:xpath).exactly(non_related_xpath_calls + 2).times
- .and_call_original
-
- subject
- end
- end
end
end
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index 612ce6b93f1..31cece108bf 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -130,11 +130,11 @@ RSpec.describe Banzai::ReferenceParser::CommitParser do
end
context 'when checking commits on another projects' do
- let(:control_links) do
+ let!(:control_links) do
[commit_link]
end
- let(:actual_links) do
+ let!(:actual_links) do
control_links + [commit_link, commit_link]
end
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 76f13e7b3aa..7de78710d34 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -5,9 +5,11 @@ require 'spec_helper'
RSpec.describe Banzai::ReferenceParser::IssueParser do
include ReferenceParserHelpers
- let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
let(:link) { empty_html_link }
subject { described_class.new(Banzai::RenderContext.new(project, user)) }
@@ -121,7 +123,7 @@ RSpec.describe Banzai::ReferenceParser::IssueParser do
end
end
- context 'when checking multiple merge requests on another project' do
+ context 'when checking multiple issues on another project' do
let(:other_project) { create(:project, :public) }
let(:other_issue) { create(:issue, project: other_project) }
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 1820141c898..04c35c8b082 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -5,9 +5,11 @@ require 'spec_helper'
RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
include ReferenceParserHelpers
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, group: group) }
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
+
subject(:parser) { described_class.new(Banzai::RenderContext.new(merge_request.target_project, user)) }
let(:link) { empty_html_link }
@@ -16,10 +18,19 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
context 'when the link has a data-issue attribute' do
before do
project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ link['data-project'] = merge_request.project_id.to_s
link['data-merge-request'] = merge_request.id.to_s
end
it_behaves_like "referenced feature visibility", "merge_requests"
+
+ context 'when optimize_merge_request_parser feature flag is off' do
+ before do
+ stub_feature_flags(optimize_merge_request_parser: false)
+ end
+
+ it_behaves_like "referenced feature visibility", "merge_requests"
+ end
end
end
@@ -27,6 +38,7 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
describe 'when the link has a data-merge-request attribute' do
context 'using an existing merge request ID' do
it 'returns an Array of merge requests' do
+ link['data-project'] = merge_request.project_id.to_s
link['data-merge-request'] = merge_request.id.to_s
expect(subject.referenced_by([link])).to eq([merge_request])
@@ -35,6 +47,7 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
context 'using a non-existing merge request ID' do
it 'returns an empty Array' do
+ link['data-project'] = merge_request.project_id.to_s
link['data-merge-request'] = ''
expect(subject.referenced_by([link])).to eq([])
@@ -47,16 +60,16 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
let(:other_project) { create(:project, :public) }
let(:other_merge_request) { create(:merge_request, source_project: other_project) }
- let(:control_links) do
+ let!(:control_links) do
[merge_request_link(other_merge_request)]
end
- let(:actual_links) do
+ let!(:actual_links) do
control_links + [merge_request_link(create(:merge_request, :conflict, source_project: other_project))]
end
def merge_request_link(merge_request)
- Nokogiri::HTML.fragment(%Q{<a data-merge-request="#{merge_request.id}"></a>}).children[0]
+ Nokogiri::HTML.fragment(%Q{<a data-project="#{merge_request.project_id}" data-merge-request="#{merge_request.id}"></a>}).children[0]
end
before do
diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb
index 213fa23675e..ac42f12a3d4 100644
--- a/spec/lib/bulk_imports/clients/http_spec.rb
+++ b/spec/lib/bulk_imports/clients/http_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Clients::Http do
+RSpec.describe BulkImports::Clients::HTTP do
include ImportSpecHelper
let(:uri) { 'http://gitlab.example' }
@@ -48,6 +48,7 @@ RSpec.describe BulkImports::Clients::Http do
[
'http://gitlab.example:80/api/v4/resource',
hash_including(
+ follow_redirects: false,
query: {
page: described_class::DEFAULT_PAGE,
per_page: described_class::DEFAULT_PER_PAGE
@@ -118,6 +119,7 @@ RSpec.describe BulkImports::Clients::Http do
'http://gitlab.example:80/api/v4/resource',
hash_including(
body: {},
+ follow_redirects: false,
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}"
@@ -127,4 +129,42 @@ RSpec.describe BulkImports::Clients::Http do
end
end
end
+
+ describe '#head' do
+ let(:method) { :head }
+
+ include_examples 'performs network request' do
+ let(:expected_args) do
+ [
+ 'http://gitlab.example:80/api/v4/resource',
+ hash_including(
+ follow_redirects: false,
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{token}"
+ }
+ )
+ ]
+ end
+ end
+ end
+
+ describe '#stream' do
+ it 'performs network request with stream_body option' do
+ expected_args = [
+ 'http://gitlab.example:80/api/v4/resource',
+ hash_including(
+ stream_body: true,
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{token}"
+ }
+ )
+ ]
+
+ expect(Gitlab::HTTP).to receive(:get).with(*expected_args).and_return(response_double)
+
+ subject.stream(resource)
+ end
+ end
end
diff --git a/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb
new file mode 100644
index 00000000000..bd306233de8
--- /dev/null
+++ b/spec/lib/bulk_imports/common/extractors/ndjson_extractor_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Common::Extractors::NdjsonExtractor do
+ let_it_be(:tmpdir) { Dir.mktmpdir }
+ let_it_be(:filepath) { 'spec/fixtures/bulk_imports/gz/labels.ndjson.gz' }
+ let_it_be(:import) { create(:bulk_import) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: import) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: import) }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ subject { described_class.new(relation: 'labels') }
+
+ before do
+ allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original
+
+ subject.instance_variable_set(:@tmp_dir, tmpdir)
+ end
+
+ after(:all) do
+ FileUtils.remove_entry(tmpdir) if File.directory?(tmpdir)
+ end
+
+ describe '#extract' do
+ before do
+ FileUtils.copy_file(filepath, File.join(tmpdir, 'labels.ndjson.gz'))
+
+ allow_next_instance_of(BulkImports::FileDownloadService) do |service|
+ allow(service).to receive(:execute)
+ end
+ end
+
+ it 'returns ExtractedData' do
+ extracted_data = subject.extract(context)
+ label = extracted_data.data.first.first
+
+ expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData)
+ expect(label['title']).to include('Label')
+ expect(label['description']).to include('Label')
+ expect(label['type']).to eq('GroupLabel')
+ end
+ end
+
+ describe '#remove_tmp_dir' do
+ it 'removes tmp dir' do
+ expect(FileUtils).to receive(:remove_entry).with(tmpdir).once
+
+ subject.remove_tmp_dir
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb
index 721dacbe3f4..5ee5cdbe413 100644
--- a/spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb
+++ b/spec/lib/bulk_imports/common/extractors/rest_extractor_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe BulkImports::Common::Extractors::RestExtractor do
- let(:http_client) { instance_double(BulkImports::Clients::Http) }
+ let(:http_client) { instance_double(BulkImports::Clients::HTTP) }
let(:options) { { query: double(to_h: { resource: nil, query: nil }) } }
let(:response) { double(parsed_response: { 'data' => { 'foo' => 'bar' } }, headers: { 'x-next-page' => '2' }) }
diff --git a/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb
index ac8786440e9..f7485b188ce 100644
--- a/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb
+++ b/spec/lib/bulk_imports/groups/extractors/subgroups_extractor_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe BulkImports::Groups::Extractors::SubgroupsExtractor do
response = [{ 'test' => 'group' }]
context = BulkImports::Pipeline::Context.new(tracker)
- allow_next_instance_of(BulkImports::Clients::Http) do |client|
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
allow(client).to receive(:each_page).and_return(response)
end
diff --git a/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb
deleted file mode 100644
index 61db644a372..00000000000
--- a/spec/lib/bulk_imports/groups/graphql/get_labels_query_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe BulkImports::Groups::Graphql::GetLabelsQuery do
- it 'has a valid query' do
- tracker = create(:bulk_import_tracker)
- context = BulkImports::Pipeline::Context.new(tracker)
-
- query = GraphQL::Query.new(
- GitlabSchema,
- described_class.to_s,
- variables: described_class.variables(context)
- )
- result = GitlabSchema.static_validator.validate(query)
-
- expect(result[:errors]).to be_empty
- end
-
- describe '#data_path' do
- it 'returns data path' do
- expected = %w[data group labels nodes]
-
- expect(described_class.data_path).to eq(expected)
- end
- end
-
- describe '#page_info_path' do
- it 'returns pagination information path' do
- expected = %w[data group labels page_info]
-
- expect(described_class.page_info_path).to eq(expected)
- end
- end
-end
diff --git a/spec/lib/bulk_imports/groups/pipelines/boards_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/boards_pipeline_spec.rb
new file mode 100644
index 00000000000..8b2f03ca15f
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/pipelines/boards_pipeline_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Pipelines::BoardsPipeline do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:filepath) { 'spec/fixtures/bulk_imports/gz/boards.ndjson.gz' }
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ group: group,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path',
+ destination_name: 'My Destination Group',
+ destination_namespace: group.full_path
+ )
+ end
+
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ let(:tmpdir) { Dir.mktmpdir }
+
+ before do
+ FileUtils.copy_file(filepath, File.join(tmpdir, 'boards.ndjson.gz'))
+ group.add_owner(user)
+ end
+
+ subject { described_class.new(context) }
+
+ describe '#run' do
+ it 'imports group boards into destination group and removes tmpdir' do
+ allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
+ allow_next_instance_of(BulkImports::FileDownloadService) do |service|
+ allow(service).to receive(:execute)
+ end
+
+ expect { subject.run }.to change(Board, :count).by(1)
+
+ lists = group.boards.find_by(name: 'first board').lists
+
+ expect(lists.count).to eq(3)
+ expect(lists.first.label.title).to eq('TSL')
+ expect(lists.second.label.title).to eq('Sosync')
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb b/spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb
index 8276349c5f4..b97aeb435b9 100644
--- a/spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/entity_finisher_spec.rb
@@ -25,13 +25,33 @@ RSpec.describe BulkImports::Groups::Pipelines::EntityFinisher do
.to change(entity, :status_name).to(:finished)
end
- it 'does nothing when the entity is already finished' do
- entity = create(:bulk_import_entity, :finished)
- pipeline_tracker = create(:bulk_import_tracker, entity: entity)
- context = BulkImports::Pipeline::Context.new(pipeline_tracker)
- subject = described_class.new(context)
+ context 'when entity is in a final finished or failed state' do
+ shared_examples 'performs no state update' do |entity_state|
+ it 'does nothing' do
+ entity = create(:bulk_import_entity, entity_state)
+ pipeline_tracker = create(:bulk_import_tracker, entity: entity)
+ context = BulkImports::Pipeline::Context.new(pipeline_tracker)
+ subject = described_class.new(context)
- expect { subject.run }
- .not_to change(entity, :status_name)
+ expect { subject.run }
+ .not_to change(entity, :status_name)
+ end
+ end
+
+ include_examples 'performs no state update', :finished
+ include_examples 'performs no state update', :failed
+ end
+
+ context 'when all entity trackers failed' do
+ it 'marks entity as failed' do
+ entity = create(:bulk_import_entity, :started)
+ create(:bulk_import_tracker, :failed, entity: entity)
+ pipeline_tracker = create(:bulk_import_tracker, entity: entity, relation: described_class)
+ context = BulkImports::Pipeline::Context.new(pipeline_tracker)
+
+ described_class.new(context).run
+
+ expect(entity.reload.failed?).to eq(true)
+ end
end
end
diff --git a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb
index 8af646d1101..6344dae0fb7 100644
--- a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb
@@ -5,98 +5,74 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
- let_it_be(:timestamp) { Time.new(2020, 01, 01).utc }
-
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:filepath) { 'spec/fixtures/bulk_imports/gz/labels.ndjson.gz' }
let_it_be(:entity) do
create(
:bulk_import_entity,
+ group: group,
+ bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: 'My Destination Group',
- destination_namespace: group.full_path,
- group: group
+ destination_namespace: group.full_path
)
end
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+ let(:tmpdir) { Dir.mktmpdir }
+
+ before do
+ FileUtils.copy_file(filepath, File.join(tmpdir, 'labels.ndjson.gz'))
+ group.add_owner(user)
+ end
+
subject { described_class.new(context) }
describe '#run' do
- it 'imports a group labels' do
- first_page = extracted_data(title: 'label1', has_next_page: true)
- last_page = extracted_data(title: 'label2')
-
- allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
- allow(extractor)
- .to receive(:extract)
- .and_return(first_page, last_page)
+ it 'imports group labels into destination group and removes tmpdir' do
+ allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
+ allow_next_instance_of(BulkImports::FileDownloadService) do |service|
+ allow(service).to receive(:execute)
end
- expect { subject.run }.to change(Label, :count).by(2)
+ expect { subject.run }.to change(::GroupLabel, :count).by(1)
- label = group.labels.order(:created_at).last
+ label = group.labels.first
- expect(label.title).to eq('label2')
- expect(label.description).to eq('desc')
- expect(label.color).to eq('#428BCA')
- expect(label.created_at).to eq(timestamp)
- expect(label.updated_at).to eq(timestamp)
+ expect(label.title).to eq('Label 1')
+ expect(label.description).to eq('Label 1')
+ expect(label.color).to eq('#6699cc')
+ expect(File.directory?(tmpdir)).to eq(false)
end
end
describe '#load' do
- it 'creates the label' do
- data = label_data('label')
+ context 'when label is not persisted' do
+ it 'saves the label' do
+ label = build(:group_label, group: group)
- expect { subject.load(context, data) }.to change(Label, :count).by(1)
+ expect(label).to receive(:save!)
- label = group.labels.first
-
- data.each do |key, value|
- expect(label[key]).to eq(value)
+ subject.load(context, label)
end
end
- end
- describe 'pipeline parts' do
- it { expect(described_class).to include_module(BulkImports::Pipeline) }
- it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
-
- it 'has extractors' do
- expect(described_class.get_extractor)
- .to eq(
- klass: BulkImports::Common::Extractors::GraphqlExtractor,
- options: {
- query: BulkImports::Groups::Graphql::GetLabelsQuery
- }
- )
- end
+ context 'when label is persisted' do
+ it 'does not save label' do
+ label = create(:group_label, group: group)
- it 'has transformers' do
- expect(described_class.transformers)
- .to contain_exactly(
- { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }
- )
- end
- end
-
- def label_data(title)
- {
- 'title' => title,
- 'description' => 'desc',
- 'color' => '#428BCA',
- 'created_at' => timestamp.to_s,
- 'updated_at' => timestamp.to_s
- }
- end
+ expect(label).not_to receive(:save!)
- def extracted_data(title:, has_next_page: false)
- page_info = {
- 'has_next_page' => has_next_page,
- 'next_page' => has_next_page ? 'cursor' : nil
- }
+ subject.load(context, label)
+ end
+ end
- BulkImports::Pipeline::ExtractedData.new(data: [label_data(title)], page_info: page_info)
+ context 'when label is missing' do
+ it 'returns' do
+ expect(subject.load(context, nil)).to be_nil
+ end
+ end
end
end
diff --git a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb
index e5cf75c566b..a8354e62459 100644
--- a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb
+++ b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb
@@ -5,119 +5,69 @@ require 'spec_helper'
RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
- let_it_be(:timestamp) { Time.new(2020, 01, 01).utc }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
-
+ let_it_be(:filepath) { 'spec/fixtures/bulk_imports/gz/milestones.ndjson.gz' }
let_it_be(:entity) do
create(
:bulk_import_entity,
+ group: group,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: 'My Destination Group',
- destination_namespace: group.full_path,
- group: group
+ destination_namespace: group.full_path
)
end
let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
- subject { described_class.new(context) }
+ let(:tmpdir) { Dir.mktmpdir }
before do
+ FileUtils.copy_file(filepath, File.join(tmpdir, 'milestones.ndjson.gz'))
group.add_owner(user)
end
- describe '#run' do
- it 'imports group milestones' do
- first_page = extracted_data(title: 'milestone1', iid: 1, has_next_page: true)
- last_page = extracted_data(title: 'milestone2', iid: 2)
+ subject { described_class.new(context) }
- allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
- allow(extractor)
- .to receive(:extract)
- .and_return(first_page, last_page)
+ describe '#run' do
+ it 'imports group milestones into destination group and removes tmpdir' do
+ allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
+ allow_next_instance_of(BulkImports::FileDownloadService) do |service|
+ allow(service).to receive(:execute)
end
- expect { subject.run }.to change(Milestone, :count).by(2)
-
- expect(group.milestones.pluck(:title)).to contain_exactly('milestone1', 'milestone2')
-
- milestone = group.milestones.last
-
- expect(milestone.description).to eq('desc')
- expect(milestone.state).to eq('closed')
- expect(milestone.start_date.to_s).to eq('2020-10-21')
- expect(milestone.due_date.to_s).to eq('2020-10-22')
- expect(milestone.created_at).to eq(timestamp)
- expect(milestone.updated_at).to eq(timestamp)
+ expect { subject.run }.to change(Milestone, :count).by(5)
+ expect(group.milestones.pluck(:title)).to contain_exactly('v4.0', 'v3.0', 'v2.0', 'v1.0', 'v0.0')
+ expect(File.directory?(tmpdir)).to eq(false)
end
end
describe '#load' do
- it 'creates the milestone' do
- data = milestone_data('milestone')
-
- expect { subject.load(context, data) }.to change(Milestone, :count).by(1)
- end
-
- context 'when user is not authorized to create the milestone' do
- before do
- allow(user).to receive(:can?).with(:admin_milestone, group).and_return(false)
- end
+ context 'when milestone is not persisted' do
+ it 'saves the milestone' do
+ milestone = build(:milestone, group: group)
- it 'raises NotAllowedError' do
- data = extracted_data(title: 'milestone')
+ expect(milestone).to receive(:save!)
- expect { subject.load(context, data) }.to raise_error(::BulkImports::Pipeline::NotAllowedError)
+ subject.load(context, milestone)
end
end
- end
- describe 'pipeline parts' do
- it { expect(described_class).to include_module(BulkImports::Pipeline) }
- it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
+ context 'when milestone is persisted' do
+ it 'does not save milestone' do
+ milestone = create(:milestone, group: group)
- it 'has extractors' do
- expect(described_class.get_extractor)
- .to eq(
- klass: BulkImports::Common::Extractors::GraphqlExtractor,
- options: {
- query: BulkImports::Groups::Graphql::GetMilestonesQuery
- }
- )
- end
+ expect(milestone).not_to receive(:save!)
- it 'has transformers' do
- expect(described_class.transformers)
- .to contain_exactly(
- { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }
- )
+ subject.load(context, milestone)
+ end
end
- end
- def milestone_data(title, iid: 1)
- {
- 'title' => title,
- 'description' => 'desc',
- 'iid' => iid,
- 'state' => 'closed',
- 'start_date' => '2020-10-21',
- 'due_date' => '2020-10-22',
- 'created_at' => timestamp.to_s,
- 'updated_at' => timestamp.to_s
- }
- end
-
- def extracted_data(title:, iid: 1, has_next_page: false)
- page_info = {
- 'has_next_page' => has_next_page,
- 'next_page' => has_next_page ? 'cursor' : nil
- }
-
- BulkImports::Pipeline::ExtractedData.new(
- data: milestone_data(title, iid: iid),
- page_info: page_info
- )
+ context 'when milestone is missing' do
+ it 'returns' do
+ expect(subject.load(context, nil)).to be_nil
+ end
+ end
end
end
diff --git a/spec/lib/bulk_imports/ndjson_pipeline_spec.rb b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
new file mode 100644
index 00000000000..a5d1a5f7fbb
--- /dev/null
+++ b/spec/lib/bulk_imports/ndjson_pipeline_spec.rb
@@ -0,0 +1,186 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::NdjsonPipeline do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:klass) do
+ Class.new do
+ include BulkImports::NdjsonPipeline
+
+ relation_name 'test'
+
+ attr_reader :portable, :current_user
+
+ def initialize(portable, user)
+ @portable = portable
+ @current_user = user
+ end
+ end
+ end
+
+ before do
+ stub_const('NdjsonPipelineClass', klass)
+ end
+
+ subject { NdjsonPipelineClass.new(group, user) }
+
+ it 'marks pipeline as ndjson' do
+ expect(NdjsonPipelineClass.ndjson_pipeline?).to eq(true)
+ end
+
+ describe '#deep_transform_relation!' do
+ it 'transforms relation hash' do
+ transformed = subject.deep_transform_relation!({}, 'test', {}) do |key, hash|
+ hash.merge(relation_key: key)
+ end
+
+ expect(transformed[:relation_key]).to eq('test')
+ end
+
+ context 'when subrelations is an array' do
+ it 'transforms each element of the array' do
+ relation_hash = {
+ 'key' => 'value',
+ 'labels' => [
+ { 'title' => 'label 1' },
+ { 'title' => 'label 2' },
+ { 'title' => 'label 3' }
+ ]
+ }
+ relation_definition = { 'labels' => {} }
+
+ transformed = subject.deep_transform_relation!(relation_hash, 'test', relation_definition) do |key, hash|
+ hash.merge(relation_key: key)
+ end
+
+ transformed['labels'].each do |label|
+ expect(label[:relation_key]).to eq('group_labels')
+ end
+ end
+ end
+
+ context 'when subrelation is a hash' do
+ it 'transforms subrelation hash' do
+ relation_hash = {
+ 'key' => 'value',
+ 'label' => { 'title' => 'label' }
+ }
+ relation_definition = { 'label' => {} }
+
+ transformed = subject.deep_transform_relation!(relation_hash, 'test', relation_definition) do |key, hash|
+ hash.merge(relation_key: key)
+ end
+
+ expect(transformed['label'][:relation_key]).to eq('group_label')
+ end
+ end
+
+ context 'when subrelation is nil' do
+ it 'removes subrelation' do
+ relation_hash = {
+ 'key' => 'value',
+ 'label' => { 'title' => 'label' }
+ }
+ relation_definition = { 'label' => {} }
+
+ transformed = subject.deep_transform_relation!(relation_hash, 'test', relation_definition) do |key, hash|
+ if key == 'group_label'
+ nil
+ else
+ hash
+ end
+ end
+
+ expect(transformed['label']).to be_nil
+ end
+ end
+ end
+
+ describe '#transform' do
+ it 'calls relation factory' do
+ hash = { key: :value }
+ data = [hash, 1]
+ user = double
+ config = double(relation_excluded_keys: nil, top_relation_tree: [])
+ context = double(portable: group, current_user: user, import_export_config: config)
+ allow(subject).to receive(:import_export_config).and_return(config)
+
+ expect(Gitlab::ImportExport::Group::RelationFactory)
+ .to receive(:create)
+ .with(
+ relation_index: 1,
+ relation_sym: :test,
+ relation_hash: hash,
+ importable: group,
+ members_mapper: instance_of(Gitlab::ImportExport::MembersMapper),
+ object_builder: Gitlab::ImportExport::Group::ObjectBuilder,
+ user: user,
+ excluded_keys: nil
+ )
+
+ subject.transform(context, data)
+ end
+ end
+
+ describe '#load' do
+ context 'when object is not persisted' do
+ it 'saves the object' do
+ object = double(persisted?: false)
+
+ expect(object).to receive(:save!)
+
+ subject.load(nil, object)
+ end
+ end
+
+ context 'when object is persisted' do
+ it 'does not save the object' do
+ object = double(persisted?: true)
+
+ expect(object).not_to receive(:save!)
+
+ subject.load(nil, object)
+ end
+ end
+
+ context 'when object is missing' do
+ it 'returns' do
+ expect(subject.load(nil, nil)).to be_nil
+ end
+ end
+ end
+
+ describe '#relation_class' do
+ context 'when relation name is pluralized' do
+ it 'returns constantized class' do
+ expect(subject.relation_class('MergeRequest::Metrics')).to eq(MergeRequest::Metrics)
+ end
+ end
+
+ context 'when relation name is singularized' do
+ it 'returns constantized class' do
+ expect(subject.relation_class('Badge')).to eq(Badge)
+ end
+ end
+ end
+
+ describe '#relation_key_override' do
+ context 'when portable is group' do
+ it 'returns group relation name override' do
+ expect(subject.relation_key_override('labels')).to eq('group_labels')
+ end
+ end
+
+ context 'when portable is project' do
+ subject { NdjsonPipelineClass.new(project, user) }
+
+ it 'returns group relation name override' do
+ expect(subject.relation_key_override('labels')).to eq('project_labels')
+ end
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb
index 5b7711ad5d7..83d6f494d53 100644
--- a/spec/lib/bulk_imports/pipeline/context_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/context_spec.rb
@@ -6,6 +6,9 @@ RSpec.describe BulkImports::Pipeline::Context do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_entity) { create(:bulk_import_entity, :project_entity, project: project) }
+ let_it_be(:project_tracker) { create(:bulk_import_tracker, entity: project_entity) }
let_it_be(:entity) do
create(
@@ -51,4 +54,24 @@ RSpec.describe BulkImports::Pipeline::Context do
describe '#extra' do
it { expect(subject.extra).to eq(extra: :data) }
end
+
+ describe '#portable' do
+ it { expect(subject.portable).to eq(group) }
+
+ context 'when portable is project' do
+ subject { described_class.new(project_tracker) }
+
+ it { expect(subject.portable).to eq(project) }
+ end
+ end
+
+ describe '#import_export_config' do
+ it { expect(subject.import_export_config).to be_instance_of(BulkImports::FileTransfer::GroupConfig) }
+
+ context 'when portable is project' do
+ subject { described_class.new(project_tracker) }
+
+ it { expect(subject.import_export_config).to be_instance_of(BulkImports::FileTransfer::ProjectConfig) }
+ end
+ end
end
diff --git a/spec/lib/bulk_imports/pipeline_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb
index dda2e41f06c..48c265d6118 100644
--- a/spec/lib/bulk_imports/pipeline_spec.rb
+++ b/spec/lib/bulk_imports/pipeline_spec.rb
@@ -63,6 +63,7 @@ RSpec.describe BulkImports::Pipeline do
BulkImports::MyPipeline.transformer(klass, options)
BulkImports::MyPipeline.loader(klass, options)
BulkImports::MyPipeline.abort_on_failure!
+ BulkImports::MyPipeline.ndjson_pipeline!
expect(BulkImports::MyPipeline.get_extractor).to eq({ klass: klass, options: options })
@@ -74,6 +75,7 @@ RSpec.describe BulkImports::Pipeline do
expect(BulkImports::MyPipeline.get_loader).to eq({ klass: klass, options: options })
expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true)
+ expect(BulkImports::MyPipeline.ndjson_pipeline?).to eq(true)
end
end
end
diff --git a/spec/lib/bulk_imports/stage_spec.rb b/spec/lib/bulk_imports/stage_spec.rb
index 713cd3f22ab..d082faa90bc 100644
--- a/spec/lib/bulk_imports/stage_spec.rb
+++ b/spec/lib/bulk_imports/stage_spec.rb
@@ -10,7 +10,8 @@ RSpec.describe BulkImports::Stage do
[1, BulkImports::Groups::Pipelines::MembersPipeline],
[1, BulkImports::Groups::Pipelines::LabelsPipeline],
[1, BulkImports::Groups::Pipelines::MilestonesPipeline],
- [1, BulkImports::Groups::Pipelines::BadgesPipeline]
+ [1, BulkImports::Groups::Pipelines::BadgesPipeline],
+ [2, BulkImports::Groups::Pipelines::BoardsPipeline]
]
end
diff --git a/spec/lib/csv_builder_spec.rb b/spec/lib/csv_builder_spec.rb
index 546be3ba6f7..ec065ee6f7d 100644
--- a/spec/lib/csv_builder_spec.rb
+++ b/spec/lib/csv_builder_spec.rb
@@ -105,5 +105,17 @@ RSpec.describe CsvBuilder do
expect(csv_data).not_to include "'*safe_desc"
expect(csv_data).not_to include "'*safe_title"
end
+
+ context 'when dangerous characters are after a line break' do
+ it 'does not append single quote to description' do
+ fake_object = double(title: "Safe title", description: "With task list\n-[x] todo 1")
+ fake_relation = FakeRelation.new([fake_object])
+ builder = described_class.new(fake_relation, 'Title' => 'title', 'Description' => 'description')
+
+ csv_data = builder.render
+
+ expect(csv_data).to eq("Title,Description\nSafe title,\"With task list\n-[x] todo 1\"\n")
+ end
+ end
end
end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 407187ea05f..1108d26b2a9 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
RSpec.describe ExpandVariables do
shared_examples 'common variable expansion' do |expander|
@@ -231,41 +232,4 @@ RSpec.describe ExpandVariables do
end
end
end
-
- describe '#possible_var_reference?' do
- context 'table tests' do
- using RSpec::Parameterized::TableSyntax
-
- where do
- {
- "empty value": {
- value: '',
- result: false
- },
- "normal value": {
- value: 'some value',
- result: false
- },
- "simple expansions": {
- value: 'key$variable',
- result: true
- },
- "complex expansions": {
- value: 'key${variable}${variable2}',
- result: true
- },
- "complex expansions for Windows": {
- value: 'key%variable%%variable2%',
- result: true
- }
- }
- end
-
- with_them do
- subject { ExpandVariables.possible_var_reference?(value) }
-
- it { is_expected.to eq(result) }
- end
- end
- end
end
diff --git a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
index 25c4001a192..4e172dd32f0 100644
--- a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'generator_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::SnowplowEventDefinitionGenerator do
+RSpec.describe Gitlab::SnowplowEventDefinitionGenerator, :silence_stdout do
let(:ce_temp_dir) { Dir.mktmpdir }
let(:ee_temp_dir) { Dir.mktmpdir }
let(:generator_options) { { 'category' => 'Groups::EmailCampaignsController', 'action' => 'click' } }
diff --git a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
index 95a577e6334..e497551bc3f 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'generator_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator do
+RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout do
include UsageDataHelpers
let(:category) { 'test_category' }
diff --git a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
index 74aaf34e82c..05833cf4ec4 100644
--- a/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
+++ b/spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'generator_helper'
+require 'spec_helper'
-RSpec.describe Gitlab::UsageMetricDefinitionGenerator do
+RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
include UsageDataHelpers
let(:key_path) { 'counts_weekly.test_metric' }
diff --git a/spec/lib/generators/gitlab/usage_metric_generator_spec.rb b/spec/lib/generators/gitlab/usage_metric_generator_spec.rb
new file mode 100644
index 00000000000..f38815acca6
--- /dev/null
+++ b/spec/lib/generators/gitlab/usage_metric_generator_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UsageMetricGenerator, :silence_stdout do
+ let(:ce_temp_dir) { Dir.mktmpdir }
+ let(:ee_temp_dir) { Dir.mktmpdir }
+ let(:spec_ce_temp_dir) { Dir.mktmpdir }
+ let(:spec_ee_temp_dir) { Dir.mktmpdir }
+ let(:args) { ['CountFoo'] }
+ let(:options) { { 'type' => 'redis_hll' } }
+
+ before do
+ stub_const("#{described_class}::CE_DIR", ce_temp_dir)
+ stub_const("#{described_class}::EE_DIR", ee_temp_dir)
+ stub_const("#{described_class}::SPEC_CE_DIR", spec_ce_temp_dir)
+ stub_const("#{described_class}::SPEC_EE_DIR", spec_ee_temp_dir)
+ end
+
+ after do
+ FileUtils.rm_rf([ce_temp_dir, ee_temp_dir, spec_ce_temp_dir, spec_ee_temp_dir])
+ end
+
+ def expect_generated_file(directory, file_name, content)
+ file_path = File.join(directory, file_name)
+ file = File.read(file_path)
+
+ expect(file).to eq(content)
+ end
+
+ describe 'Creating metric instrumentation files' do
+ let(:sample_metric_dir) { 'lib/generators/gitlab/usage_metric_generator' }
+ let(:sample_metric) { fixture_file(File.join(sample_metric_dir, 'sample_metric.rb')) }
+ let(:sample_spec) { fixture_file(File.join(sample_metric_dir, 'sample_metric_test.rb')) }
+
+ it 'creates CE metric instrumentation files using the template' do
+ described_class.new(args, options).invoke_all
+
+ expect_generated_file(ce_temp_dir, 'count_foo_metric.rb', sample_metric)
+ expect_generated_file(spec_ce_temp_dir, 'count_foo_metric_spec.rb', sample_spec)
+ end
+
+ context 'with EE flag true' do
+ let(:options) { { 'type' => 'redis_hll', 'ee' => true } }
+
+ it 'creates EE metric instrumentation files using the template' do
+ described_class.new(args, options).invoke_all
+
+ expect_generated_file(ee_temp_dir, 'count_foo_metric.rb', sample_metric)
+ expect_generated_file(spec_ee_temp_dir, 'count_foo_metric_spec.rb', sample_spec)
+ end
+ end
+
+ context 'with type option missing' do
+ let(:options) { {} }
+
+ it 'raises an ArgumentError' do
+ expect { described_class.new(args, options).invoke_all }.to raise_error(ArgumentError, /Type is required/)
+ end
+ end
+
+ context 'with type option value not included in approved superclasses' do
+ let(:options) { { 'type' => 'some_other_type' } }
+
+ it 'raises an ArgumentError' do
+ expect { described_class.new(args, options).invoke_all }.to raise_error(ArgumentError, /Unknown type 'some_other_type'/)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb
index c4fe2ebaba9..ecd68caba79 100644
--- a/spec/lib/gitlab/application_context_spec.rb
+++ b/spec/lib/gitlab/application_context_spec.rb
@@ -68,6 +68,24 @@ RSpec.describe Gitlab::ApplicationContext do
end
end
+ describe '.current_context_attribute' do
+ it 'returns the raw attribute value' do
+ described_class.with_context(caller_id: "Hello") do
+ expect(described_class.current_context_attribute(:caller_id)).to be('Hello')
+ end
+ end
+
+ it 'returns the attribute value with meta prefix' do
+ described_class.with_context(feature_category: "secure") do
+ expect(described_class.current_context_attribute('meta.feature_category')).to be('secure')
+ end
+ end
+
+ it 'returns nil if the key was not present in the current context' do
+ expect(described_class.current_context_attribute(:caller_id)).to be(nil)
+ end
+ end
+
describe '#to_lazy_hash' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index cddcaf09b74..7475ed2796f 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
shared_examples 'find user from job token' do |without_job_token_allowed|
- context 'when route is allowed to be authenticated' do
+ context 'when route is allowed to be authenticated', :request_store do
let(:route_authentication_setting) { { job_token_allowed: true } }
context 'for an invalid token' do
@@ -68,6 +68,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do
it 'return user' do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
+ expect(subject).to be_from_ci_job_token
+ expect(subject.ci_job_token_scope.source_project).to eq(job.project)
end
end
@@ -81,7 +83,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
end
end
- context 'when route is not allowed to be authenticated' do
+ context 'when route is not allowed to be authenticated', :request_store do
let(:route_authentication_setting) { { job_token_allowed: false } }
context 'with a running job' do
@@ -96,6 +98,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do
it 'returns the user' do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
+ expect(subject).to be_from_ci_job_token
+ expect(subject.ci_job_token_scope.source_project).to eq(job.project)
end
else
it 'returns nil' do
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 7f06e66ad50..d529d4a96e1 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -196,8 +196,8 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
it 'recognizes other ci services' do
- project.create_drone_ci_service(active: true)
- project.drone_ci_service.update(token: 'token')
+ project.create_drone_ci_integration(active: true)
+ project.drone_ci_integration.update(token: 'token')
expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, described_class.build_authentication_abilities))
end
@@ -683,6 +683,28 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
end
+ describe '#build_access_token_check' do
+ subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: build.project, ip: '1.2.3.4') }
+
+ let_it_be(:user) { create(:user) }
+
+ context 'for running build' do
+ let!(:build) { create(:ci_build, :running, user: user) }
+
+ it 'executes query using primary database' do
+ expect(Ci::Build).to receive(:find_by_token).with(build.token).and_wrap_original do |m, *args|
+ expect(::Gitlab::Database::LoadBalancing::Session.current.use_primary?).to eq(true)
+ m.call(*args)
+ end
+
+ expect(subject).to be_a(Gitlab::Auth::Result)
+ expect(subject.actor).to eq(user)
+ expect(subject.project).to eq(build.project)
+ expect(subject.type).to eq(:build)
+ end
+ end
+ end
+
describe 'find_with_user_password' do
let!(:user) do
create(:user,
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
new file mode 100644
index 00000000000..8a3671b2e53
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::CleanupOrphanedLfsObjectsProjects, schema: 20210514063252 do
+ let(:lfs_objects_projects) { table(:lfs_objects_projects) }
+ let(:lfs_objects) { table(:lfs_objects) }
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+
+ let(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
+ let(:another_project) { projects.create!(namespace_id: namespace.id) }
+ let(:lfs_object) { lfs_objects.create!(oid: 'abcdef', size: 1) }
+ let(:another_lfs_object) { lfs_objects.create!(oid: '1abcde', size: 2) }
+
+ let!(:without_object1) { create_object(project_id: project.id) }
+ let!(:without_object2) { create_object(project_id: another_project.id) }
+ let!(:without_object3) { create_object(project_id: another_project.id) }
+ let!(:with_project_and_object1) { create_object(project_id: project.id, lfs_object_id: lfs_object.id) }
+ let!(:with_project_and_object2) { create_object(project_id: project.id, lfs_object_id: another_lfs_object.id) }
+ let!(:with_project_and_object3) { create_object(project_id: another_project.id, lfs_object_id: another_lfs_object.id) }
+ let!(:without_project1) { create_object(lfs_object_id: lfs_object.id) }
+ let!(:without_project2) { create_object(lfs_object_id: another_lfs_object.id) }
+ let!(:without_project_and_object) { create_object }
+
+ def create_object(project_id: non_existing_record_id, lfs_object_id: non_existing_record_id)
+ lfs_objects_project = nil
+
+ ActiveRecord::Base.connection.disable_referential_integrity do
+ lfs_objects_project = lfs_objects_projects.create!(project_id: project_id, lfs_object_id: lfs_object_id)
+ end
+
+ lfs_objects_project
+ end
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'lfs_objects_projects without an existing lfs object or project are removed' do
+ subject.perform(without_object1.id, without_object3.id)
+
+ expect(lfs_objects_projects.all).to match_array([
+ with_project_and_object1, with_project_and_object2, with_project_and_object3,
+ without_project1, without_project2, without_project_and_object
+ ])
+
+ subject.perform(with_project_and_object1.id, with_project_and_object3.id)
+
+ expect(lfs_objects_projects.all).to match_array([
+ with_project_and_object1, with_project_and_object2, with_project_and_object3,
+ without_project1, without_project2, without_project_and_object
+ ])
+
+ subject.perform(without_project1.id, without_project_and_object.id)
+
+ expect(lfs_objects_projects.all).to match_array([
+ with_project_and_object1, with_project_and_object2, with_project_and_object3
+ ])
+
+ expect(lfs_objects.ids).to contain_exactly(lfs_object.id, another_lfs_object.id)
+ expect(projects.ids).to contain_exactly(project.id, another_project.id)
+ end
+
+ it 'cache for affected projects is being reset' do
+ expect(ProjectCacheWorker).to receive(:bulk_perform_in) do |delay, args|
+ expect(delay).to eq(1.minute)
+ expect(args).to match_array([[project.id, [], [:lfs_objects_size]], [another_project.id, [], [:lfs_objects_size]]])
+ end
+
+ subject.perform(without_object1.id, with_project_and_object1.id)
+
+ expect(ProjectCacheWorker).not_to receive(:bulk_perform_in)
+
+ subject.perform(with_project_and_object1.id, with_project_and_object3.id)
+
+ expect(ProjectCacheWorker).not_to receive(:bulk_perform_in)
+
+ subject.perform(without_project1.id, without_project_and_object.id)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb b/spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb
new file mode 100644
index 00000000000..04eb9ad475f
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::DisableExpirationPoliciesLinkedToNoContainerImages do
+ let_it_be(:projects) { table(:projects) }
+ let_it_be(:container_expiration_policies) { table(:container_expiration_policies) }
+ let_it_be(:container_repositories) { table(:container_repositories) }
+ let_it_be(:namespaces) { table(:namespaces) }
+
+ let!(:namespace) { namespaces.create!(name: 'test', path: 'test') }
+
+ let!(:policy1) { create_expiration_policy(project_id: 1, enabled: true) }
+ let!(:policy2) { create_expiration_policy(project_id: 2, enabled: false) }
+ let!(:policy3) { create_expiration_policy(project_id: 3, enabled: false) }
+ let!(:policy4) { create_expiration_policy(project_id: 4, enabled: true, with_images: true) }
+ let!(:policy5) { create_expiration_policy(project_id: 5, enabled: false, with_images: true) }
+ let!(:policy6) { create_expiration_policy(project_id: 6, enabled: false) }
+ let!(:policy7) { create_expiration_policy(project_id: 7, enabled: true) }
+ let!(:policy8) { create_expiration_policy(project_id: 8, enabled: true, with_images: true) }
+ let!(:policy9) { create_expiration_policy(project_id: 9, enabled: true) }
+
+ describe '#perform' do
+ subject { described_class.new.perform(from_id, to_id) }
+
+ shared_examples 'disabling policies with no images' do
+ it 'disables the proper policies' do
+ subject
+
+ rows = container_expiration_policies.order(:project_id).to_h do |row|
+ [row.project_id, row.enabled]
+ end
+ expect(rows).to eq(expected_rows)
+ end
+ end
+
+ context 'the whole range' do
+ let(:from_id) { 1 }
+ let(:to_id) { 9 }
+
+ it_behaves_like 'disabling policies with no images' do
+ let(:expected_rows) do
+ {
+ 1 => false,
+ 2 => false,
+ 3 => false,
+ 4 => true,
+ 5 => false,
+ 6 => false,
+ 7 => false,
+ 8 => true,
+ 9 => false
+ }
+ end
+ end
+ end
+
+ context 'a range with no policies to disable' do
+ let(:from_id) { 2 }
+ let(:to_id) { 6 }
+
+ it_behaves_like 'disabling policies with no images' do
+ let(:expected_rows) do
+ {
+ 1 => true,
+ 2 => false,
+ 3 => false,
+ 4 => true,
+ 5 => false,
+ 6 => false,
+ 7 => true,
+ 8 => true,
+ 9 => true
+ }
+ end
+ end
+ end
+
+ context 'a range with only images' do
+ let(:from_id) { 4 }
+ let(:to_id) { 5 }
+
+ it_behaves_like 'disabling policies with no images' do
+ let(:expected_rows) do
+ {
+ 1 => true,
+ 2 => false,
+ 3 => false,
+ 4 => true,
+ 5 => false,
+ 6 => false,
+ 7 => true,
+ 8 => true,
+ 9 => true
+ }
+ end
+ end
+ end
+
+ context 'a range with a single element' do
+ let(:from_id) { 9 }
+ let(:to_id) { 9 }
+
+ it_behaves_like 'disabling policies with no images' do
+ let(:expected_rows) do
+ {
+ 1 => true,
+ 2 => false,
+ 3 => false,
+ 4 => true,
+ 5 => false,
+ 6 => false,
+ 7 => true,
+ 8 => true,
+ 9 => false
+ }
+ end
+ end
+ end
+ end
+
+ def create_expiration_policy(project_id:, enabled:, with_images: false)
+ projects.create!(id: project_id, namespace_id: namespace.id, name: "gitlab-#{project_id}")
+
+ if with_images
+ container_repositories.create!(project_id: project_id, name: "image-#{project_id}")
+ end
+
+ container_expiration_policies.create!(
+ enabled: enabled,
+ project_id: project_id
+ )
+ 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/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
index 8668216d014..80879c8c6d9 100644
--- a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
@@ -291,7 +291,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
services.create!(id: 20, type: 'JiraService', properties: jira_properties.to_json, category: 'issue_tracker')
end
- let!(:bugzilla_service_valid) do
+ let!(:bugzilla_integration_valid) do
services.create!(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker')
end
@@ -314,14 +314,14 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s
expect(jira_service_valid.title).to eq(title)
expect(jira_service_valid.description).to eq(description)
- bugzilla_service_valid.reload
- data = IssueTrackerData.find_by(service_id: bugzilla_service_valid.id)
+ bugzilla_integration_valid.reload
+ data = IssueTrackerData.find_by(service_id: bugzilla_integration_valid.id)
expect(data.project_url).to eq(url)
expect(data.issues_url).to eq(issues_url)
expect(data.new_issue_url).to eq(new_issue_url)
- expect(bugzilla_service_valid.title).to eq(title)
- expect(bugzilla_service_valid.description).to eq(description)
+ expect(bugzilla_integration_valid.title).to eq(title)
+ expect(bugzilla_integration_valid.description).to eq(description)
end
end
end
diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
index 990ef4fbe6a..70906961641 100644
--- a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
+++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -73,6 +73,14 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
expect(vulnerabilities_findings.pluck(:uuid)).to eq([desired_uuid_v5])
end
+
+ it 'logs recalculation' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:info).once
+ end
+
+ subject
+ end
end
context "when finding has a UUIDv5" do
@@ -99,6 +107,32 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrence
end
end
+ context 'when recalculation fails' do
+ before do
+ @uuid_v4 = create_finding!(
+ vulnerability_id: vulnerability_for_uuidv4.id,
+ project_id: project.id,
+ scanner_id: different_scanner.id,
+ primary_identifier_id: different_vulnerability_identifier.id,
+ report_type: 0, # "sast"
+ location_fingerprint: "fa18f432f1d56675f4098d318739c3cd5b14eb3e",
+ uuid: known_uuid_v4
+ )
+
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
+ allow(::Gitlab::Database::BulkUpdate).to receive(:execute).and_raise(expected_error)
+ end
+
+ let(:finding) { @uuid_v4 }
+ let(:expected_error) { RuntimeError.new }
+
+ it 'captures the errors and does not crash entirely' do
+ expect { subject }.not_to raise_error
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(expected_error).once
+ end
+ end
+
private
def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
diff --git a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb
new file mode 100644
index 00000000000..f7466a2ddfd
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do
+ let(:services_table) { table(:services) }
+ let(:service_jira_cloud) { services_table.create!(id: 1, type: 'JiraService') }
+ let(:service_jira_server) { services_table.create!(id: 2, type: 'JiraService') }
+
+ before do
+ jira_tracker_data = Class.new(ApplicationRecord) do
+ self.table_name = 'jira_tracker_data'
+
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :username, encryption_options
+ attr_encrypted :password, encryption_options
+ end
+
+ stub_const('JiraTrackerData', jira_tracker_data)
+ end
+
+ let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: 0) }
+ let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: 0) }
+
+ subject { described_class.new.perform(tracker_data_cloud.id, tracker_data_server.id) }
+
+ it "changes unknown deployment_types based on URL" do
+ expect(JiraTrackerData.pluck(:deployment_type)).to eq([0, 0])
+
+ subject
+
+ expect(JiraTrackerData.pluck(:deployment_type)).to eq([2, 1])
+ end
+end
diff --git a/spec/lib/gitlab/cache/import/caching_spec.rb b/spec/lib/gitlab/cache/import/caching_spec.rb
index d6911dad9d4..8ce12f5d32e 100644
--- a/spec/lib/gitlab/cache/import/caching_spec.rb
+++ b/spec/lib/gitlab/cache/import/caching_spec.rb
@@ -88,6 +88,18 @@ RSpec.describe Gitlab::Cache::Import::Caching, :clean_gitlab_redis_cache do
end
end
+ describe '.values_from_set' do
+ it 'returns empty list when the set is empty' do
+ expect(described_class.values_from_set('foo')).to eq([])
+ end
+
+ it 'returns the set list of values' do
+ described_class.set_add('foo', 10)
+
+ expect(described_class.values_from_set('foo')).to eq(['10'])
+ end
+ end
+
describe '.write_multiple' do
it 'sets multiple keys when key_prefix not set' do
mapping = { 'foo' => 10, 'bar' => 20 }
diff --git a/spec/lib/gitlab/cache_spec.rb b/spec/lib/gitlab/cache_spec.rb
index 5b1034a77a3..67c70a77880 100644
--- a/spec/lib/gitlab/cache_spec.rb
+++ b/spec/lib/gitlab/cache_spec.rb
@@ -26,4 +26,16 @@ RSpec.describe Gitlab::Cache, :request_store do
expect(subject.call).to eq("return value")
end
end
+
+ describe '.delete' do
+ let(:key) { %w{a cache key} }
+
+ subject(:delete) { described_class.delete(key) }
+
+ it 'calls Rails.cache.delete' do
+ expect(Rails.cache).to receive(:delete).with(key)
+
+ delete
+ end
+ end
end
diff --git a/spec/lib/gitlab/checks/changes_access_spec.rb b/spec/lib/gitlab/checks/changes_access_spec.rb
new file mode 100644
index 00000000000..a46732f8255
--- /dev/null
+++ b/spec/lib/gitlab/checks/changes_access_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Checks::ChangesAccess do
+ describe '#validate!' do
+ include_context 'changes access checks context'
+
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
+ end
+
+ subject { changes_access }
+
+ context 'without failed checks' do
+ it "doesn't raise an error" do
+ expect { subject.validate! }.not_to raise_error
+ end
+
+ it 'calls lfs checks' do
+ expect_next_instance_of(Gitlab::Checks::LfsCheck) do |instance|
+ expect(instance).to receive(:validate!)
+ end
+
+ subject.validate!
+ end
+ end
+
+ context 'when time limit was reached' do
+ it 'raises a TimeoutError' do
+ logger = Gitlab::Checks::TimedLogger.new(start_time: timeout.ago, timeout: timeout)
+ access = described_class.new(changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger)
+
+ expect { access.validate! }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/lfs_check_spec.rb b/spec/lib/gitlab/checks/lfs_check_spec.rb
index 19c1d820dff..331a43b814f 100644
--- a/spec/lib/gitlab/checks/lfs_check_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_check_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Checks::LfsCheck do
- include_context 'change access checks context'
+ include_context 'changes access checks context'
let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
@@ -15,6 +15,10 @@ RSpec.describe Gitlab::Checks::LfsCheck do
describe '#validate!' do
context 'with LFS not enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+ end
+
it 'skips integrity check' do
expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
@@ -51,13 +55,13 @@ RSpec.describe Gitlab::Checks::LfsCheck do
context 'with missing newrev' do
it_behaves_like 'a skipped integrity check' do
- let(:changes) { { oldrev: oldrev, ref: ref } }
+ let(:changes) { [{ oldrev: oldrev, ref: ref }] }
end
end
context 'with blank newrev' do
it_behaves_like 'a skipped integrity check' do
- let(:changes) { { oldrev: oldrev, newrev: Gitlab::Git::BLANK_SHA, ref: ref } }
+ let(:changes) { [{ oldrev: oldrev, newrev: Gitlab::Git::BLANK_SHA, ref: ref }] }
end
end
end
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index 4583cd72cfd..3468094ffa5 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -18,12 +18,18 @@ RSpec.describe Gitlab::Checks::LfsIntegrity do
operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects")
end
- subject { described_class.new(project, newrev, time_left) }
+ let(:newrevs) { [newrev] }
+
+ subject { described_class.new(project, newrevs, time_left) }
describe '#objects_missing?' do
let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
context 'with LFS not enabled' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+ end
+
it 'skips integrity check' do
expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
@@ -36,8 +42,28 @@ RSpec.describe Gitlab::Checks::LfsIntegrity do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
+ context 'nil rev' do
+ let(:newrevs) { [nil] }
+
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
context 'deletion' do
- let(:newrev) { nil }
+ let(:newrevs) { [Gitlab::Git::BLANK_SHA] }
+
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
+
+ expect(subject.objects_missing?).to be_falsey
+ end
+ end
+
+ context 'no changes' do
+ let(:newrevs) { [] }
it 'skips integrity check' do
expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
diff --git a/spec/lib/gitlab/checks/matching_merge_request_spec.rb b/spec/lib/gitlab/checks/matching_merge_request_spec.rb
index ca7ee784ee3..feda488a936 100644
--- a/spec/lib/gitlab/checks/matching_merge_request_spec.rb
+++ b/spec/lib/gitlab/checks/matching_merge_request_spec.rb
@@ -18,6 +18,9 @@ RSpec.describe Gitlab::Checks::MatchingMergeRequest do
subject { described_class.new(newrev, target_branch, project) }
+ let(:total_counter) { subject.send(:total_counter) }
+ let(:stale_counter) { subject.send(:stale_counter) }
+
it 'matches a merge request' do
expect(subject.match?).to be true
end
@@ -27,5 +30,70 @@ RSpec.describe Gitlab::Checks::MatchingMergeRequest do
expect(matcher.match?).to be false
end
+
+ context 'with load balancing disabled', :request_store, :redis do
+ before do
+ expect(::Gitlab::Database::LoadBalancing).to receive(:enable?).at_least(:once).and_return(false)
+ expect(::Gitlab::Database::LoadBalancing::Sticking).not_to receive(:unstick_or_continue_sticking)
+ expect(::Gitlab::Database::LoadBalancing::Sticking).not_to receive(:select_valid_replicas)
+ end
+
+ it 'does not attempt to stick to primary' do
+ expect(subject.match?).to be true
+ end
+
+ it 'increments no counters' do
+ expect { subject.match? }
+ .to change { total_counter.get }.by(0)
+ .and change { stale_counter.get }.by(0)
+ end
+ end
+
+ context 'with load balancing enabled', :request_store, :redis do
+ let(:session) { ::Gitlab::Database::LoadBalancing::Session.current }
+ let(:all_caught_up) { true }
+
+ before do
+ expect(::Gitlab::Database::LoadBalancing).to receive(:enable?).at_least(:once).and_return(true)
+ allow(::Gitlab::Database::LoadBalancing::Sticking).to receive(:all_caught_up?).and_return(all_caught_up)
+
+ expect(::Gitlab::Database::LoadBalancing::Sticking).to receive(:select_valid_host).with(:project, project.id).and_call_original
+ allow(::Gitlab::Database::LoadBalancing::Sticking).to receive(:select_caught_up_replicas).with(:project, project.id).and_return(all_caught_up)
+ end
+
+ shared_examples 'secondary that has caught up to a primary' do
+ it 'continues to use the secondary' do
+ expect(session.use_primary?).to be false
+ expect(subject.match?).to be true
+ end
+
+ it 'only increments total counter' do
+ expect { subject.match? }
+ .to change { total_counter.get }.by(1)
+ .and change { stale_counter.get }.by(0)
+ end
+ end
+
+ shared_examples 'secondary that is lagging primary' do
+ it 'sticks to the primary' do
+ expect(subject.match?).to be true
+ expect(session.use_primary?).to be true
+ end
+
+ it 'increments both total and stale counters' do
+ expect { subject.match? }
+ .to change { total_counter.get }.by(1)
+ .and change { stale_counter.get }.by(1)
+ end
+ end
+
+ it_behaves_like 'secondary that has caught up to a primary'
+
+ context 'on secondary behind primary' do
+ let(:all_caught_up) { false }
+
+ it_behaves_like 'secondary that is lagging primary'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/single_change_access_spec.rb
index 6f82dabb285..8b235005b3e 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/single_change_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Checks::ChangeAccess do
+RSpec.describe Gitlab::Checks::SingleChangeAccess do
describe '#validate!' do
include_context 'change access checks context'
@@ -37,14 +37,6 @@ RSpec.describe Gitlab::Checks::ChangeAccess do
subject.validate!
end
- it 'calls lfs checks' do
- expect_next_instance_of(Gitlab::Checks::LfsCheck) do |instance|
- expect(instance).to receive(:validate!)
- end
-
- subject.validate!
- end
-
it 'calls diff checks' do
expect_next_instance_of(Gitlab::Checks::DiffCheck) do |instance|
expect(instance).to receive(:validate!)
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
index d681447a0e8..909c0f1b3ea 100644
--- a/spec/lib/gitlab/ci/ansi2json/line_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -76,10 +76,30 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
end
describe '#set_section_duration' do
- it 'sets and formats the section_duration' do
- subject.set_section_duration(75)
+ shared_examples 'set_section_duration' do
+ it 'sets and formats the section_duration' do
+ subject.set_section_duration(75)
- expect(subject.section_duration).to eq('01:15')
+ expect(subject.section_duration).to eq('01:15')
+ end
+ end
+
+ context 'with default timezone' do
+ it_behaves_like 'set_section_duration'
+ end
+
+ context 'with a timezone carrying minutes offset' do
+ before do
+ # The actual call by does use Time.at(...).utc that the following
+ # rubocop rule (Rails/TimeZone) suggests, but for this specific
+ # test's purposes we needed to mock at the Time.at call point.
+
+ # rubocop:disable Rails/TimeZone
+ allow(Time).to receive(:at).with(75).and_return(Time.at(75, in: '+05:30'))
+ # rubocop:enable Rails/TimeZone
+ end
+
+ it_behaves_like 'set_section_duration'
end
end
diff --git a/spec/lib/gitlab/ci/badge/coverage/template_spec.rb b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb
index f010d1bce50..b03ca3c93ca 100644
--- a/spec/lib/gitlab/ci/badge/coverage/template_spec.rb
+++ b/spec/lib/gitlab/ci/badge/coverage/template_spec.rb
@@ -6,31 +6,7 @@ RSpec.describe Gitlab::Ci::Badge::Coverage::Template do
let(:badge) { double(entity: 'coverage', status: 90.00, customization: {}) }
let(:template) { described_class.new(badge) }
- describe '#key_text' do
- it 'says coverage by default' do
- expect(template.key_text).to eq 'coverage'
- end
-
- context 'when custom key_text is defined' do
- before do
- allow(badge).to receive(:customization).and_return({ key_text: "custom text" })
- end
-
- it 'returns custom value' do
- expect(template.key_text).to eq "custom text"
- end
-
- context 'when its size is larger than the max allowed value' do
- before do
- allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 })
- end
-
- it 'returns default value' do
- expect(template.key_text).to eq 'coverage'
- end
- end
- end
- end
+ it_behaves_like 'a badge template', 'coverage'
describe '#value_text' do
context 'when coverage is known' do
@@ -60,32 +36,6 @@ RSpec.describe Gitlab::Ci::Badge::Coverage::Template do
end
end
- describe '#key_width' do
- it 'is fixed by default' do
- expect(template.key_width).to eq 62
- end
-
- context 'when custom key_width is defined' do
- before do
- allow(badge).to receive(:customization).and_return({ key_width: 101 })
- end
-
- it 'returns custom value' do
- expect(template.key_width).to eq 101
- end
-
- context 'when it is larger than the max allowed value' do
- before do
- allow(badge).to receive(:customization).and_return({ key_width: 513 })
- end
-
- it 'returns default value' do
- expect(template.key_width).to eq 62
- end
- end
- end
- end
-
describe '#value_width' do
context 'when coverage is known' do
it 'is narrower when coverage is known' do
diff --git a/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb
index 696bb62b4d6..9392ccef147 100644
--- a/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb
+++ b/spec/lib/gitlab/ci/badge/pipeline/template_spec.rb
@@ -6,31 +6,7 @@ RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do
let(:badge) { double(entity: 'pipeline', status: 'success', customization: {}) }
let(:template) { described_class.new(badge) }
- describe '#key_text' do
- it 'says pipeline by default' do
- expect(template.key_text).to eq 'pipeline'
- end
-
- context 'when custom key_text is defined' do
- before do
- allow(badge).to receive(:customization).and_return({ key_text: 'custom text' })
- end
-
- it 'returns custom value' do
- expect(template.key_text).to eq 'custom text'
- end
-
- context 'when its size is larger than the max allowed value' do
- before do
- allow(badge).to receive(:customization).and_return({ key_text: 't' * 65 })
- end
-
- it 'returns default value' do
- expect(template.key_text).to eq 'pipeline'
- end
- end
- end
- end
+ it_behaves_like 'a badge template', 'pipeline'
describe '#value_text' do
it 'is status value' do
@@ -38,32 +14,6 @@ RSpec.describe Gitlab::Ci::Badge::Pipeline::Template do
end
end
- describe '#key_width' do
- it 'is fixed by default' do
- expect(template.key_width).to eq 62
- end
-
- context 'when custom key_width is defined' do
- before do
- allow(badge).to receive(:customization).and_return({ key_width: 101 })
- end
-
- it 'returns custom value' do
- expect(template.key_width).to eq 101
- end
-
- context 'when it is larger than the max allowed value' do
- before do
- allow(badge).to receive(:customization).and_return({ key_width: 513 })
- end
-
- it 'returns default value' do
- expect(template.key_width).to eq 62
- end
- end
- end
- end
-
describe 'widths and text anchors' do
it 'has fixed width and text anchors' do
expect(template.width).to eq 116
diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
index cfa8c9cd938..b107553bbce 100644
--- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb
+++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do
describe '#allowed?' do
using RSpec::Parameterized::TableSyntax
- let(:build) { create(:ci_build) }
+ let(:build) { build_stubbed(:ci_build) }
subject { auto_retry.allowed? }
@@ -22,6 +22,8 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do
"not matching reason" | 0 | { when: %w[script_error], max: 2 } | :api_failure | false
"scheduler failure override" | 1 | { when: %w[scheduler_failure], max: 1 } | :scheduler_failure | false
"default for scheduler failure" | 1 | {} | :scheduler_failure | true
+ "quota is exceeded" | 0 | { max: 2 } | :ci_quota_exceeded | false
+ "no matching runner" | 0 | { max: 2 } | :no_matching_runner | false
end
with_them do
diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb
index a0a5dd52ad4..ab2e8d4db78 100644
--- a/spec/lib/gitlab/ci/config/entry/need_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb
@@ -25,16 +25,6 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do
it 'returns job needs configuration' do
expect(need.value).to eq(name: 'job_name', artifacts: true, optional: false)
end
-
- context 'when the FF ci_needs_optional is disabled' do
- before do
- stub_feature_flags(ci_needs_optional: false)
- end
-
- it 'returns job needs configuration without `optional`' do
- expect(need.value).to eq(name: 'job_name', artifacts: true)
- end
- end
end
it_behaves_like 'job type'
@@ -134,16 +124,6 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do
it 'returns job needs configuration' do
expect(need.value).to eq(name: 'job_name', artifacts: true, optional: true)
end
-
- context 'when the FF ci_needs_optional is disabled' do
- before do
- stub_feature_flags(ci_needs_optional: false)
- end
-
- it 'returns job needs configuration without `optional`' do
- expect(need.value).to eq(name: 'job_name', artifacts: true)
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index 016d59e98b9..f98a6a869d6 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -271,10 +271,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
context 'when workflow rules is not used' do
let(:workflow) { double('workflow', 'has_rules?' => false) }
- let(:feature_flag_value) { true }
before do
- stub_feature_flags(ci_raise_job_rules_without_workflow_rules_warning: feature_flag_value)
entry.compose!(deps)
end
@@ -298,12 +296,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
it 'raises a warning' do
expect(entry.warnings).to contain_exactly(/may allow multiple pipelines/)
end
-
- context 'when feature flag is disabled' do
- let(:feature_flag_value) { false }
-
- it_behaves_like 'has no warnings'
- end
end
context 'and its value is `never`' do
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 98105ebcd55..d8907f7015b 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -41,7 +41,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do
:dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json'
:dast | 'gl-dast-report.json'
- :license_management | 'gl-license-management-report.json'
:license_scanning | 'gl-license-scanning-report.json'
:performance | 'performance.json'
:browser_performance | 'browser-performance.json'
diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb
index 480a4a05379..b0d6f5adfb1 100644
--- a/spec/lib/gitlab/ci/jwt_spec.rb
+++ b/spec/lib/gitlab/ci/jwt_spec.rb
@@ -42,6 +42,7 @@ RSpec.describe Gitlab::Ci::Jwt do
expect(payload[:user_email]).to eq(user.email)
expect(payload[:user_login]).to eq(user.username)
expect(payload[:pipeline_id]).to eq(pipeline.id.to_s)
+ expect(payload[:pipeline_source]).to eq(pipeline.source.to_s)
expect(payload[:job_id]).to eq(build.id.to_s)
expect(payload[:ref]).to eq(pipeline.source_ref)
expect(payload[:ref_protected]).to eq(build.protected.to_s)
diff --git a/spec/lib/gitlab/ci/matching/build_matcher_spec.rb b/spec/lib/gitlab/ci/matching/build_matcher_spec.rb
new file mode 100644
index 00000000000..f12e85da9c2
--- /dev/null
+++ b/spec/lib/gitlab/ci/matching/build_matcher_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Ci::Matching::BuildMatcher do
+ let(:dummy_attributes) do
+ {
+ protected: true,
+ tag_list: %w[tag1 tag2],
+ build_ids: [1, 2, 3],
+ project: :my_project
+ }
+ end
+
+ subject(:matcher) { described_class.new(attributes) }
+
+ describe '.new' do
+ context 'when attributes are missing' do
+ let(:attributes) { {} }
+
+ it { expect { matcher }.to raise_error(KeyError) }
+ end
+
+ context 'with attributes' do
+ let(:attributes) { dummy_attributes }
+
+ it { expect(matcher.protected).to eq(true) }
+
+ it { expect(matcher.tag_list).to eq(%w[tag1 tag2]) }
+
+ it { expect(matcher.build_ids).to eq([1, 2, 3]) }
+
+ it { expect(matcher.project).to eq(:my_project) }
+ end
+ end
+
+ describe '#protected?' do
+ context 'when protected is set to true' do
+ let(:attributes) { dummy_attributes }
+
+ it { expect(matcher.protected?).to be_truthy }
+ end
+
+ context 'when protected is set to false' do
+ let(:attributes) { dummy_attributes.merge(protected: false) }
+
+ it { expect(matcher.protected?).to be_falsey }
+ end
+ end
+
+ describe '#has_tags?' do
+ context 'when tags are present' do
+ let(:attributes) { dummy_attributes }
+
+ it { expect(matcher.has_tags?).to be_truthy }
+ end
+
+ context 'when tags are empty' do
+ let(:attributes) { dummy_attributes.merge(tag_list: []) }
+
+ it { expect(matcher.has_tags?).to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/matching/runner_matcher_spec.rb b/spec/lib/gitlab/ci/matching/runner_matcher_spec.rb
new file mode 100644
index 00000000000..d6492caa31a
--- /dev/null
+++ b/spec/lib/gitlab/ci/matching/runner_matcher_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Matching::RunnerMatcher do
+ let(:dummy_attributes) do
+ {
+ runner_type: 'instance_type',
+ public_projects_minutes_cost_factor: 0,
+ private_projects_minutes_cost_factor: 1,
+ run_untagged: false,
+ access_level: 'ref_protected',
+ tag_list: %w[tag1 tag2]
+ }
+ end
+
+ subject(:matcher) { described_class.new(attributes) }
+
+ describe '.new' do
+ context 'when attributes are missing' do
+ let(:attributes) { {} }
+
+ it { expect { matcher }.to raise_error(KeyError) }
+ end
+
+ context 'with attributes' do
+ let(:attributes) { dummy_attributes }
+
+ it { expect(matcher.runner_type).to eq('instance_type') }
+
+ it { expect(matcher.public_projects_minutes_cost_factor).to eq(0) }
+
+ it { expect(matcher.private_projects_minutes_cost_factor).to eq(1) }
+
+ it { expect(matcher.run_untagged).to eq(false) }
+
+ it { expect(matcher.access_level).to eq('ref_protected') }
+
+ it { expect(matcher.tag_list).to eq(%w[tag1 tag2]) }
+ end
+ end
+
+ describe '#instance_type?' do
+ let(:attributes) { dummy_attributes }
+
+ it { expect(matcher.instance_type?).to be_truthy }
+
+ context 'context with private runners' do
+ let(:attributes) { dummy_attributes.merge(runner_type: 'project_type') }
+
+ it { expect(matcher.instance_type?).to be_falsey }
+ end
+ end
+
+ describe '#matches?' do
+ let(:build) { build_stubbed(:ci_build, build_attributes) }
+ let(:runner_matcher) { described_class.new(dummy_attributes.merge(runner_attributes)) }
+
+ subject { runner_matcher.matches?(record) }
+
+ context 'with an instance of BuildMatcher' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:ref_protected, :build_protected, :run_untagged, :runner_tags, :build_tags, :result) do
+ # the `ref_protected? && !build.protected?` part:
+ true | true | true | [] | [] | true
+ true | false | true | [] | [] | false
+ false | true | true | [] | [] | true
+ false | false | true | [] | [] | true
+ # `accepting_tags?(build)` bit:
+ true | true | true | [] | [] | true
+ true | true | true | [] | ['a'] | false
+ true | true | true | %w[a b] | ['a'] | true
+ true | true | true | ['a'] | %w[a b] | false
+ true | true | true | ['a'] | ['a'] | true
+ true | true | false | ['a'] | ['a'] | true
+ true | true | false | ['b'] | ['a'] | false
+ true | true | false | %w[a b] | ['a'] | true
+ end
+
+ with_them do
+ let(:build_attributes) do
+ {
+ tag_list: build_tags,
+ protected: build_protected
+ }
+ end
+
+ let(:runner_attributes) do
+ {
+ access_level: ref_protected ? 'ref_protected' : 'not_protected',
+ run_untagged: run_untagged,
+ tag_list: runner_tags
+ }
+ end
+
+ let(:record) { build.build_matcher }
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ context 'with an instance of Ci::Build' do
+ let(:runner_attributes) { {} }
+ let(:build_attributes) { {} }
+ let(:record) { build }
+
+ it 'raises ArgumentError' do
+ expect { subject }.to raise_error ArgumentError, /BuildMatcher are allowed/
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index 7da602251a5..4ca8f74e57f 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -417,6 +417,30 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
end
end
+ context 'when attachment is specified in test case with error' do
+ let(:junit) do
+ <<~EOF
+ <testsuites>
+ <testsuite>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'>
+ <error>Some error</error>
+ <system-out>[[ATTACHMENT|some/path.png]]</system-out>
+ </testcase>
+ </testsuite>
+ </testsuites>
+ EOF
+ end
+
+ it 'assigns correct attributes to the test case' do
+ expect { subject }.not_to raise_error
+
+ expect(test_cases[0].has_attachment?).to be_truthy
+ expect(test_cases[0].attachment).to eq("some/path.png")
+
+ expect(test_cases[0].job).to eq(job)
+ end
+ end
+
private
def flattened_test_cases(test_suite)
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
index e3061f8095b..16517b39a45 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
+ let_it_be(:user) { create(:user, :with_sign_ins) }
let(:pipeline) { build(:ci_empty_pipeline, user: user, project: project) }
let!(:step) { described_class.new(pipeline, command) }
@@ -43,7 +43,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
end
let(:save_incompleted) { true }
- let(:dot_com) { true }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, current_user: user, yaml_processor_result: yaml_processor_result, save_incompleted: save_incompleted
@@ -57,7 +56,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
before do
stub_env('EXTERNAL_VALIDATION_SERVICE_URL', validation_service_url)
- allow(Gitlab).to receive(:com?).and_return(dot_com)
allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('correlation-id')
end
@@ -199,61 +197,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do
end
end
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(ci_external_validation_service: false)
- stub_request(:post, validation_service_url)
- end
-
- it 'does not drop the pipeline' do
- perform!
-
- expect(pipeline.status).not_to eq('failed')
- expect(pipeline.errors).to be_empty
- end
-
- it 'does not break the chain' do
- perform!
-
- expect(step.break?).to be false
- end
-
- it 'does not make requests' do
- perform!
-
- expect(WebMock).not_to have_requested(:post, validation_service_url)
- end
- end
-
- context 'when not on .com' do
- let(:dot_com) { false }
-
- before do
- stub_feature_flags(ci_external_validation_service: false)
- stub_request(:post, validation_service_url).to_return(status: 404, body: "{}")
- end
-
- it 'drops the pipeline' do
- perform!
-
- expect(pipeline.status).to eq('failed')
- expect(pipeline).to be_persisted
- expect(pipeline.errors.to_a).to include('External validation failed')
- end
-
- it 'breaks the chain' do
- perform!
-
- expect(step.break?).to be true
- end
-
- it 'logs the authorization' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: 'Pipeline not authorized', project_id: project.id, user_id: user.id)
-
- perform!
- end
- end
-
context 'when validation returns 406 Not Acceptable' do
before do
stub_request(:post, validation_service_url).to_return(status: 406, body: "{}")
diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
index ae423fa04f9..5b644e42451 100644
--- a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
@@ -5,9 +5,11 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Preloader do
let(:stage) { double(:stage) }
let(:commit) { double(:commit) }
+ let(:scheduled_action) { double(:scheduled_action) }
+ let(:manual_action) { double(:manual_action) }
let(:pipeline) do
- double(:pipeline, commit: commit, stages: [stage])
+ double(:pipeline, commit: commit, stages: [stage], scheduled_actions: [scheduled_action], manual_actions: [manual_action])
end
describe '.preload!' do
@@ -33,6 +35,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Preloader do
expect(pipeline).to receive(:lazy_ref_commit)
expect(pipeline).to receive(:number_of_warnings)
expect(stage).to receive(:number_of_warnings)
+ expect(scheduled_action).to receive(:persisted_environment)
+ expect(manual_action).to receive(:persisted_environment)
described_class.preload!([pipeline])
end
@@ -42,6 +46,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Preloader do
allow(pipeline).to receive(:lazy_ref_commit)
allow(pipeline).to receive(:number_of_warnings)
allow(stage).to receive(:number_of_warnings)
+ allow(scheduled_action).to receive(:persisted_environment)
+ allow(manual_action).to receive(:persisted_environment)
pipelines = [pipeline, pipeline]
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 058fb25807d..020f957cf70 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -1101,17 +1101,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
it "does not return an error" do
expect(subject.errors).to be_empty
end
-
- context 'when the FF ci_needs_optional is disabled' do
- before do
- stub_feature_flags(ci_needs_optional: false)
- end
-
- it "returns an error" do
- expect(subject.errors).to contain_exactly(
- "'rspec' job needs 'build' job, but it was not added to the pipeline")
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
index 175b12637e6..ad89f1f5cda 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
@@ -128,7 +128,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
context 'when environment has already been created' do
before do
- create(:environment, :staging, project: project, name: 'customer-portal')
+ create(:environment, project: project, name: 'customer-portal', tier: :staging)
end
it 'does not overwrite the specified deployment tier' do
diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb
index b10e2b0e057..2456c9ae545 100644
--- a/spec/lib/gitlab/ci/templates/npm_spec.rb
+++ b/spec/lib/gitlab/ci/templates/npm_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe 'npm.latest.gitlab-ci.yml' do
- subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('npm.latest') }
+RSpec.describe 'npm.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('npm') }
describe 'the created pipeline' do
let(:repo_files) { { 'package.json' => '{}', 'README.md' => '' } }
diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb
index 56443e611e8..2e6df7da232 100644
--- a/spec/lib/gitlab/ci/templates/templates_spec.rb
+++ b/spec/lib/gitlab/ci/templates/templates_spec.rb
@@ -6,26 +6,105 @@ RSpec.describe 'CI YML Templates' do
subject { Gitlab::Ci::YamlProcessor.new(content).execute }
let(:all_templates) { Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name) }
-
let(:excluded_templates) do
- all_templates.select do |name|
+ excluded = all_templates.select do |name|
Gitlab::Template::GitlabCiYmlTemplate.excluded_patterns.any? { |pattern| pattern.match?(name) }
end
+ excluded + ["Terraform.gitlab-ci.yml"]
end
- context 'when including available templates in a CI YAML configuration' do
- using RSpec::Parameterized::TableSyntax
+ before do
+ stub_feature_flags(
+ redirect_to_latest_template_terraform: false,
+ redirect_to_latest_template_security_api_fuzzing: false,
+ redirect_to_latest_template_security_dast: false)
+ end
- where(:template_name) do
- all_templates - excluded_templates
+ shared_examples 'require default stages to be included' do
+ it 'require default stages to be included' do
+ expect(subject.stages).to include(*Gitlab::Ci::Config::Entry::Stages.default)
end
+ end
+
+ context 'that support autodevops' do
+ non_autodevops_templates = [
+ 'Security/DAST-API.gitlab-ci.yml',
+ 'Security/API-Fuzzing.gitlab-ci.yml'
+ ]
+
+ context 'when including available templates in a CI YAML configuration' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:template_name) do
+ all_templates - excluded_templates - non_autodevops_templates
+ end
+
+ with_them do
+ let(:content) do
+ <<~EOS
+ include:
+ - template: #{template_name}
+
+ concrete_build_implemented_by_a_user:
+ stage: test
+ script: do something
+ EOS
+ end
+
+ it { is_expected.to be_valid }
+
+ include_examples 'require default stages to be included'
+ end
+ end
+
+ context 'when including unavailable templates in a CI YAML configuration' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:template_name) do
+ excluded_templates
+ end
+
+ with_them do
+ let(:content) do
+ <<~EOS
+ include:
+ - template: #{template_name}
+
+ concrete_build_implemented_by_a_user:
+ stage: test
+ script: do something
+ EOS
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+ end
+
+ describe 'that do not support autodevops' do
+ context 'when DAST API template' do
+ # The DAST API template purposly excludes a stages
+ # definition.
- with_them do
- let(:content) do
- if template_name == 'Security/DAST-API.gitlab-ci.yml'
- # The DAST-API template purposly excludes a stages
- # definition.
+ let(:template_name) { 'Security/DAST-API.gitlab-ci.yml' }
+ context 'with default stages' do
+ let(:content) do
+ <<~EOS
+ include:
+ - template: #{template_name}
+
+ concrete_build_implemented_by_a_user:
+ stage: test
+ script: do something
+ EOS
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with defined stages' do
+ let(:content) do
<<~EOS
include:
- template: #{template_name}
@@ -40,7 +119,22 @@ RSpec.describe 'CI YML Templates' do
stage: test
script: do something
EOS
- else
+ end
+
+ it { is_expected.to be_valid }
+
+ include_examples 'require default stages to be included'
+ end
+ end
+
+ context 'when API Fuzzing template' do
+ # The API Fuzzing template purposly excludes a stages
+ # definition.
+
+ let(:template_name) { 'Security/API-Fuzzing.gitlab-ci.yml' }
+
+ context 'with default stages' do
+ let(:content) do
<<~EOS
include:
- template: #{template_name}
@@ -50,39 +144,31 @@ RSpec.describe 'CI YML Templates' do
script: do something
EOS
end
- end
-
- it 'is valid' do
- expect(subject).to be_valid
- end
- it 'require default stages to be included' do
- expect(subject.stages).to include(*Gitlab::Ci::Config::Entry::Stages.default)
+ it { is_expected.not_to be_valid }
end
- end
- end
- context 'when including unavailable templates in a CI YAML configuration' do
- using RSpec::Parameterized::TableSyntax
+ context 'with defined stages' do
+ let(:content) do
+ <<~EOS
+ include:
+ - template: #{template_name}
- where(:template_name) do
- excluded_templates
- end
+ stages:
+ - build
+ - test
+ - deploy
+ - fuzz
- with_them do
- let(:content) do
- <<~EOS
- include:
- - template: #{template_name}
+ concrete_build_implemented_by_a_user:
+ stage: test
+ script: do something
+ EOS
+ end
- concrete_build_implemented_by_a_user:
- stage: test
- script: do something
- EOS
- end
+ it { is_expected.to be_valid }
- it 'is not valid' do
- expect(subject).not_to be_valid
+ include_examples 'require default stages to be included'
end
end
end
diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
index f878d24fe4b..63625244fe8 100644
--- a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
+++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do
let(:chunked_io) { described_class.new(build) }
before do
- stub_feature_flags(ci_enable_live_trace: true, gitlab_ci_trace_read_consistency: true)
+ stub_feature_flags(ci_enable_live_trace: true)
end
describe "#initialize" do
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index ca9dc95711d..9443bf6d6d5 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -70,6 +70,43 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
end
end
+ describe '.possible_var_reference?' do
+ context 'table tests' do
+ using RSpec::Parameterized::TableSyntax
+
+ where do
+ {
+ "empty value": {
+ value: '',
+ result: false
+ },
+ "normal value": {
+ value: 'some value',
+ result: false
+ },
+ "simple expansions": {
+ value: 'key$variable',
+ result: true
+ },
+ "complex expansions": {
+ value: 'key${variable}${variable2}',
+ result: true
+ },
+ "complex expansions for Windows": {
+ value: 'key%variable%%variable2%',
+ result: true
+ }
+ }
+ end
+
+ with_them do
+ subject { Gitlab::Ci::Variables::Collection::Item.possible_var_reference?(value) }
+
+ it { is_expected.to eq(result) }
+ end
+ end
+ end
+
describe '#depends_on' do
let(:item) { Gitlab::Ci::Variables::Collection::Item.new(**variable) }
@@ -128,7 +165,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
end
it 'supports using an active record resource' do
- variable = create(:ci_variable, key: 'CI_VAR', value: '123')
+ variable = build(:ci_variable, key: 'CI_VAR', value: '123')
resource = described_class.fabricate(variable)
expect(resource).to be_a(described_class)
diff --git a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
index 73cf0e19d00..01eef673c35 100644
--- a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
describe '#initialize with non-Collection value' do
@@ -57,9 +58,9 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
},
"variable containing escaped variable reference": {
variables: [
- { key: 'variable_a', value: 'value' },
{ key: 'variable_b', value: '$$variable_a' },
- { key: 'variable_c', value: '$variable_b' }
+ { key: 'variable_c', value: '$variable_a' },
+ { key: 'variable_a', value: 'value' }
],
expected_errors: nil
}
@@ -144,11 +145,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do
},
"variable containing escaped variable reference": {
variables: [
- { key: 'variable_c', value: '$variable_b' },
{ key: 'variable_b', value: '$$variable_a' },
+ { key: 'variable_c', value: '$variable_a' },
{ key: 'variable_a', value: 'value' }
],
- result: %w[variable_a variable_b variable_c]
+ result: %w[variable_b variable_a variable_c]
}
}
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 7b77754190a..abda27f0d6e 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -44,6 +44,30 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
end
end
+ describe '#compact' do
+ subject do
+ described_class.new
+ .append(key: 'STRING', value: 'string')
+ .append(key: 'NIL', value: nil)
+ .append(key: nil, value: 'string')
+ end
+
+ it 'returns a new Collection instance', :aggregate_failures do
+ collection = subject.compact
+
+ expect(collection).to be_an_instance_of(described_class)
+ expect(collection).not_to eql(subject)
+ end
+
+ it 'rejects pair that has nil value', :aggregate_failures do
+ collection = subject.compact
+
+ expect(collection).not_to include(key: 'NIL', value: nil, public: true)
+ expect(collection).to include(key: 'STRING', value: 'string', public: true)
+ expect(collection).to include(key: nil, value: 'string', public: true)
+ end
+ end
+
describe '#concat' do
it 'appends all elements from an array' do
collection = described_class.new([{ key: 'VAR_1', value: '1' }])
@@ -229,6 +253,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
value: 'key${MISSING_VAR}-${CI_JOB_NAME}',
result: 'key${MISSING_VAR}-test-1',
keep_undefined: true
+ },
+ "escaped characters are kept intact": {
+ value: 'key-$TEST1-%%HOME%%-$${HOME}',
+ result: 'key-test-3-%%HOME%%-$${HOME}',
+ keep_undefined: false
}
}
end
@@ -291,6 +320,14 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
],
keep_undefined: false
},
+ "escaped characters in complex expansions are kept intact": {
+ variables: [
+ { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
+ { key: 'variable', value: '$variable2' },
+ { key: 'variable2', value: 'value2' }
+ ],
+ keep_undefined: false
+ },
"array with cyclic dependency": {
variables: [
{ key: 'variable', value: '$variable2' },
@@ -391,6 +428,30 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
{ key: 'variable3', value: 'keyvalueresult' }
]
},
+ "escaped characters in complex expansions keeping undefined are kept intact": {
+ variables: [
+ { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' },
+ { key: 'variable', value: '$variable2' },
+ { key: 'variable2', value: 'value' }
+ ],
+ keep_undefined: true,
+ result: [
+ { key: 'variable', value: 'value' },
+ { key: 'variable2', value: 'value' },
+ { key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' }
+ ]
+ },
+ "escaped characters in complex expansions discarding undefined are kept intact": {
+ variables: [
+ { key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' },
+ { key: 'variable', value: 'value_$${HOME}_%%HOME%%' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable', value: 'value_$${HOME}_%%HOME%%' },
+ { key: 'variable2', value: 'key__$${HOME}_%%HOME%%' }
+ ]
+ },
"out-of-order expansion": {
variables: [
{ key: 'variable3', value: 'key$variable2$variable' },
@@ -417,7 +478,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
{ key: 'variable3', value: 'keyresultvalue' }
]
},
- "missing variable": {
+ "missing variable discarding original": {
variables: [
{ key: 'variable2', value: 'key$variable' }
],
@@ -461,6 +522,19 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
{ key: 'variable3', value: 'key_$variable2_value2' }
]
},
+ "variable value referencing password with special characters": {
+ variables: [
+ { key: 'VAR', value: '$PASSWORD' },
+ { key: 'PASSWORD', value: 'my_password$$_%%_$A' },
+ { key: 'A', value: 'value' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'VAR', value: 'my_password$$_%%_value' },
+ { key: 'PASSWORD', value: 'my_password$$_%%_value' },
+ { key: 'A', value: 'value' }
+ ]
+ },
"cyclic dependency causes original array to be returned": {
variables: [
{ key: 'variable', value: '$variable2' },
diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
index e345cd4de9b..25705fd4260 100644
--- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb
@@ -39,6 +39,59 @@ module Gitlab
expect(expanded_config).to include(*included_config.keys)
end
end
+
+ describe '#yaml_variables_for' do
+ let(:config_content) do
+ <<~YAML
+ variables:
+ VAR1: value 1
+ VAR2: value 2
+
+ job:
+ script: echo 'hello'
+ variables:
+ VAR1: value 11
+ YAML
+ end
+
+ let(:job_name) { :job }
+
+ subject(:yaml_variables_for) { result.yaml_variables_for(job_name) }
+
+ it 'returns calculated variables with root and job variables' do
+ is_expected.to match_array([
+ { key: 'VAR1', value: 'value 11', public: true },
+ { key: 'VAR2', value: 'value 2', public: true }
+ ])
+ end
+
+ context 'when an absent job is sent' do
+ let(:job_name) { :invalid_job }
+
+ it { is_expected.to eq([]) }
+ end
+ end
+
+ describe '#stage_for' do
+ let(:config_content) do
+ <<~YAML
+ job:
+ script: echo 'hello'
+ YAML
+ end
+
+ let(:job_name) { :job }
+
+ subject(:stage_for) { result.stage_for(job_name) }
+
+ it { is_expected.to eq('test') }
+
+ context 'when an absent job is sent' do
+ let(:job_name) { :invalid_job }
+
+ it { is_expected.to be_nil }
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 94ab4819361..e8e44f884cf 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -485,10 +485,6 @@ module Gitlab
end
describe '#warnings' do
- before do
- stub_feature_flags(ci_raise_job_rules_without_workflow_rules_warning: true)
- end
-
context 'when a warning is raised in a given entry' do
let(:config) do
<<-EOYML
@@ -602,27 +598,6 @@ module Gitlab
it_behaves_like 'has warnings and expected error', /build job: need test is not defined in prior stages/
end
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ci_raise_job_rules_without_workflow_rules_warning: false)
- end
-
- context 'job rules used without workflow rules' do
- let(:config) do
- <<-EOYML
- rspec:
- script: rspec
- rules:
- - when: always
- EOYML
- end
-
- it 'does not raise the warning' do
- expect(subject.warnings).to be_empty
- end
- end
- end
end
describe 'only / except policies validations' do
diff --git a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
index 6b568320953..b0f7703462a 100644
--- a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
+++ b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Gitlab::Cleanup::OrphanLfsFileReferences do
expect(null_logger).to receive(:info).with(/Looking for orphan LFS files/)
expect(null_logger).to receive(:info).with(/Nothing to do/)
- project.lfs_objects_projects.delete_all
+ LfsObjectsProject.where(project: project).delete_all
expect(service).not_to receive(:remove_orphan_references)
diff --git a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
deleted file mode 100644
index 7f7c95b2527..00000000000
--- a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# For easier debugging set `UNICORN_DEBUG=1`
-
-RSpec.describe Gitlab::Cluster::Mixins::UnicornHttpServer do
- before do
- stub_const('UNICORN_STARTUP_TIMEOUT', 30)
- end
-
- context 'when running Unicorn' do
- using RSpec::Parameterized::TableSyntax
-
- where(:signal, :exitstatus, :termsig) do
- # executes phased restart block
- :USR2 | 140 | nil
- :QUIT | 140 | nil
-
- # does not execute phased restart block
- :INT | 0 | nil
- :TERM | 0 | nil
- end
-
- with_them do
- it 'properly handles process lifecycle' do
- with_unicorn(workers: 1) do |pid|
- Process.kill(signal, pid)
-
- child_pid, child_status = Process.wait2(pid)
- expect(child_pid).to eq(pid)
- expect(child_status.exitstatus).to eq(exitstatus)
- expect(child_status.termsig).to eq(termsig)
- end
- end
- end
- end
-
- private
-
- def with_unicorn(workers:, timeout: UNICORN_STARTUP_TIMEOUT)
- with_unicorn_configs(workers: workers) do |unicorn_rb, config_ru|
- cmdline = [
- "bundle", "exec", "unicorn",
- "-I", Rails.root.to_s,
- "-c", unicorn_rb,
- config_ru
- ]
-
- IO.popen(cmdline) do |process|
- # wait for process to start:
- # I, [2019-10-15T13:21:27.565225 #3089] INFO -- : master process ready
- wait_for_output(process, /master process ready/, timeout: timeout)
- consume_output(process)
-
- yield(process.pid)
- ensure
- begin
- Process.kill(:KILL, process.pid)
- rescue Errno::ESRCH
- end
- end
- end
- end
-
- def with_unicorn_configs(workers:)
- Dir.mktmpdir do |dir|
- File.write "#{dir}/unicorn.rb", <<-EOF
- require './lib/gitlab/cluster/lifecycle_events'
- require './lib/gitlab/cluster/mixins/unicorn_http_server'
-
- worker_processes #{workers}
- listen "127.0.0.1:0"
- preload_app true
-
- Unicorn::HttpServer.prepend(#{described_class})
-
- mutex = Mutex.new
-
- Gitlab::Cluster::LifecycleEvents.on_before_blackout_period do
- mutex.synchronize do
- exit(140)
- end
- end
-
- # redirect stderr to stdout
- $stderr.reopen($stdout)
- EOF
-
- File.write "#{dir}/config.ru", <<-EOF
- run -> (env) { [404, {}, ['']] }
- EOF
-
- yield("#{dir}/unicorn.rb", "#{dir}/config.ru")
- end
- end
-
- def wait_for_output(process, output, timeout:)
- Timeout.timeout(timeout) do
- loop do
- line = process.readline
- puts "UNICORN_DEBUG: #{line}" if ENV['UNICORN_DEBUG']
- break if line =~ output
- end
- end
- end
-
- def consume_output(process)
- Thread.new do
- loop do
- line = process.readline
- puts "UNICORN_DEBUG: #{line}" if ENV['UNICORN_DEBUG']
- end
- rescue StandardError
- end
- end
-end
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index 19e52d2cf4a..d08057fb10a 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -58,6 +58,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com https://example.com")
expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com")
+ expect(directives['font_src']).to eq("'self' https://example.com")
end
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 2cdf95ea101..b9e0132badb 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -4,14 +4,15 @@ require 'spec_helper'
RSpec.describe Gitlab::CycleAnalytics::StageSummary do
let(:project) { create(:project, :repository) }
- let(:options) { { from: 1.day.ago, current_user: user } }
+ let(:options) { { from: 1.day.ago } }
+ let(:args) { { options: options, current_user: user } }
let(:user) { create(:user, :admin) }
before do
project.add_maintainer(user)
end
- let(:stage_summary) { described_class.new(project, **options).data }
+ let(:stage_summary) { described_class.new(project, **args).data }
describe "#new_issues" do
subject { stage_summary.first }
@@ -117,11 +118,11 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
before do
project.add_guest(guest_user)
- options.merge!({ current_user: guest_user })
+ args.merge!({ current_user: guest_user })
end
it 'does not include commit stats' do
- data = described_class.new(project, **options).data
+ data = described_class.new(project, **args).data
expect(includes_commits?(data)).to be_falsy
end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index a31e5a1d1e2..325fdb90929 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -47,6 +47,8 @@ RSpec.describe Gitlab::DataBuilder::Build do
it { expect(data[:runner][:id]).to eq(build.runner.id) }
it { expect(data[:runner][:tags]).to match_array(tag_names) }
it { expect(data[:runner][:description]).to eq(build.runner.description) }
+ it { expect(data[:runner][:runner_type]).to eq(build.runner.runner_type) }
+ it { expect(data[:runner][:is_shared]).to eq(build.runner.instance_type?) }
it { expect(data[:environment]).to be_nil }
context 'commit author_url' do
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index bec1e612c02..c05a044f0de 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -58,8 +58,10 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
it 'has runner attributes', :aggregate_failures do
expect(runner_data[:id]).to eq(ci_runner.id)
expect(runner_data[:description]).to eq(ci_runner.description)
+ expect(runner_data[:runner_type]).to eq(ci_runner.runner_type)
expect(runner_data[:active]).to eq(ci_runner.active)
expect(runner_data[:tags]).to match_array(tag_names)
+ expect(runner_data[:is_shared]).to eq(ci_runner.instance_type?)
end
end
diff --git a/spec/lib/gitlab/data_builder/wiki_page_spec.rb b/spec/lib/gitlab/data_builder/wiki_page_spec.rb
index ec768cf9719..276ddb1e0af 100644
--- a/spec/lib/gitlab/data_builder/wiki_page_spec.rb
+++ b/spec/lib/gitlab/data_builder/wiki_page_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe Gitlab::DataBuilder::WikiPage do
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
- let(:user) { create(:user) }
+ let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
+ let_it_be(:user) { create(:user) }
describe '.build' do
let(:data) { described_class.build(wiki_page, user, 'create') }
@@ -19,5 +19,6 @@ RSpec.describe Gitlab::DataBuilder::WikiPage do
it { expect(data[:object_attributes]).to include(wiki_page.hook_attrs) }
it { expect(data[:object_attributes]).to include(url: Gitlab::UrlBuilder.build(wiki_page)) }
it { expect(data[:object_attributes]).to include(action: 'create') }
+ it { expect(data[:object_attributes]).to include(diff_url: Gitlab::UrlBuilder.build(wiki_page, action: :diff, version_id: wiki_page.version.id)) }
end
end
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
index 78e0b7627e9..2de784d3e16 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -49,16 +49,6 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
let(:batched_job) { build(:batched_background_migration_job) }
let(:batched_migration) { batched_job.batched_migration }
- describe '#migration_aborted?' do
- before do
- batched_migration.status = :aborted
- end
-
- it 'returns the migration aborted?' do
- expect(batched_job.migration_aborted?).to eq(batched_migration.aborted?)
- end
- end
-
describe '#migration_job_class' do
it 'returns the migration job_class' do
expect(batched_job.migration_job_class).to eq(batched_migration.job_class)
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 43e34325419..d881390cd52 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -19,6 +19,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end
end
+ describe 'validations' do
+ subject { build(:batched_background_migration) }
+
+ it { is_expected.to validate_uniqueness_of(:job_arguments).scoped_to(:job_class_name, :table_name, :column_name) }
+ end
+
describe '.queue_order' do
let!(:migration1) { create(:batched_background_migration) }
let!(:migration2) { create(:batched_background_migration) }
@@ -36,6 +42,38 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
it 'returns the first active migration according to queue order' do
expect(described_class.active_migration).to eq(migration2)
+ create(:batched_background_migration_job, batched_migration: migration1, batch_size: 1000, status: :succeeded)
+ end
+ end
+
+ describe '.queued' do
+ let!(:migration1) { create(:batched_background_migration, :finished) }
+ let!(:migration2) { create(:batched_background_migration, :paused) }
+ let!(:migration3) { create(:batched_background_migration, :active) }
+
+ it 'returns active and paused migrations' do
+ expect(described_class.queued).to contain_exactly(migration2, migration3)
+ end
+ end
+
+ describe '.successful_rows_counts' do
+ let!(:migration1) { create(:batched_background_migration) }
+ let!(:migration2) { create(:batched_background_migration) }
+ let!(:migration_without_jobs) { create(:batched_background_migration) }
+
+ before do
+ create(:batched_background_migration_job, batched_migration: migration1, batch_size: 1000, status: :succeeded)
+ create(:batched_background_migration_job, batched_migration: migration1, batch_size: 200, status: :failed)
+ create(:batched_background_migration_job, batched_migration: migration2, batch_size: 500, status: :succeeded)
+ create(:batched_background_migration_job, batched_migration: migration2, batch_size: 200, status: :running)
+ end
+
+ it 'returns totals from successful jobs' do
+ results = described_class.successful_rows_counts([migration1, migration2, migration_without_jobs])
+
+ expect(results[migration1.id]).to eq(1000)
+ expect(results[migration2.id]).to eq(500)
+ expect(results[migration_without_jobs.id]).to eq(nil)
end
end
@@ -324,4 +362,29 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
subject
end
end
+
+ describe '.for_configuration' do
+ let!(:migration) do
+ create(
+ :batched_background_migration,
+ job_class_name: 'MyJobClass',
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: [[:id], [:id_convert_to_bigint]]
+ )
+ end
+
+ before do
+ create(:batched_background_migration, job_class_name: 'OtherClass')
+ create(:batched_background_migration, table_name: 'other_table')
+ create(:batched_background_migration, column_name: 'other_column')
+ create(:batched_background_migration, job_arguments: %w[other arguments])
+ end
+
+ it 'finds the migration matching the given configuration parameters' do
+ actual = described_class.for_configuration('MyJobClass', :projects, :id, [[:id], [:id_convert_to_bigint]])
+
+ expect(actual).to contain_exactly(migration)
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/consistency_spec.rb b/spec/lib/gitlab/database/consistency_spec.rb
new file mode 100644
index 00000000000..35fa65512ae
--- /dev/null
+++ b/spec/lib/gitlab/database/consistency_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Consistency do
+ let(:session) do
+ Gitlab::Database::LoadBalancing::Session.current
+ end
+
+ describe '.with_read_consistency' do
+ it 'sticks to primary database' do
+ expect(session).not_to be_using_primary
+
+ block = -> (&control) do
+ described_class.with_read_consistency do
+ expect(session).to be_using_primary
+
+ control.call
+ end
+ end
+
+ expect { |probe| block.call(&probe) }.to yield_control
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
index 324ed498abc..cdcc862c376 100644
--- a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Database::Count::ReltuplesCountStrategy do
end
context 'when models using single-type inheritance are used' do
- let(:models) { [Group, CiService, Namespace] }
+ let(:models) { [Group, Integrations::BaseCi, Namespace] }
before do
models.each do |model|
diff --git a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
index 23ad621d0ee..0844616ee1c 100644
--- a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
+++ b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::DynamicModelHelpers do
+ let(:including_class) { Class.new.include(described_class) }
+ let(:table_name) { 'projects' }
+
describe '#define_batchable_model' do
subject { including_class.new.define_batchable_model(table_name) }
- let(:including_class) { Class.new.include(described_class) }
- let(:table_name) { 'projects' }
-
it 'is an ActiveRecord model' do
expect(subject.ancestors).to include(ActiveRecord::Base)
end
@@ -25,4 +25,86 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
expect(subject.inheritance_column).to eq('_type_disabled')
end
end
+
+ describe '#each_batch' do
+ subject { including_class.new }
+
+ before do
+ create_list(:project, 2)
+ end
+
+ context 'when no transaction is open' do
+ before do
+ allow(subject).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'iterates table in batches' do
+ each_batch_size = ->(&block) do
+ subject.each_batch(table_name, of: 1) do |batch|
+ block.call(batch.size)
+ end
+ end
+
+ expect { |b| each_batch_size.call(&b) }
+ .to yield_successive_args(1, 1)
+ end
+ end
+
+ context 'when transaction is open' do
+ before do
+ allow(subject).to receive(:transaction_open?).and_return(true)
+ end
+
+ it 'raises an error' do
+ expect { subject.each_batch(table_name, of: 1) { |batch| batch.size } }
+ .to raise_error(RuntimeError, /each_batch should not run inside a transaction/)
+ end
+ end
+ end
+
+ describe '#each_batch_range' do
+ subject { including_class.new }
+
+ let(:first_project) { create(:project) }
+ let(:second_project) { create(:project) }
+
+ context 'when no transaction is open' do
+ before do
+ allow(subject).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'iterates table in batch ranges' do
+ expect { |b| subject.each_batch_range(table_name, of: 1, &b) }
+ .to yield_successive_args(
+ [first_project.id, first_project.id],
+ [second_project.id, second_project.id]
+ )
+ end
+
+ it 'yields only one batch if bigger than the table size' do
+ expect { |b| subject.each_batch_range(table_name, of: 2, &b) }
+ .to yield_successive_args([first_project.id, second_project.id])
+ end
+
+ it 'makes it possible to apply a scope' do
+ each_batch_limited = ->(&b) do
+ subject.each_batch_range(table_name, scope: ->(table) { table.limit(1) }, of: 1, &b)
+ end
+
+ expect { |b| each_batch_limited.call(&b) }
+ .to yield_successive_args([first_project.id, first_project.id])
+ end
+ end
+
+ context 'when transaction is open' do
+ before do
+ allow(subject).to receive(:transaction_open?).and_return(true)
+ end
+
+ it 'raises an error' do
+ expect { subject.each_batch_range(table_name, of: 1) { 1 } }
+ .to raise_error(RuntimeError, /each_batch should not run inside a transaction/)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/load_balancing/active_record_proxy_spec.rb b/spec/lib/gitlab/database/load_balancing/active_record_proxy_spec.rb
new file mode 100644
index 00000000000..8886ce9756d
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/active_record_proxy_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::ActiveRecordProxy do
+ describe '#connection' do
+ it 'returns a connection proxy' do
+ dummy = Class.new do
+ include Gitlab::Database::LoadBalancing::ActiveRecordProxy
+ end
+
+ proxy = double(:proxy)
+
+ expect(Gitlab::Database::LoadBalancing).to receive(:proxy)
+ .and_return(proxy)
+
+ expect(dummy.new.connection).to eq(proxy)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb b/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
new file mode 100644
index 00000000000..015dd2ba8d2
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
@@ -0,0 +1,316 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
+ let(:proxy) { described_class.new }
+
+ describe '#select' do
+ it 'performs a read' do
+ expect(proxy).to receive(:read_using_load_balancer).with(:select, ['foo'])
+
+ proxy.select('foo')
+ end
+ end
+
+ describe '#select_all' do
+ let(:override_proxy) { ActiveRecord::Base.connection.class }
+
+ # We can't use :Gitlab::Utils::Override because this method is dynamically prepended
+ it 'method signatures match' do
+ expect(proxy.method(:select_all).parameters).to eq(override_proxy.instance_method(:select_all).parameters)
+ end
+
+ describe 'using a SELECT query' do
+ it 'runs the query on a secondary' do
+ arel = double(:arel)
+
+ expect(proxy).to receive(:read_using_load_balancer)
+ .with(:select_all, [arel, 'foo', []])
+
+ proxy.select_all(arel, 'foo')
+ end
+ end
+
+ describe 'using a SELECT FOR UPDATE query' do
+ it 'runs the query on the primary and sticks to it' do
+ arel = double(:arel, locked: true)
+
+ expect(proxy).to receive(:write_using_load_balancer)
+ .with(:select_all, [arel, 'foo', []], sticky: true)
+
+ proxy.select_all(arel, 'foo')
+ end
+ end
+ end
+
+ Gitlab::Database::LoadBalancing::ConnectionProxy::NON_STICKY_READS.each do |name|
+ describe "#{name}" do
+ it 'runs the query on the replica' do
+ expect(proxy).to receive(:read_using_load_balancer)
+ .with(name, ['foo'])
+
+ proxy.send(name, 'foo')
+ end
+ end
+ end
+
+ Gitlab::Database::LoadBalancing::ConnectionProxy::STICKY_WRITES.each do |name|
+ describe "#{name}" do
+ it 'runs the query on the primary and sticks to it' do
+ expect(proxy).to receive(:write_using_load_balancer)
+ .with(name, ['foo'], sticky: true)
+
+ proxy.send(name, 'foo')
+ end
+ end
+ end
+
+ describe '.insert_all!' do
+ before do
+ ActiveRecord::Schema.define do
+ create_table :connection_proxy_bulk_insert, force: true do |t|
+ t.string :name, null: true
+ end
+ end
+ end
+
+ after do
+ ActiveRecord::Schema.define do
+ drop_table :connection_proxy_bulk_insert, force: true
+ end
+ end
+
+ let(:model_class) do
+ Class.new(ApplicationRecord) do
+ self.table_name = "connection_proxy_bulk_insert"
+ end
+ end
+
+ it 'inserts data in bulk' do
+ expect(model_class).to receive(:connection)
+ .at_least(:once)
+ .and_return(proxy)
+
+ expect(proxy).to receive(:write_using_load_balancer)
+ .at_least(:once)
+ .and_call_original
+
+ expect do
+ model_class.insert_all! [
+ { name: "item1" },
+ { name: "item2" }
+ ]
+ end.to change { model_class.count }.by(2)
+ end
+ end
+
+ # We have an extra test for #transaction here to make sure that nested queries
+ # are also sent to a primary.
+ describe '#transaction' do
+ let(:session) { double(:session) }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
+ .and_return(session)
+ end
+
+ context 'session fallbacks ambiguous queries to replicas' do
+ let(:replica) { double(:connection) }
+
+ before do
+ allow(session).to receive(:fallback_to_replicas_for_ambiguous_queries?).and_return(true)
+ allow(session).to receive(:use_primary?).and_return(false)
+ allow(replica).to receive(:transaction).and_yield
+ allow(replica).to receive(:select)
+ end
+
+ context 'with a read query' do
+ it 'runs the transaction and any nested queries on the replica' do
+ expect(proxy.load_balancer).to receive(:read)
+ .twice.and_yield(replica)
+ expect(proxy.load_balancer).not_to receive(:read_write)
+ expect(session).not_to receive(:write!)
+
+ proxy.transaction { proxy.select('true') }
+ end
+ end
+
+ context 'with a write query' do
+ it 'raises an exception' do
+ allow(proxy.load_balancer).to receive(:read).and_yield(replica)
+ allow(proxy.load_balancer).to receive(:read_write).and_yield(replica)
+
+ expect do
+ proxy.transaction { proxy.insert('something') }
+ end.to raise_error(Gitlab::Database::LoadBalancing::ConnectionProxy::WriteInsideReadOnlyTransactionError)
+ end
+ end
+ end
+
+ context 'session does not fallback to replicas for ambiguous queries' do
+ let(:primary) { double(:connection) }
+
+ before do
+ allow(session).to receive(:fallback_to_replicas_for_ambiguous_queries?).and_return(false)
+ allow(session).to receive(:use_replicas_for_read_queries?).and_return(false)
+ allow(session).to receive(:use_primary?).and_return(true)
+ allow(primary).to receive(:transaction).and_yield
+ allow(primary).to receive(:select)
+ allow(primary).to receive(:insert)
+ end
+
+ context 'with a read query' do
+ it 'runs the transaction and any nested queries on the primary and stick to it' do
+ expect(proxy.load_balancer).to receive(:read_write)
+ .twice.and_yield(primary)
+ expect(proxy.load_balancer).not_to receive(:read)
+ expect(session).to receive(:write!)
+
+ proxy.transaction { proxy.select('true') }
+ end
+ end
+
+ context 'with a write query' do
+ it 'runs the transaction and any nested queries on the primary and stick to it' do
+ expect(proxy.load_balancer).to receive(:read_write)
+ .twice.and_yield(primary)
+ expect(proxy.load_balancer).not_to receive(:read)
+ expect(session).to receive(:write!).twice
+
+ proxy.transaction { proxy.insert('something') }
+ end
+ end
+ end
+ end
+
+ describe '#method_missing' do
+ it 'runs the query on the primary without sticking to it' do
+ expect(proxy).to receive(:write_using_load_balancer)
+ .with(:foo, ['foo'])
+
+ proxy.foo('foo')
+ end
+
+ it 'properly forwards trailing hash arguments' do
+ allow(proxy.load_balancer).to receive(:read_write)
+
+ expect(proxy).to receive(:write_using_load_balancer).and_call_original
+
+ expect { proxy.case_sensitive_comparison(:table, :attribute, :column, { value: :value, format: :format }) }
+ .not_to raise_error
+ end
+
+ context 'current session prefers to fallback ambiguous queries to replicas' do
+ let(:session) { double(:session) }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
+ .and_return(session)
+ allow(session).to receive(:fallback_to_replicas_for_ambiguous_queries?).and_return(true)
+ allow(session).to receive(:use_primary?).and_return(false)
+ end
+
+ it 'runs the query on the replica' do
+ expect(proxy).to receive(:read_using_load_balancer).with(:foo, ['foo'])
+
+ proxy.foo('foo')
+ end
+
+ it 'properly forwards trailing hash arguments' do
+ allow(proxy.load_balancer).to receive(:read)
+
+ expect(proxy).to receive(:read_using_load_balancer).and_call_original
+
+ expect { proxy.case_sensitive_comparison(:table, :attribute, :column, { value: :value, format: :format }) }
+ .not_to raise_error
+ end
+ end
+ end
+
+ describe '#read_using_load_balancer' do
+ let(:session) { double(:session) }
+ let(:connection) { double(:connection) }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
+ .and_return(session)
+ end
+
+ context 'with a regular session' do
+ it 'uses a secondary' do
+ allow(session).to receive(:use_primary?).and_return(false)
+ allow(session).to receive(:use_replicas_for_read_queries?).and_return(false)
+
+ expect(connection).to receive(:foo).with('foo')
+ expect(proxy.load_balancer).to receive(:read).and_yield(connection)
+
+ proxy.read_using_load_balancer(:foo, ['foo'])
+ end
+ end
+
+ context 'with a regular session and forcing all reads to replicas' do
+ it 'uses a secondary' do
+ allow(session).to receive(:use_primary?).and_return(false)
+ allow(session).to receive(:use_replicas_for_read_queries?).and_return(true)
+
+ expect(connection).to receive(:foo).with('foo')
+ expect(proxy.load_balancer).to receive(:read).and_yield(connection)
+
+ proxy.read_using_load_balancer(:foo, ['foo'])
+ end
+ end
+
+ context 'with a session using the primary but forcing all reads to replicas' do
+ it 'uses a secondary' do
+ allow(session).to receive(:use_primary?).and_return(true)
+ allow(session).to receive(:use_replicas_for_read_queries?).and_return(true)
+
+ expect(connection).to receive(:foo).with('foo')
+ expect(proxy.load_balancer).to receive(:read).and_yield(connection)
+
+ proxy.read_using_load_balancer(:foo, ['foo'])
+ end
+ end
+
+ describe 'with a session using the primary' do
+ it 'uses the primary' do
+ allow(session).to receive(:use_primary?).and_return(true)
+ allow(session).to receive(:use_replicas_for_read_queries?).and_return(false)
+
+ expect(connection).to receive(:foo).with('foo')
+
+ expect(proxy.load_balancer).to receive(:read_write)
+ .and_yield(connection)
+
+ proxy.read_using_load_balancer(:foo, ['foo'])
+ end
+ end
+ end
+
+ describe '#write_using_load_balancer' do
+ let(:session) { double(:session) }
+ let(:connection) { double(:connection) }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session).to receive(:current)
+ .and_return(session)
+ end
+
+ it 'uses but does not stick to the primary when sticking is disabled' do
+ expect(proxy.load_balancer).to receive(:read_write).and_yield(connection)
+ expect(connection).to receive(:foo).with('foo')
+ expect(session).not_to receive(:write!)
+
+ proxy.write_using_load_balancer(:foo, ['foo'])
+ end
+
+ it 'sticks to the primary when sticking is enabled' do
+ expect(proxy.load_balancer).to receive(:read_write).and_yield(connection)
+ expect(connection).to receive(:foo).with('foo')
+ expect(session).to receive(:write!)
+
+ proxy.write_using_load_balancer(:foo, ['foo'], sticky: true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/host_list_spec.rb b/spec/lib/gitlab/database/load_balancing/host_list_spec.rb
new file mode 100644
index 00000000000..873b599f84d
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/host_list_spec.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::HostList do
+ def expect_metrics(hosts)
+ expect(Gitlab::Metrics.registry.get(:db_load_balancing_hosts).get({})).to eq(hosts)
+ end
+
+ before do
+ allow(Gitlab::Database)
+ .to receive(:create_connection_pool)
+ .and_return(ActiveRecord::Base.connection_pool)
+ end
+
+ let(:load_balancer) { double(:load_balancer) }
+ let(:host_count) { 2 }
+
+ let(:host_list) do
+ hosts = Array.new(host_count) do
+ Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer, port: 5432)
+ end
+
+ described_class.new(hosts)
+ end
+
+ describe '#initialize' do
+ it 'sets metrics for current number of hosts and current index' do
+ host_list
+
+ expect_metrics(2)
+ end
+ end
+
+ describe '#length' do
+ it 'returns the number of hosts in the list' do
+ expect(host_list.length).to eq(2)
+ end
+ end
+
+ describe '#host_names_and_ports' do
+ context 'with ports' do
+ it 'returns the host names of all hosts' do
+ hosts = [
+ ['localhost', 5432],
+ ['localhost', 5432]
+ ]
+
+ expect(host_list.host_names_and_ports).to eq(hosts)
+ end
+ end
+
+ context 'without ports' do
+ let(:host_list) do
+ hosts = Array.new(2) do
+ Gitlab::Database::LoadBalancing::Host.new('localhost', load_balancer)
+ end
+
+ described_class.new(hosts)
+ end
+
+ it 'returns the host names of all hosts' do
+ hosts = [
+ ['localhost', nil],
+ ['localhost', nil]
+ ]
+
+ expect(host_list.host_names_and_ports).to eq(hosts)
+ end
+ end
+ end
+
+ describe '#manage_pool?' do
+ before do
+ allow(Gitlab::Database).to receive(:create_connection_pool) { double(:connection) }
+ end
+
+ context 'when the testing pool belongs to one host of the host list' do
+ it 'returns true' do
+ pool = host_list.hosts.first.pool
+
+ expect(host_list.manage_pool?(pool)).to be(true)
+ end
+ end
+
+ context 'when the testing pool belongs to a former host of the host list' do
+ it 'returns false' do
+ pool = host_list.hosts.first.pool
+ host_list.hosts = [
+ Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
+ ]
+
+ expect(host_list.manage_pool?(pool)).to be(false)
+ end
+ end
+
+ context 'when the testing pool belongs to a new host of the host list' do
+ it 'returns true' do
+ host = Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
+ host_list.hosts = [host]
+
+ expect(host_list.manage_pool?(host.pool)).to be(true)
+ end
+ end
+
+ context 'when the testing pool does not have any relation with the host list' do
+ it 'returns false' do
+ host = Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
+
+ expect(host_list.manage_pool?(host.pool)).to be(false)
+ end
+ end
+ end
+
+ describe '#hosts' do
+ it 'returns a copy of the host' do
+ first = host_list.hosts
+
+ expect(host_list.hosts).to eq(first)
+ expect(host_list.hosts.object_id).not_to eq(first.object_id)
+ end
+ end
+
+ describe '#hosts=' do
+ it 'updates the list of hosts to use' do
+ host_list.hosts = [
+ Gitlab::Database::LoadBalancing::Host.new('foo', load_balancer)
+ ]
+
+ expect(host_list.length).to eq(1)
+ expect(host_list.hosts[0].host).to eq('foo')
+ expect_metrics(1)
+ end
+ end
+
+ describe '#next' do
+ it 'returns a host' do
+ expect(host_list.next)
+ .to be_an_instance_of(Gitlab::Database::LoadBalancing::Host)
+ end
+
+ it 'cycles through all available hosts' do
+ expect(host_list.next).to eq(host_list.hosts[0])
+ expect_metrics(2)
+
+ expect(host_list.next).to eq(host_list.hosts[1])
+ expect_metrics(2)
+
+ expect(host_list.next).to eq(host_list.hosts[0])
+ expect_metrics(2)
+ end
+
+ it 'skips hosts that are offline' do
+ allow(host_list.hosts[0]).to receive(:online?).and_return(false)
+
+ expect(host_list.next).to eq(host_list.hosts[1])
+ expect_metrics(2)
+ end
+
+ it 'returns nil if no hosts are online' do
+ host_list.hosts.each do |host|
+ allow(host).to receive(:online?).and_return(false)
+ end
+
+ expect(host_list.next).to be_nil
+ expect_metrics(2)
+ end
+
+ it 'returns nil if no hosts are available' do
+ expect(described_class.new.next).to be_nil
+ end
+ end
+
+ describe '#shuffle' do
+ let(:host_count) { 3 }
+
+ it 'randomizes the list' do
+ 2.times do
+ all_hosts = host_list.hosts
+
+ host_list.shuffle
+
+ expect(host_list.length).to eq(host_count)
+ expect(host_list.hosts).to contain_exactly(*all_hosts)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/host_spec.rb b/spec/lib/gitlab/database/load_balancing/host_spec.rb
new file mode 100644
index 00000000000..4dfddef68c8
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/host_spec.rb
@@ -0,0 +1,445 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::Host do
+ let(:load_balancer) do
+ Gitlab::Database::LoadBalancing::LoadBalancer.new(%w[localhost])
+ end
+
+ let(:host) { load_balancer.host_list.hosts.first }
+
+ before do
+ allow(Gitlab::Database).to receive(:create_connection_pool)
+ .and_return(ActiveRecord::Base.connection_pool)
+ end
+
+ def raise_and_wrap(wrapper, original)
+ raise original
+ rescue original.class
+ raise wrapper, 'boom'
+ end
+
+ def wrapped_exception(wrapper, original)
+ raise_and_wrap(wrapper, original.new)
+ rescue wrapper => error
+ error
+ end
+
+ describe '#connection' do
+ it 'returns a connection from the pool' do
+ expect(host.pool).to receive(:connection)
+
+ host.connection
+ end
+ end
+
+ describe '#disconnect!' do
+ it 'disconnects the pool' do
+ connection = double(:connection, in_use?: false)
+ pool = double(:pool, connections: [connection])
+
+ allow(host)
+ .to receive(:pool)
+ .and_return(pool)
+
+ expect(host)
+ .not_to receive(:sleep)
+
+ expect(host.pool)
+ .to receive(:disconnect!)
+
+ host.disconnect!
+ end
+
+ it 'disconnects the pool when waiting for connections takes too long' do
+ connection = double(:connection, in_use?: true)
+ pool = double(:pool, connections: [connection])
+
+ allow(host)
+ .to receive(:pool)
+ .and_return(pool)
+
+ expect(host.pool)
+ .to receive(:disconnect!)
+
+ host.disconnect!(1)
+ end
+ end
+
+ describe '#release_connection' do
+ it 'releases the current connection from the pool' do
+ expect(host.pool).to receive(:release_connection)
+
+ host.release_connection
+ end
+ end
+
+ describe '#offline!' do
+ it 'marks the host as offline' do
+ expect(host.pool).to receive(:disconnect!)
+
+ expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn)
+ .with(hash_including(event: :host_offline))
+ .and_call_original
+
+ host.offline!
+ end
+ end
+
+ describe '#online?' do
+ context 'when the replica status is recent enough' do
+ before do
+ expect(host).to receive(:check_replica_status?).and_return(false)
+ end
+
+ it 'returns the latest status' do
+ expect(host).not_to receive(:refresh_status)
+ expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
+ expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:warn)
+
+ expect(host).to be_online
+ end
+
+ it 'returns an offline status' do
+ host.offline!
+
+ expect(host).not_to receive(:refresh_status)
+ expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:info)
+ expect(Gitlab::Database::LoadBalancing::Logger).not_to receive(:warn)
+
+ expect(host).not_to be_online
+ end
+ end
+
+ context 'when the replica status is outdated' do
+ before do
+ expect(host)
+ .to receive(:check_replica_status?)
+ .and_return(true)
+ end
+
+ it 'refreshes the status' do
+ expect(Gitlab::Database::LoadBalancing::Logger).to receive(:info)
+ .with(hash_including(event: :host_online))
+ .and_call_original
+
+ expect(host).to be_online
+ end
+
+ context 'and replica is not up to date' do
+ before do
+ expect(host).to receive(:replica_is_up_to_date?).and_return(false)
+ end
+
+ it 'marks the host offline' do
+ expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn)
+ .with(hash_including(event: :host_offline))
+ .and_call_original
+
+ expect(host).not_to be_online
+ end
+ end
+ end
+
+ context 'when the replica is not online' do
+ it 'returns false when ActionView::Template::Error is raised' do
+ wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
+
+ allow(host)
+ .to receive(:check_replica_status?)
+ .and_raise(wrapped_error)
+
+ expect(host).not_to be_online
+ end
+
+ it 'returns false when ActiveRecord::StatementInvalid is raised' do
+ allow(host)
+ .to receive(:check_replica_status?)
+ .and_raise(ActiveRecord::StatementInvalid.new('foo'))
+
+ expect(host).not_to be_online
+ end
+
+ it 'returns false when PG::Error is raised' do
+ allow(host)
+ .to receive(:check_replica_status?)
+ .and_raise(PG::Error)
+
+ expect(host).not_to be_online
+ end
+ end
+ end
+
+ describe '#refresh_status' do
+ it 'refreshes the status' do
+ host.offline!
+
+ expect(host)
+ .to receive(:replica_is_up_to_date?)
+ .and_call_original
+
+ host.refresh_status
+
+ expect(host).to be_online
+ end
+ end
+
+ describe '#check_replica_status?' do
+ it 'returns true when we need to check the replica status' do
+ allow(host)
+ .to receive(:last_checked_at)
+ .and_return(1.year.ago)
+
+ expect(host.check_replica_status?).to eq(true)
+ end
+
+ it 'returns false when we do not need to check the replica status' do
+ freeze_time do
+ allow(host)
+ .to receive(:last_checked_at)
+ .and_return(Time.zone.now)
+
+ expect(host.check_replica_status?).to eq(false)
+ end
+ end
+ end
+
+ describe '#replica_is_up_to_date?' do
+ context 'when the lag time is below the threshold' do
+ it 'returns true' do
+ expect(host)
+ .to receive(:replication_lag_below_threshold?)
+ .and_return(true)
+
+ expect(host.replica_is_up_to_date?).to eq(true)
+ end
+ end
+
+ context 'when the lag time exceeds the threshold' do
+ before do
+ allow(host)
+ .to receive(:replication_lag_below_threshold?)
+ .and_return(false)
+ end
+
+ it 'returns true if the data is recent enough' do
+ expect(host)
+ .to receive(:data_is_recent_enough?)
+ .and_return(true)
+
+ expect(host.replica_is_up_to_date?).to eq(true)
+ end
+
+ it 'returns false when the data is not recent enough' do
+ expect(host)
+ .to receive(:data_is_recent_enough?)
+ .and_return(false)
+
+ expect(host.replica_is_up_to_date?).to eq(false)
+ end
+ end
+ end
+
+ describe '#replication_lag_below_threshold' do
+ it 'returns true when the lag time is below the threshold' do
+ expect(host)
+ .to receive(:replication_lag_time)
+ .and_return(1)
+
+ expect(host.replication_lag_below_threshold?).to eq(true)
+ end
+
+ it 'returns false when the lag time exceeds the threshold' do
+ expect(host)
+ .to receive(:replication_lag_time)
+ .and_return(9000)
+
+ expect(host.replication_lag_below_threshold?).to eq(false)
+ end
+
+ it 'returns false when no lag time could be calculated' do
+ expect(host)
+ .to receive(:replication_lag_time)
+ .and_return(nil)
+
+ expect(host.replication_lag_below_threshold?).to eq(false)
+ end
+ end
+
+ describe '#data_is_recent_enough?' do
+ it 'returns true when the data is recent enough' do
+ expect(host.data_is_recent_enough?).to eq(true)
+ end
+
+ it 'returns false when the data is not recent enough' do
+ diff = Gitlab::Database::LoadBalancing.max_replication_difference * 2
+
+ expect(host)
+ .to receive(:query_and_release)
+ .and_return({ 'diff' => diff })
+
+ expect(host.data_is_recent_enough?).to eq(false)
+ end
+
+ it 'returns false when no lag size could be calculated' do
+ expect(host)
+ .to receive(:replication_lag_size)
+ .and_return(nil)
+
+ expect(host.data_is_recent_enough?).to eq(false)
+ end
+ end
+
+ describe '#replication_lag_time' do
+ it 'returns the lag time as a Float' do
+ expect(host.replication_lag_time).to be_an_instance_of(Float)
+ end
+
+ it 'returns nil when the database query returned no rows' do
+ expect(host)
+ .to receive(:query_and_release)
+ .and_return({})
+
+ expect(host.replication_lag_time).to be_nil
+ end
+ end
+
+ describe '#replication_lag_size' do
+ it 'returns the lag size as an Integer' do
+ expect(host.replication_lag_size).to be_an_instance_of(Integer)
+ end
+
+ it 'returns nil when the database query returned no rows' do
+ expect(host)
+ .to receive(:query_and_release)
+ .and_return({})
+
+ expect(host.replication_lag_size).to be_nil
+ end
+
+ it 'returns nil when the database connection fails' do
+ wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
+
+ allow(host)
+ .to receive(:connection)
+ .and_raise(wrapped_error)
+
+ expect(host.replication_lag_size).to be_nil
+ end
+ end
+
+ describe '#primary_write_location' do
+ it 'returns the write location of the primary' do
+ expect(host.primary_write_location).to be_an_instance_of(String)
+ expect(host.primary_write_location).not_to be_empty
+ end
+ end
+
+ describe '#caught_up?' do
+ let(:connection) { double(:connection) }
+
+ before do
+ allow(connection).to receive(:quote).and_return('foo')
+ end
+
+ it 'returns true when a host has caught up' do
+ allow(host).to receive(:connection).and_return(connection)
+ expect(connection).to receive(:select_all).and_return([{ 'result' => 't' }])
+
+ expect(host.caught_up?('foo')).to eq(true)
+ end
+
+ it 'returns true when a host has caught up' do
+ allow(host).to receive(:connection).and_return(connection)
+ expect(connection).to receive(:select_all).and_return([{ 'result' => true }])
+
+ expect(host.caught_up?('foo')).to eq(true)
+ end
+
+ it 'returns false when a host has not caught up' do
+ allow(host).to receive(:connection).and_return(connection)
+ expect(connection).to receive(:select_all).and_return([{ 'result' => 'f' }])
+
+ expect(host.caught_up?('foo')).to eq(false)
+ end
+
+ it 'returns false when a host has not caught up' do
+ allow(host).to receive(:connection).and_return(connection)
+ expect(connection).to receive(:select_all).and_return([{ 'result' => false }])
+
+ expect(host.caught_up?('foo')).to eq(false)
+ end
+
+ it 'returns false when the connection fails' do
+ wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
+
+ allow(host)
+ .to receive(:connection)
+ .and_raise(wrapped_error)
+
+ expect(host.caught_up?('foo')).to eq(false)
+ end
+ end
+
+ describe '#database_replica_location' do
+ let(:connection) { double(:connection) }
+
+ it 'returns the write ahead location of the replica', :aggregate_failures do
+ expect(host)
+ .to receive(:query_and_release)
+ .and_return({ 'location' => '0/D525E3A8' })
+
+ expect(host.database_replica_location).to be_an_instance_of(String)
+ end
+
+ it 'returns nil when the database query returned no rows' do
+ expect(host)
+ .to receive(:query_and_release)
+ .and_return({})
+
+ expect(host.database_replica_location).to be_nil
+ end
+
+ it 'returns nil when the database connection fails' do
+ wrapped_error = wrapped_exception(ActionView::Template::Error, StandardError)
+
+ allow(host)
+ .to receive(:connection)
+ .and_raise(wrapped_error)
+
+ expect(host.database_replica_location).to be_nil
+ end
+ end
+
+ describe '#query_and_release' do
+ it 'executes a SQL query' do
+ results = host.query_and_release('SELECT 10 AS number')
+
+ expect(results).to be_an_instance_of(Hash)
+ expect(results['number'].to_i).to eq(10)
+ end
+
+ it 'releases the connection after running the query' do
+ expect(host)
+ .to receive(:release_connection)
+ .once
+
+ host.query_and_release('SELECT 10 AS number')
+ end
+
+ it 'returns an empty Hash in the event of an error' do
+ expect(host.connection)
+ .to receive(:select_all)
+ .and_raise(RuntimeError, 'kittens')
+
+ expect(host.query_and_release('SELECT 10 AS number')).to eq({})
+ end
+ end
+
+ describe '#host' do
+ it 'returns the hostname' do
+ expect(host.host).to eq('localhost')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
new file mode 100644
index 00000000000..4705bb23885
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
@@ -0,0 +1,522 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
+ let(:pool) { Gitlab::Database.create_connection_pool(2) }
+ let(:conflict_error) { Class.new(RuntimeError) }
+
+ let(:lb) { described_class.new(%w(localhost localhost)) }
+
+ before do
+ allow(Gitlab::Database).to receive(:create_connection_pool)
+ .and_return(pool)
+ stub_const(
+ 'Gitlab::Database::LoadBalancing::LoadBalancer::PG::TRSerializationFailure',
+ conflict_error
+ )
+ end
+
+ def raise_and_wrap(wrapper, original)
+ raise original
+ rescue original.class
+ raise wrapper, 'boop'
+ end
+
+ def wrapped_exception(wrapper, original)
+ raise_and_wrap(wrapper, original.new)
+ rescue wrapper => error
+ error
+ end
+
+ def twice_wrapped_exception(top, middle, original)
+ begin
+ raise_and_wrap(middle, original.new)
+ rescue middle => middle_error
+ raise_and_wrap(top, middle_error)
+ end
+ rescue top => top_error
+ top_error
+ end
+
+ describe '#read' do
+ it 'yields a connection for a read' do
+ connection = double(:connection)
+ host = double(:host)
+
+ allow(lb).to receive(:host).and_return(host)
+ allow(host).to receive(:query_cache_enabled).and_return(true)
+
+ expect(host).to receive(:connection).and_return(connection)
+
+ expect { |b| lb.read(&b) }.to yield_with_args(connection)
+ end
+
+ it 'ensures that query cache is enabled' do
+ connection = double(:connection)
+ host = double(:host)
+
+ allow(lb).to receive(:host).and_return(host)
+ allow(host).to receive(:query_cache_enabled).and_return(false)
+ allow(host).to receive(:connection).and_return(connection)
+
+ expect(host).to receive(:enable_query_cache!).once
+
+ lb.read { 10 }
+ end
+
+ it 'marks hosts that are offline' do
+ allow(lb).to receive(:connection_error?).and_return(true)
+
+ expect(lb.host_list.hosts[0]).to receive(:offline!)
+ expect(lb).to receive(:release_host)
+
+ raised = false
+
+ returned = lb.read do
+ unless raised
+ raised = true
+ raise
+ end
+
+ 10
+ end
+
+ expect(returned).to eq(10)
+ end
+
+ it 'retries a query in the event of a serialization failure' do
+ raised = false
+
+ expect(lb).to receive(:release_host)
+
+ returned = lb.read do
+ unless raised
+ raised = true
+ raise conflict_error
+ end
+
+ 10
+ end
+
+ expect(returned).to eq(10)
+ end
+
+ it 'retries every host at most 3 times when a query conflict is raised' do
+ expect(lb).to receive(:release_host).exactly(6).times
+ expect(lb).to receive(:read_write)
+
+ lb.read { raise conflict_error }
+ end
+
+ it 'uses the primary if no secondaries are available' do
+ allow(lb).to receive(:connection_error?).and_return(true)
+
+ expect(lb.host_list.hosts).to all(receive(:online?).and_return(false))
+
+ expect(lb).to receive(:read_write).and_call_original
+
+ expect { |b| lb.read(&b) }
+ .to yield_with_args(ActiveRecord::Base.retrieve_connection)
+ end
+ end
+
+ describe '#read_write' do
+ it 'yields a connection for a write' do
+ expect { |b| lb.read_write(&b) }
+ .to yield_with_args(ActiveRecord::Base.retrieve_connection)
+ end
+
+ it 'uses a retry with exponential backoffs' do
+ expect(lb).to receive(:retry_with_backoff).and_yield
+
+ lb.read_write { 10 }
+ end
+ end
+
+ describe '#db_role_for_connection' do
+ context 'when the load balancer creates the connection with #read' do
+ it 'returns :replica' do
+ role = nil
+ lb.read do |connection|
+ role = lb.db_role_for_connection(connection)
+ end
+
+ expect(role).to be(:replica)
+ end
+ end
+
+ context 'when the load balancer uses nested #read' do
+ it 'returns :replica' do
+ roles = []
+ lb.read do |connection_1|
+ lb.read do |connection_2|
+ roles << lb.db_role_for_connection(connection_2)
+ end
+ roles << lb.db_role_for_connection(connection_1)
+ end
+
+ expect(roles).to eq([:replica, :replica])
+ end
+ end
+
+ context 'when the load balancer creates the connection with #read_write' do
+ it 'returns :primary' do
+ role = nil
+ lb.read_write do |connection|
+ role = lb.db_role_for_connection(connection)
+ end
+
+ expect(role).to be(:primary)
+ end
+ end
+
+ context 'when the load balancer uses nested #read_write' do
+ it 'returns :primary' do
+ roles = []
+ lb.read_write do |connection_1|
+ lb.read_write do |connection_2|
+ roles << lb.db_role_for_connection(connection_2)
+ end
+ roles << lb.db_role_for_connection(connection_1)
+ end
+
+ expect(roles).to eq([:primary, :primary])
+ end
+ end
+
+ context 'when the load balancer falls back the connection creation to primary' do
+ it 'returns :primary' do
+ allow(lb).to receive(:serialization_failure?).and_return(true)
+
+ role = nil
+ raised = 7 # 2 hosts = 6 retries
+
+ lb.read do |connection|
+ if raised > 0
+ raised -= 1
+ raise
+ end
+
+ role = lb.db_role_for_connection(connection)
+ end
+
+ expect(role).to be(:primary)
+ end
+ end
+
+ context 'when the load balancer uses replica after recovery from a failure' do
+ it 'returns :replica' do
+ allow(lb).to receive(:connection_error?).and_return(true)
+
+ role = nil
+ raised = false
+
+ lb.read do |connection|
+ unless raised
+ raised = true
+ raise
+ end
+
+ role = lb.db_role_for_connection(connection)
+ end
+
+ expect(role).to be(:replica)
+ end
+ end
+
+ context 'when the connection comes from a pool managed by the host list' do
+ it 'returns :replica' do
+ connection = double(:connection)
+ allow(connection).to receive(:pool).and_return(lb.host_list.hosts.first.pool)
+
+ expect(lb.db_role_for_connection(connection)).to be(:replica)
+ end
+ end
+
+ context 'when the connection comes from the primary pool' do
+ it 'returns :primary' do
+ connection = double(:connection)
+ allow(connection).to receive(:pool).and_return(ActiveRecord::Base.connection_pool)
+
+ expect(lb.db_role_for_connection(connection)).to be(:primary)
+ end
+ end
+
+ context 'when the connection does not come from any known pool' do
+ it 'returns nil' do
+ connection = double(:connection)
+ pool = double(:connection_pool)
+ allow(connection).to receive(:pool).and_return(pool)
+
+ expect(lb.db_role_for_connection(connection)).to be(nil)
+ end
+ end
+ end
+
+ describe '#host' do
+ it 'returns the secondary host to use' do
+ expect(lb.host).to be_an_instance_of(Gitlab::Database::LoadBalancing::Host)
+ end
+
+ it 'stores the host in a thread-local variable' do
+ RequestStore.delete(described_class::CACHE_KEY)
+ RequestStore.delete(described_class::VALID_HOSTS_CACHE_KEY)
+
+ expect(lb.host_list).to receive(:next).once.and_call_original
+
+ lb.host
+ lb.host
+ end
+ end
+
+ describe '#release_host' do
+ it 'releases the host and its connection' do
+ host = lb.host
+
+ expect(host).to receive(:disable_query_cache!)
+
+ lb.release_host
+
+ expect(RequestStore[described_class::CACHE_KEY]).to be_nil
+ expect(RequestStore[described_class::VALID_HOSTS_CACHE_KEY]).to be_nil
+ end
+ end
+
+ describe '#release_primary_connection' do
+ it 'releases the connection to the primary' do
+ expect(ActiveRecord::Base.connection_pool).to receive(:release_connection)
+
+ lb.release_primary_connection
+ end
+ end
+
+ describe '#primary_write_location' do
+ it 'returns a String in the right format' do
+ expect(lb.primary_write_location).to match(%r{[A-F0-9]{1,8}/[A-F0-9]{1,8}})
+ end
+
+ it 'raises an error if the write location could not be retrieved' do
+ connection = double(:connection)
+
+ allow(lb).to receive(:read_write).and_yield(connection)
+ allow(connection).to receive(:select_all).and_return([])
+
+ expect { lb.primary_write_location }.to raise_error(RuntimeError)
+ end
+ end
+
+ describe '#all_caught_up?' do
+ it 'returns true if all hosts caught up to the write location' do
+ expect(lb.host_list.hosts).to all(receive(:caught_up?).with('foo').and_return(true))
+
+ expect(lb.all_caught_up?('foo')).to eq(true)
+ end
+
+ it 'returns false if a host has not yet caught up' do
+ expect(lb.host_list.hosts[0]).to receive(:caught_up?)
+ .with('foo')
+ .and_return(true)
+
+ expect(lb.host_list.hosts[1]).to receive(:caught_up?)
+ .with('foo')
+ .and_return(false)
+
+ expect(lb.all_caught_up?('foo')).to eq(false)
+ end
+ end
+
+ describe '#retry_with_backoff' do
+ it 'returns the value returned by the block' do
+ value = lb.retry_with_backoff { 10 }
+
+ expect(value).to eq(10)
+ end
+
+ it 're-raises errors not related to database connections' do
+ expect(lb).not_to receive(:sleep) # to make sure we're not retrying
+
+ expect { lb.retry_with_backoff { raise 'boop' } }
+ .to raise_error(RuntimeError)
+ end
+
+ it 'retries the block when a connection error is raised' do
+ allow(lb).to receive(:connection_error?).and_return(true)
+ expect(lb).to receive(:sleep).with(2)
+ expect(lb).to receive(:release_primary_connection)
+
+ raised = false
+ returned = lb.retry_with_backoff do
+ unless raised
+ raised = true
+ raise
+ end
+
+ 10
+ end
+
+ expect(returned).to eq(10)
+ end
+
+ it 're-raises the connection error if the retries did not succeed' do
+ allow(lb).to receive(:connection_error?).and_return(true)
+ expect(lb).to receive(:sleep).with(2).ordered
+ expect(lb).to receive(:sleep).with(4).ordered
+ expect(lb).to receive(:sleep).with(16).ordered
+
+ expect(lb).to receive(:release_primary_connection).exactly(3).times
+
+ expect { lb.retry_with_backoff { raise } }.to raise_error(RuntimeError)
+ end
+ end
+
+ describe '#connection_error?' do
+ before do
+ stub_const('Gitlab::Database::LoadBalancing::LoadBalancer::CONNECTION_ERRORS',
+ [NotImplementedError])
+ end
+
+ it 'returns true for a connection error' do
+ error = NotImplementedError.new
+
+ expect(lb.connection_error?(error)).to eq(true)
+ end
+
+ it 'returns true for a wrapped connection error' do
+ wrapped = wrapped_exception(ActiveRecord::StatementInvalid, NotImplementedError)
+
+ expect(lb.connection_error?(wrapped)).to eq(true)
+ end
+
+ it 'returns true for a wrapped connection error from a view' do
+ wrapped = wrapped_exception(ActionView::Template::Error, NotImplementedError)
+
+ expect(lb.connection_error?(wrapped)).to eq(true)
+ end
+
+ it 'returns true for deeply wrapped/nested errors' do
+ top = twice_wrapped_exception(ActionView::Template::Error, ActiveRecord::StatementInvalid, NotImplementedError)
+
+ expect(lb.connection_error?(top)).to eq(true)
+ end
+
+ it 'returns true for an invalid encoding error' do
+ error = RuntimeError.new('invalid encoding name: unicode')
+
+ expect(lb.connection_error?(error)).to eq(true)
+ end
+
+ it 'returns false for errors not related to database connections' do
+ error = RuntimeError.new
+
+ expect(lb.connection_error?(error)).to eq(false)
+ end
+ end
+
+ describe '#serialization_failure?' do
+ let(:conflict_error) { Class.new(RuntimeError) }
+
+ before do
+ stub_const(
+ 'Gitlab::Database::LoadBalancing::LoadBalancer::PG::TRSerializationFailure',
+ conflict_error
+ )
+ end
+
+ it 'returns for a serialization error' do
+ expect(lb.serialization_failure?(conflict_error.new)).to eq(true)
+ end
+
+ it 'returns true for a wrapped error' do
+ wrapped = wrapped_exception(ActionView::Template::Error, conflict_error)
+
+ expect(lb.serialization_failure?(wrapped)).to eq(true)
+ end
+ end
+
+ describe '#select_caught_up_hosts' do
+ let(:location) { 'AB/12345'}
+ let(:hosts) { lb.host_list.hosts }
+ let(:valid_host_list) { RequestStore[described_class::VALID_HOSTS_CACHE_KEY] }
+ let(:valid_hosts) { valid_host_list.hosts }
+
+ subject { lb.select_caught_up_hosts(location) }
+
+ context 'when all replicas are caught up' do
+ before do
+ expect(hosts).to all(receive(:caught_up?).with(location).and_return(true))
+ end
+
+ it 'returns true and sets all hosts to valid' do
+ expect(subject).to be true
+ expect(valid_host_list).to be_a(Gitlab::Database::LoadBalancing::HostList)
+ expect(valid_hosts).to contain_exactly(*hosts)
+ end
+ end
+
+ context 'when none of the replicas are caught up' do
+ before do
+ expect(hosts).to all(receive(:caught_up?).with(location).and_return(false))
+ end
+
+ it 'returns false and does not set the valid hosts' do
+ expect(subject).to be false
+ expect(valid_host_list).to be_nil
+ end
+ end
+
+ context 'when one of the replicas is caught up' do
+ before do
+ expect(hosts[0]).to receive(:caught_up?).with(location).and_return(false)
+ expect(hosts[1]).to receive(:caught_up?).with(location).and_return(true)
+ end
+
+ it 'returns true and sets one host to valid' do
+ expect(subject).to be true
+ expect(valid_host_list).to be_a(Gitlab::Database::LoadBalancing::HostList)
+ expect(valid_hosts).to contain_exactly(hosts[1])
+ end
+
+ it 'host always returns the caught-up replica' do
+ subject
+
+ 3.times do
+ expect(lb.host).to eq(hosts[1])
+ RequestStore.delete(described_class::CACHE_KEY)
+ end
+ end
+ end
+ end
+
+ describe '#select_caught_up_hosts' do
+ let(:location) { 'AB/12345'}
+ let(:hosts) { lb.host_list.hosts }
+ let(:set_host) { RequestStore[described_class::CACHE_KEY] }
+
+ subject { lb.select_up_to_date_host(location) }
+
+ context 'when none of the replicas are caught up' do
+ before do
+ expect(hosts).to all(receive(:caught_up?).with(location).and_return(false))
+ end
+
+ it 'returns false and does not update the host thread-local variable' do
+ expect(subject).to be false
+ expect(set_host).to be_nil
+ end
+ end
+
+ context 'when any of the replicas is caught up' do
+ before do
+ # `allow` for non-caught up host, because we may not even check it, if will find the caught up one earlier
+ allow(hosts[0]).to receive(:caught_up?).with(location).and_return(false)
+ expect(hosts[1]).to receive(:caught_up?).with(location).and_return(true)
+ end
+
+ it 'returns true and sets host thread-local variable' do
+ expect(subject).to be true
+ expect(set_host).to eq(hosts[1])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
new file mode 100644
index 00000000000..01367716518
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/rack_middleware_spec.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::RackMiddleware, :redis do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+ let(:warden_user) { double(:warden, user: double(:user, id: 42)) }
+ let(:single_sticking_object) { Set.new([[:user, 42]]) }
+ let(:multiple_sticking_objects) do
+ Set.new([
+ [:user, 42],
+ [:runner, '123456789'],
+ [:runner, '1234']
+ ])
+ end
+
+ after do
+ Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+
+ describe '.stick_or_unstick' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?)
+ .and_return(true)
+ end
+
+ it 'sticks or unsticks a single object and updates the Rack environment' do
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:user, 42)
+
+ env = {}
+
+ described_class.stick_or_unstick(env, :user, 42)
+
+ expect(env[described_class::STICK_OBJECT].to_a).to eq([[:user, 42]])
+ end
+
+ it 'sticks or unsticks multiple objects and updates the Rack environment' do
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:user, 42)
+ .ordered
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:runner, '123456789')
+ .ordered
+
+ env = {}
+
+ described_class.stick_or_unstick(env, :user, 42)
+ described_class.stick_or_unstick(env, :runner, '123456789')
+
+ expect(env[described_class::STICK_OBJECT].to_a).to eq([
+ [:user, 42],
+ [:runner, '123456789']
+ ])
+ end
+ end
+
+ describe '#call' do
+ it 'handles a request' do
+ env = {}
+
+ expect(middleware).to receive(:clear).twice
+
+ expect(middleware).to receive(:unstick_or_continue_sticking).with(env)
+ expect(middleware).to receive(:stick_if_necessary).with(env)
+
+ expect(app).to receive(:call).with(env).and_return(10)
+
+ expect(middleware.call(env)).to eq(10)
+ end
+ end
+
+ describe '#unstick_or_continue_sticking' do
+ it 'does not stick if no namespace and identifier could be found' do
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .not_to receive(:unstick_or_continue_sticking)
+
+ middleware.unstick_or_continue_sticking({})
+ end
+
+ it 'sticks to the primary if a warden user is found' do
+ env = { 'warden' => warden_user }
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:user, 42)
+
+ middleware.unstick_or_continue_sticking(env)
+ end
+
+ it 'sticks to the primary if a sticking namespace and identifier is found' do
+ env = { described_class::STICK_OBJECT => single_sticking_object }
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:user, 42)
+
+ middleware.unstick_or_continue_sticking(env)
+ end
+
+ it 'sticks to the primary if multiple sticking namespaces and identifiers were found' do
+ env = { described_class::STICK_OBJECT => multiple_sticking_objects }
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:user, 42)
+ .ordered
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:runner, '123456789')
+ .ordered
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:unstick_or_continue_sticking)
+ .with(:runner, '1234')
+ .ordered
+
+ middleware.unstick_or_continue_sticking(env)
+ end
+ end
+
+ describe '#stick_if_necessary' do
+ it 'does not stick to the primary if not necessary' do
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .not_to receive(:stick_if_necessary)
+
+ middleware.stick_if_necessary({})
+ end
+
+ it 'sticks to the primary if a warden user is found' do
+ env = { 'warden' => warden_user }
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:stick_if_necessary)
+ .with(:user, 42)
+
+ middleware.stick_if_necessary(env)
+ end
+
+ it 'sticks to the primary if a a single sticking object is found' do
+ env = { described_class::STICK_OBJECT => single_sticking_object }
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:stick_if_necessary)
+ .with(:user, 42)
+
+ middleware.stick_if_necessary(env)
+ end
+
+ it 'sticks to the primary if multiple sticking namespaces and identifiers were found' do
+ env = { described_class::STICK_OBJECT => multiple_sticking_objects }
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:stick_if_necessary)
+ .with(:user, 42)
+ .ordered
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:stick_if_necessary)
+ .with(:runner, '123456789')
+ .ordered
+
+ expect(Gitlab::Database::LoadBalancing::Sticking)
+ .to receive(:stick_if_necessary)
+ .with(:runner, '1234')
+ .ordered
+
+ middleware.stick_if_necessary(env)
+ end
+ end
+
+ describe '#clear' do
+ it 'clears the currently used host and session' do
+ lb = double(:lb)
+ session = double(:session)
+
+ allow(middleware).to receive(:load_balancer).and_return(lb)
+
+ expect(lb).to receive(:release_host)
+
+ stub_const('Gitlab::Database::LoadBalancing::RackMiddleware::Session',
+ session)
+
+ expect(session).to receive(:clear_session)
+
+ middleware.clear
+ end
+ end
+
+ describe '.load_balancer' do
+ it 'returns a the load balancer' do
+ proxy = double(:proxy)
+
+ expect(Gitlab::Database::LoadBalancing).to receive(:proxy)
+ .and_return(proxy)
+
+ expect(proxy).to receive(:load_balancer)
+
+ middleware.load_balancer
+ end
+ end
+
+ describe '#sticking_namespaces_and_ids' do
+ context 'using a Warden request' do
+ it 'returns the warden user if present' do
+ env = { 'warden' => warden_user }
+
+ expect(middleware.sticking_namespaces_and_ids(env)).to eq([[:user, 42]])
+ end
+
+ it 'returns an empty Array if no user was present' do
+ warden = double(:warden, user: nil)
+ env = { 'warden' => warden }
+
+ expect(middleware.sticking_namespaces_and_ids(env)).to eq([])
+ end
+ end
+
+ context 'using a request with a manually set sticking object' do
+ it 'returns the sticking object' do
+ env = { described_class::STICK_OBJECT => multiple_sticking_objects }
+
+ expect(middleware.sticking_namespaces_and_ids(env)).to eq([
+ [:user, 42],
+ [:runner, '123456789'],
+ [:runner, '1234']
+ ])
+ end
+ end
+
+ context 'using a regular request' do
+ it 'returns an empty Array' do
+ expect(middleware.sticking_namespaces_and_ids({})).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/resolver_spec.rb b/spec/lib/gitlab/database/load_balancing/resolver_spec.rb
new file mode 100644
index 00000000000..0051cf50255
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/resolver_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::Resolver do
+ describe '#resolve' do
+ let(:ip_addr) { IPAddr.new('127.0.0.2') }
+
+ context 'when nameserver is an IP' do
+ it 'returns an IPAddr object' do
+ service = described_class.new('127.0.0.2')
+
+ expect(service.resolve).to eq(ip_addr)
+ end
+ end
+
+ context 'when nameserver is not an IP' do
+ subject { described_class.new('localhost').resolve }
+
+ it 'looks the nameserver up in the hosts file' do
+ allow_next_instance_of(Resolv::Hosts) do |instance|
+ allow(instance).to receive(:getaddress).with('localhost').and_return('127.0.0.2')
+ end
+
+ expect(subject).to eq(ip_addr)
+ end
+
+ context 'when nameserver is not in the hosts file' do
+ it 'looks the nameserver up in DNS' do
+ resource = double(:resource, address: ip_addr)
+ packet = double(:packet, answer: [resource])
+
+ allow_next_instance_of(Resolv::Hosts) do |instance|
+ allow(instance).to receive(:getaddress).with('localhost').and_raise(Resolv::ResolvError)
+ end
+
+ allow(Net::DNS::Resolver).to receive(:start)
+ .with('localhost', Net::DNS::A)
+ .and_return(packet)
+
+ expect(subject).to eq(ip_addr)
+ end
+
+ context 'when nameserver is not in DNS' do
+ it 'raises an exception' do
+ allow_next_instance_of(Resolv::Hosts) do |instance|
+ allow(instance).to receive(:getaddress).with('localhost').and_raise(Resolv::ResolvError)
+ end
+
+ allow(Net::DNS::Resolver).to receive(:start)
+ .with('localhost', Net::DNS::A)
+ .and_return(double(:packet, answer: []))
+
+ expect { subject }.to raise_exception(
+ described_class::UnresolvableNameserverError,
+ 'could not resolve localhost'
+ )
+ end
+ end
+
+ context 'when DNS does not respond' do
+ it 'raises an exception' do
+ allow_next_instance_of(Resolv::Hosts) do |instance|
+ allow(instance).to receive(:getaddress).with('localhost').and_raise(Resolv::ResolvError)
+ end
+
+ allow(Net::DNS::Resolver).to receive(:start)
+ .with('localhost', Net::DNS::A)
+ .and_raise(Net::DNS::Resolver::NoResponseError)
+
+ expect { subject }.to raise_exception(
+ described_class::UnresolvableNameserverError,
+ 'no response from DNS server(s)'
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
new file mode 100644
index 00000000000..7fc7b5e8d11
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
@@ -0,0 +1,252 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
+ let(:service) do
+ described_class.new(nameserver: 'localhost', port: 8600, record: 'foo')
+ end
+
+ before do
+ resource = double(:resource, address: IPAddr.new('127.0.0.1'))
+ packet = double(:packet, answer: [resource])
+
+ allow(Net::DNS::Resolver).to receive(:start)
+ .with('localhost', Net::DNS::A)
+ .and_return(packet)
+ end
+
+ describe '#initialize' do
+ describe ':record_type' do
+ subject { described_class.new(nameserver: 'localhost', port: 8600, record: 'foo', record_type: record_type) }
+
+ context 'with a supported type' do
+ let(:record_type) { 'SRV' }
+
+ it { expect(subject.record_type).to eq Net::DNS::SRV }
+ end
+
+ context 'with an unsupported type' do
+ let(:record_type) { 'AAAA' }
+
+ it 'raises an argument error' do
+ expect { subject }.to raise_error(ArgumentError, 'Unsupported record type: AAAA')
+ end
+ end
+ end
+ end
+
+ describe '#start' do
+ before do
+ allow(service)
+ .to receive(:loop)
+ .and_yield
+ end
+
+ it 'starts service discovery in a new thread' do
+ expect(service)
+ .to receive(:refresh_if_necessary)
+ .and_return(5)
+
+ expect(service)
+ .to receive(:rand)
+ .and_return(2)
+
+ expect(service)
+ .to receive(:sleep)
+ .with(7)
+
+ service.start.join
+ end
+
+ it 'reports exceptions to Sentry' do
+ error = StandardError.new
+
+ expect(service)
+ .to receive(:refresh_if_necessary)
+ .and_raise(error)
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(error)
+
+ expect(service)
+ .to receive(:rand)
+ .and_return(2)
+
+ expect(service)
+ .to receive(:sleep)
+ .with(62)
+
+ service.start.join
+ end
+ end
+
+ describe '#refresh_if_necessary' do
+ let(:address_foo) { described_class::Address.new('foo') }
+ let(:address_bar) { described_class::Address.new('bar') }
+
+ context 'when a refresh is necessary' do
+ before do
+ allow(service)
+ .to receive(:addresses_from_load_balancer)
+ .and_return(%w[localhost])
+
+ allow(service)
+ .to receive(:addresses_from_dns)
+ .and_return([10, [address_foo, address_bar]])
+ end
+
+ it 'refreshes the load balancer hosts' do
+ expect(service)
+ .to receive(:replace_hosts)
+ .with([address_foo, address_bar])
+
+ expect(service.refresh_if_necessary).to eq(10)
+ end
+ end
+
+ context 'when a refresh is not necessary' do
+ before do
+ allow(service)
+ .to receive(:addresses_from_load_balancer)
+ .and_return(%w[localhost])
+
+ allow(service)
+ .to receive(:addresses_from_dns)
+ .and_return([10, %w[localhost]])
+ end
+
+ it 'does not refresh the load balancer hosts' do
+ expect(service)
+ .not_to receive(:replace_hosts)
+
+ expect(service.refresh_if_necessary).to eq(10)
+ end
+ end
+ end
+
+ describe '#replace_hosts' do
+ let(:address_foo) { described_class::Address.new('foo') }
+ let(:address_bar) { described_class::Address.new('bar') }
+
+ let(:load_balancer) do
+ Gitlab::Database::LoadBalancing::LoadBalancer.new([address_foo])
+ end
+
+ before do
+ allow(service)
+ .to receive(:load_balancer)
+ .and_return(load_balancer)
+ end
+
+ it 'replaces the hosts of the load balancer' do
+ service.replace_hosts([address_bar])
+
+ expect(load_balancer.host_list.host_names_and_ports).to eq([['bar', nil]])
+ end
+
+ it 'disconnects the old connections' do
+ host = load_balancer.host_list.hosts.first
+
+ allow(service)
+ .to receive(:disconnect_timeout)
+ .and_return(2)
+
+ expect(host)
+ .to receive(:disconnect!)
+ .with(2)
+
+ service.replace_hosts([address_bar])
+ end
+ end
+
+ describe '#addresses_from_dns' do
+ let(:service) { described_class.new(nameserver: 'localhost', port: 8600, record: 'foo', record_type: record_type) }
+ let(:packet) { double(:packet, answer: [res1, res2]) }
+
+ before do
+ allow(service.resolver)
+ .to receive(:search)
+ .with('foo', described_class::RECORD_TYPES[record_type])
+ .and_return(packet)
+ end
+
+ context 'with an A record' do
+ let(:record_type) { 'A' }
+
+ let(:res1) { double(:resource, address: IPAddr.new('255.255.255.0'), ttl: 90) }
+ let(:res2) { double(:resource, address: IPAddr.new('127.0.0.1'), ttl: 90) }
+
+ it 'returns a TTL and ordered list of IP addresses' do
+ addresses = [
+ described_class::Address.new('127.0.0.1'),
+ described_class::Address.new('255.255.255.0')
+ ]
+
+ expect(service.addresses_from_dns).to eq([90, addresses])
+ end
+ end
+
+ context 'with an SRV record' do
+ let(:record_type) { 'SRV' }
+
+ let(:res1) { double(:resource, host: 'foo1.service.consul.', port: 5432, weight: 1, priority: 1, ttl: 90) }
+ let(:res2) { double(:resource, host: 'foo2.service.consul.', port: 5433, weight: 1, priority: 1, ttl: 90) }
+ let(:res3) { double(:resource, host: 'foo3.service.consul.', port: 5434, weight: 1, priority: 1, ttl: 90) }
+ let(:packet) { double(:packet, answer: [res1, res2, res3], additional: []) }
+
+ before do
+ expect_next_instance_of(Gitlab::Database::LoadBalancing::SrvResolver) do |resolver|
+ allow(resolver).to receive(:address_for).with('foo1.service.consul.').and_return(IPAddr.new('255.255.255.0'))
+ allow(resolver).to receive(:address_for).with('foo2.service.consul.').and_return(IPAddr.new('127.0.0.1'))
+ allow(resolver).to receive(:address_for).with('foo3.service.consul.').and_return(nil)
+ end
+ end
+
+ it 'returns a TTL and ordered list of hosts' do
+ addresses = [
+ described_class::Address.new('127.0.0.1', 5433),
+ described_class::Address.new('255.255.255.0', 5432)
+ ]
+
+ expect(service.addresses_from_dns).to eq([90, addresses])
+ end
+ end
+ end
+
+ describe '#new_wait_time_for' do
+ it 'returns the DNS TTL if greater than the default interval' do
+ res = double(:resource, ttl: 90)
+
+ expect(service.new_wait_time_for([res])).to eq(90)
+ end
+
+ it 'returns the default interval if greater than the DNS TTL' do
+ res = double(:resource, ttl: 10)
+
+ expect(service.new_wait_time_for([res])).to eq(60)
+ end
+
+ it 'returns the default interval if no resources are given' do
+ expect(service.new_wait_time_for([])).to eq(60)
+ end
+ end
+
+ describe '#addresses_from_load_balancer' do
+ it 'returns the ordered host names of the load balancer' do
+ load_balancer = Gitlab::Database::LoadBalancing::LoadBalancer.new(%w[b a])
+
+ allow(service)
+ .to receive(:load_balancer)
+ .and_return(load_balancer)
+
+ addresses = [
+ described_class::Address.new('a'),
+ described_class::Address.new('b')
+ ]
+
+ expect(service.addresses_from_load_balancer).to eq(addresses)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/session_spec.rb b/spec/lib/gitlab/database/load_balancing/session_spec.rb
new file mode 100644
index 00000000000..74512f76fd4
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/session_spec.rb
@@ -0,0 +1,353 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::Session do
+ after do
+ described_class.clear_session
+ end
+
+ describe '.current' do
+ it 'returns the current session' do
+ expect(described_class.current).to be_an_instance_of(described_class)
+ end
+ end
+
+ describe '.clear_session' do
+ it 'clears the current session' do
+ described_class.current
+ described_class.clear_session
+
+ expect(RequestStore[described_class::CACHE_KEY]).to be_nil
+ end
+ end
+
+ describe '.without_sticky_writes' do
+ it 'ignores sticky write events sent by a connection proxy' do
+ described_class.without_sticky_writes do
+ described_class.current.write!
+ end
+
+ session = described_class.current
+
+ expect(session).not_to be_using_primary
+ end
+
+ it 'still is aware of write that happened' do
+ described_class.without_sticky_writes do
+ described_class.current.write!
+ end
+
+ session = described_class.current
+
+ expect(session.performed_write?).to be true
+ end
+ end
+
+ describe '#use_primary?' do
+ it 'returns true when the primary should be used' do
+ instance = described_class.new
+
+ instance.use_primary!
+
+ expect(instance.use_primary?).to eq(true)
+ end
+
+ it 'returns false when a secondary should be used' do
+ expect(described_class.new.use_primary?).to eq(false)
+ end
+
+ it 'returns true when a write was performed' do
+ instance = described_class.new
+
+ instance.write!
+
+ expect(instance.use_primary?).to eq(true)
+ end
+ end
+
+ describe '#use_primary' do
+ let(:instance) { described_class.new }
+
+ context 'when primary was used before' do
+ before do
+ instance.write!
+ end
+
+ it 'restores state after use' do
+ expect { |blk| instance.use_primary(&blk) }.to yield_with_no_args
+
+ expect(instance.use_primary?).to eq(true)
+ end
+ end
+
+ context 'when primary was not used' do
+ it 'restores state after use' do
+ expect { |blk| instance.use_primary(&blk) }.to yield_with_no_args
+
+ expect(instance.use_primary?).to eq(false)
+ end
+ end
+
+ it 'uses primary during block' do
+ expect do |blk|
+ instance.use_primary do
+ expect(instance.use_primary?).to eq(true)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+ end.to yield_control
+ end
+
+ it 'continues using primary when write was performed' do
+ instance.use_primary do
+ instance.write!
+ end
+
+ expect(instance.use_primary?).to eq(true)
+ end
+ end
+
+ describe '#performed_write?' do
+ it 'returns true if a write was performed' do
+ instance = described_class.new
+
+ instance.write!
+
+ expect(instance.performed_write?).to eq(true)
+ end
+ end
+
+ describe '#ignore_writes' do
+ it 'ignores write events' do
+ instance = described_class.new
+
+ instance.ignore_writes { instance.write! }
+
+ expect(instance).not_to be_using_primary
+ expect(instance.performed_write?).to eq true
+ end
+
+ it 'does not prevent using primary if an exception is raised' do
+ instance = described_class.new
+
+ instance.ignore_writes { raise ArgumentError } rescue ArgumentError
+ instance.write!
+
+ expect(instance).to be_using_primary
+ end
+ end
+
+ describe '#use_replicas_for_read_queries' do
+ let(:instance) { described_class.new }
+
+ it 'sets the flag inside the block' do
+ expect do |blk|
+ instance.use_replicas_for_read_queries do
+ expect(instance.use_replicas_for_read_queries?).to eq(true)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+ end.to yield_control
+
+ expect(instance.use_replicas_for_read_queries?).to eq(false)
+ end
+
+ it 'restores state after use' do
+ expect do |blk|
+ instance.use_replicas_for_read_queries do
+ instance.use_replicas_for_read_queries do
+ expect(instance.use_replicas_for_read_queries?).to eq(true)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+
+ expect(instance.use_replicas_for_read_queries?).to eq(true)
+ end
+ end.to yield_control
+
+ expect(instance.use_replicas_for_read_queries?).to eq(false)
+ end
+
+ context 'when primary was used before' do
+ before do
+ instance.use_primary!
+ end
+
+ it 'sets the flag inside the block' do
+ expect do |blk|
+ instance.use_replicas_for_read_queries do
+ expect(instance.use_replicas_for_read_queries?).to eq(true)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+ end.to yield_control
+
+ expect(instance.use_replicas_for_read_queries?).to eq(false)
+ end
+ end
+
+ context 'when a write query is performed before' do
+ before do
+ instance.write!
+ end
+
+ it 'sets the flag inside the block' do
+ expect do |blk|
+ instance.use_replicas_for_read_queries do
+ expect(instance.use_replicas_for_read_queries?).to eq(true)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+ end.to yield_control
+
+ expect(instance.use_replicas_for_read_queries?).to eq(false)
+ end
+ end
+ end
+
+ describe '#fallback_to_replicas_for_ambiguous_queries' do
+ let(:instance) { described_class.new }
+
+ it 'sets the flag inside the block' do
+ expect do |blk|
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(true)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+ end.to yield_control
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ it 'restores state after use' do
+ expect do |blk|
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(true)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(true)
+ end
+ end.to yield_control
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ context 'when primary was used before' do
+ before do
+ instance.use_primary!
+ end
+
+ it 'uses primary during block' do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+
+ expect do |blk|
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+ end.to yield_control
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+ end
+
+ context 'when a write was performed before' do
+ before do
+ instance.write!
+ end
+
+ it 'uses primary during block' do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+
+ expect do |blk|
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+
+ # call yield probe
+ blk.to_proc.call
+ end
+ end.to yield_control
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+ end
+
+ context 'when primary was used inside the block' do
+ it 'uses primary aterward' do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(true)
+
+ instance.use_primary!
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ it 'restores state after use' do
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(true)
+
+ instance.use_primary!
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+ end
+
+ context 'when a write was performed inside the block' do
+ it 'uses primary aterward' do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(true)
+
+ instance.write!
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ it 'restores state after use' do
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ instance.fallback_to_replicas_for_ambiguous_queries do
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(true)
+
+ instance.write!
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+
+ expect(instance.fallback_to_replicas_for_ambiguous_queries?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
new file mode 100644
index 00000000000..90051172fca
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_client_middleware_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::SidekiqClientMiddleware do
+ let(:middleware) { described_class.new }
+
+ after do
+ Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+
+ describe '#call' do
+ shared_context 'data consistency worker class' do |data_consistency, feature_flag|
+ let(:worker_class) do
+ Class.new do
+ def self.name
+ 'TestDataConsistencyWorker'
+ end
+
+ include ApplicationWorker
+
+ data_consistency data_consistency, feature_flag: feature_flag
+
+ def perform(*args)
+ end
+ end
+ end
+
+ before do
+ stub_const('TestDataConsistencyWorker', worker_class)
+ end
+ end
+
+ shared_examples_for 'does not pass database locations' do
+ it 'does not pass database locations', :aggregate_failures do
+ middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
+
+ expect(job['database_replica_location']).to be_nil
+ expect(job['database_write_location']).to be_nil
+ end
+ end
+
+ shared_examples_for 'mark data consistency location' do |data_consistency|
+ include_context 'data consistency worker class', data_consistency, :load_balancing_for_test_data_consistency_worker
+
+ let(:location) { '0/D525E3A8' }
+
+ context 'when feature flag load_balancing_for_sidekiq is disabled' do
+ before do
+ stub_feature_flags(load_balancing_for_test_data_consistency_worker: false)
+ end
+
+ include_examples 'does not pass database locations'
+ end
+
+ context 'when write was not performed' do
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(false)
+ end
+
+ it 'passes database_replica_location' do
+ expect(middleware).to receive_message_chain(:load_balancer, :host, "database_replica_location").and_return(location)
+
+ middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
+
+ expect(job['database_replica_location']).to eq(location)
+ end
+ end
+
+ context 'when write was performed' do
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(true)
+ end
+
+ it 'passes primary write location', :aggregate_failures do
+ expect(middleware).to receive_message_chain(:load_balancer, :primary_write_location).and_return(location)
+
+ middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
+
+ expect(job['database_write_location']).to eq(location)
+ end
+ end
+ end
+
+ shared_examples_for 'database location was already provided' do |provided_database_location, other_location|
+ shared_examples_for 'does not set database location again' do |use_primary|
+ before do
+ allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary?).and_return(use_primary)
+ end
+
+ it 'does not set database locations again' do
+ middleware.call(worker_class, job, double(:queue), redis_pool) { 10 }
+
+ expect(job[provided_database_location]).to eq(old_location)
+ expect(job[other_location]).to be_nil
+ end
+ end
+
+ let(:old_location) { '0/D525E3A8' }
+ let(:new_location) { 'AB/12345' }
+ let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", provided_database_location => old_location } }
+
+ before do
+ allow(middleware).to receive_message_chain(:load_balancer, :primary_write_location).and_return(new_location)
+ allow(middleware).to receive_message_chain(:load_balancer, :database_replica_location).and_return(new_location)
+ end
+
+ context "when write was performed" do
+ include_examples 'does not set database location again', true
+ end
+
+ context "when write was not performed" do
+ include_examples 'does not set database location again', false
+ end
+ end
+
+ let(:queue) { 'default' }
+ let(:redis_pool) { Sidekiq.redis_pool }
+ let(:worker_class) { 'TestDataConsistencyWorker' }
+ let(:job) { { "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e" } }
+
+ before do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
+ end
+
+ context 'when worker cannot be constantized' do
+ let(:worker_class) { 'ActionMailer::MailDeliveryJob' }
+
+ include_examples 'does not pass database locations'
+ end
+
+ context 'when worker class does not include ApplicationWorker' do
+ let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
+
+ include_examples 'does not pass database locations'
+ end
+
+ context 'database write location was already provided' do
+ include_examples 'database location was already provided', 'database_write_location', 'database_replica_location'
+ end
+
+ context 'database replica location was already provided' do
+ include_examples 'database location was already provided', 'database_replica_location', 'database_write_location'
+ end
+
+ context 'when worker data consistency is :always' do
+ include_context 'data consistency worker class', :always, :load_balancing_for_test_data_consistency_worker
+
+ include_examples 'does not pass database locations'
+ end
+
+ context 'when worker data consistency is :delayed' do
+ include_examples 'mark data consistency location', :delayed
+ end
+
+ context 'when worker data consistency is :sticky' do
+ include_examples 'mark data consistency location', :sticky
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
new file mode 100644
index 00000000000..b7cd0caa922
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware do
+ let(:middleware) { described_class.new }
+
+ after do
+ Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+
+ describe '#call' do
+ shared_context 'data consistency worker class' do |data_consistency, feature_flag|
+ let(:worker_class) do
+ Class.new do
+ def self.name
+ 'TestDataConsistencyWorker'
+ end
+
+ include ApplicationWorker
+
+ data_consistency data_consistency, feature_flag: feature_flag
+
+ def perform(*args)
+ end
+ end
+ end
+
+ before do
+ stub_const('TestDataConsistencyWorker', worker_class)
+ end
+ end
+
+ shared_examples_for 'stick to the primary' do
+ it 'sticks to the primary' do
+ middleware.call(worker, job, double(:queue)) do
+ expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?).to be_truthy
+ end
+ end
+ end
+
+ shared_examples_for 'replica is up to date' do |location, data_consistency|
+ it 'does not stick to the primary', :aggregate_failures do
+ expect(middleware).to receive(:replica_caught_up?).with(location).and_return(true)
+
+ middleware.call(worker, job, double(:queue)) do
+ expect(Gitlab::Database::LoadBalancing::Session.current.use_primary?).not_to be_truthy
+ end
+
+ expect(job[:database_chosen]).to eq('replica')
+ end
+
+ it "updates job hash with data_consistency :#{data_consistency}" do
+ middleware.call(worker, job, double(:queue)) do
+ expect(job).to include(data_consistency: data_consistency.to_s)
+ end
+ end
+ end
+
+ shared_examples_for 'sticks based on data consistency' do |data_consistency|
+ include_context 'data consistency worker class', data_consistency, :load_balancing_for_test_data_consistency_worker
+
+ context 'when load_balancing_for_test_data_consistency_worker is disabled' do
+ before do
+ stub_feature_flags(load_balancing_for_test_data_consistency_worker: false)
+ end
+
+ include_examples 'stick to the primary'
+ end
+
+ context 'when database replica location is set' do
+ let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'database_replica_location' => '0/D525E3A8' } }
+
+ before do
+ allow(middleware).to receive(:replica_caught_up?).and_return(true)
+ end
+
+ it_behaves_like 'replica is up to date', '0/D525E3A8', data_consistency
+ end
+
+ context 'when database primary location is set' do
+ let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e', 'database_write_location' => '0/D525E3A8' } }
+
+ before do
+ allow(middleware).to receive(:replica_caught_up?).and_return(true)
+ end
+
+ it_behaves_like 'replica is up to date', '0/D525E3A8', data_consistency
+ end
+
+ context 'when database location is not set' do
+ let(:job) { { 'job_id' => 'a180b47c-3fd6-41b8-81e9-34da61c3400e' } }
+
+ it_behaves_like 'stick to the primary', nil
+ end
+ end
+
+ let(:queue) { 'default' }
+ let(:redis_pool) { Sidekiq.redis_pool }
+ let(:worker) { worker_class.new }
+ let(:job) { { "retry" => 3, "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e", 'database_replica_location' => '0/D525E3A8' } }
+ let(:block) { 10 }
+
+ before do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
+ allow(middleware).to receive(:clear)
+ allow(Gitlab::Database::LoadBalancing::Session.current).to receive(:performed_write?).and_return(true)
+ end
+
+ context 'when worker class does not include ApplicationWorker' do
+ let(:worker) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper.new }
+
+ include_examples 'stick to the primary'
+ end
+
+ context 'when worker data consistency is :always' do
+ include_context 'data consistency worker class', :always, :load_balancing_for_test_data_consistency_worker
+
+ include_examples 'stick to the primary'
+ end
+
+ context 'when worker data consistency is :delayed' do
+ include_examples 'sticks based on data consistency', :delayed
+
+ context 'when replica is not up to date' do
+ before do
+ allow(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer, :release_host)
+ allow(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer, :select_up_to_date_host).and_return(false)
+ end
+
+ around do |example|
+ with_sidekiq_server_middleware do |chain|
+ chain.add described_class
+ Sidekiq::Testing.disable! { example.run }
+ end
+ end
+
+ context 'when job is executed first' do
+ it 'raise an error and retries', :aggregate_failures do
+ expect do
+ process_job(job)
+ end.to raise_error(Sidekiq::JobRetry::Skip)
+
+ expect(job['error_class']).to eq('Gitlab::Database::LoadBalancing::SidekiqServerMiddleware::JobReplicaNotUpToDate')
+ expect(job[:database_chosen]).to eq('retry')
+ end
+ end
+
+ context 'when job is retried' do
+ it 'stick to the primary', :aggregate_failures do
+ expect do
+ process_job(job)
+ end.to raise_error(Sidekiq::JobRetry::Skip)
+
+ process_job(job)
+ expect(job[:database_chosen]).to eq('primary')
+ end
+ end
+
+ context 'replica selection mechanism feature flag rollout' do
+ before do
+ stub_feature_flags(sidekiq_load_balancing_rotate_up_to_date_replica: false)
+ end
+
+ it 'uses different implmentation' do
+ expect(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer, :host, :caught_up?).and_return(false)
+
+ expect do
+ process_job(job)
+ end.to raise_error(Sidekiq::JobRetry::Skip)
+ end
+ end
+ end
+ end
+
+ context 'when worker data consistency is :sticky' do
+ include_examples 'sticks based on data consistency', :sticky
+
+ context 'when replica is not up to date' do
+ before do
+ allow(middleware).to receive(:replica_caught_up?).and_return(false)
+ end
+
+ include_examples 'stick to the primary'
+
+ it 'updates job hash with primary database chosen', :aggregate_failures do
+ expect { |b| middleware.call(worker, job, double(:queue), &b) }.to yield_control
+
+ expect(job[:database_chosen]).to eq('primary')
+ end
+ end
+ end
+ end
+
+ def process_job(job)
+ Sidekiq::JobRetry.new.local(worker_class, job, queue) do
+ worker_class.process_job(job)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/srv_resolver_spec.rb b/spec/lib/gitlab/database/load_balancing/srv_resolver_spec.rb
new file mode 100644
index 00000000000..6ac0608d485
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/srv_resolver_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::SrvResolver do
+ let(:resolver) { Net::DNS::Resolver.new(nameservers: '127.0.0.1', port: 8600, use_tcp: true) }
+ let(:additional) { dns_response_packet_from_fixture('srv_with_a_rr_in_additional_section').additional }
+
+ describe '#address_for' do
+ let(:host) { 'patroni-02-db-gstg.node.east-us-2.consul.' }
+
+ subject { described_class.new(resolver, additional).address_for(host) }
+
+ context 'when additional section contains an A record' do
+ it 'returns an IP4 address' do
+ expect(subject).to eq(IPAddr.new('10.224.29.102'))
+ end
+ end
+
+ context 'when additional section contains an AAAA record' do
+ let(:host) { 'a.gtld-servers.net.' }
+ let(:additional) { dns_response_packet_from_fixture('a_with_aaaa_rr_in_additional_section').additional }
+
+ it 'returns an IP6 address' do
+ expect(subject).to eq(IPAddr.new('2001:503:a83e::2:30'))
+ end
+ end
+
+ context 'when additional section does not contain A nor AAAA records' do
+ let(:additional) { [] }
+
+ context 'when host resolves to an A record' do
+ before do
+ allow(resolver).to receive(:search).with(host, Net::DNS::ANY).and_return(dns_response_packet_from_fixture('a_rr'))
+ end
+
+ it 'returns an IP4 address' do
+ expect(subject).to eq(IPAddr.new('10.224.29.102'))
+ end
+ end
+
+ context 'when host does resolves to an AAAA record' do
+ before do
+ allow(resolver).to receive(:search).with(host, Net::DNS::ANY).and_return(dns_response_packet_from_fixture('aaaa_rr'))
+ end
+
+ it 'returns an IP6 address' do
+ expect(subject).to eq(IPAddr.new('2a00:1450:400e:80a::200e'))
+ end
+ end
+ end
+ end
+
+ def dns_response_packet_from_fixture(fixture_name)
+ fixture = File.read(Rails.root + "spec/fixtures/dns/#{fixture_name}.json")
+ encoded_payload = Gitlab::Json.parse(fixture)['payload']
+ payload = Base64.decode64(encoded_payload)
+
+ Net::DNS::Packet.parse(payload)
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing/sticking_spec.rb b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
new file mode 100644
index 00000000000..bf4e3756e0e
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/sticking_spec.rb
@@ -0,0 +1,307 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing::Sticking, :redis do
+ after do
+ Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+
+ describe '.stick_if_necessary' do
+ context 'when sticking is disabled' do
+ it 'does not perform any sticking' do
+ expect(described_class).not_to receive(:stick)
+
+ described_class.stick_if_necessary(:user, 42)
+ end
+ end
+
+ context 'when sticking is enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?)
+ .and_return(true)
+ end
+
+ it 'does not stick if no write was performed' do
+ allow(Gitlab::Database::LoadBalancing::Session.current)
+ .to receive(:performed_write?)
+ .and_return(false)
+
+ expect(described_class).not_to receive(:stick)
+
+ described_class.stick_if_necessary(:user, 42)
+ 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)
+
+ expect(described_class).to receive(:stick).with(:user, 42)
+
+ described_class.stick_if_necessary(:user, 42)
+ end
+ end
+ end
+
+ describe '.all_caught_up?' do
+ let(:lb) { double(:lb) }
+
+ before do
+ allow(described_class).to receive(:load_balancer).and_return(lb)
+ end
+
+ it 'returns true if no write location could be found' do
+ allow(described_class).to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return(nil)
+
+ expect(lb).not_to receive(:all_caught_up?)
+
+ expect(described_class.all_caught_up?(:user, 42)).to eq(true)
+ end
+
+ it 'returns true, and unsticks if all secondaries have caught up' do
+ allow(described_class).to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return('foo')
+
+ allow(lb).to receive(:all_caught_up?).with('foo').and_return(true)
+
+ expect(described_class).to receive(:unstick).with(:user, 42)
+
+ expect(described_class.all_caught_up?(:user, 42)).to eq(true)
+ end
+
+ it 'return false if the secondaries have not yet caught up' do
+ allow(described_class).to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return('foo')
+
+ allow(lb).to receive(:all_caught_up?).with('foo').and_return(false)
+
+ expect(described_class.all_caught_up?(:user, 42)).to eq(false)
+ end
+ end
+
+ describe '.unstick_or_continue_sticking' do
+ let(:lb) { double(:lb) }
+
+ before do
+ allow(described_class).to receive(:load_balancer).and_return(lb)
+ end
+
+ it 'simply returns if no write location could be found' do
+ allow(described_class).to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return(nil)
+
+ expect(lb).not_to receive(:all_caught_up?)
+
+ described_class.unstick_or_continue_sticking(:user, 42)
+ end
+
+ it 'unsticks if all secondaries have caught up' do
+ allow(described_class).to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return('foo')
+
+ allow(lb).to receive(:all_caught_up?).with('foo').and_return(true)
+
+ expect(described_class).to receive(:unstick).with(:user, 42)
+
+ described_class.unstick_or_continue_sticking(:user, 42)
+ end
+
+ it 'continues using the primary if the secondaries have not yet caught up' do
+ allow(described_class).to receive(:last_write_location_for)
+ .with(:user, 42)
+ .and_return('foo')
+
+ allow(lb).to receive(:all_caught_up?).with('foo').and_return(false)
+
+ expect(Gitlab::Database::LoadBalancing::Session.current)
+ .to receive(:use_primary!)
+
+ described_class.unstick_or_continue_sticking(:user, 42)
+ end
+ end
+
+ RSpec.shared_examples 'sticking' do
+ context 'when sticking is disabled' do
+ it 'does not perform any sticking', :aggregate_failures do
+ expect(described_class).not_to receive(:set_write_location_for)
+ expect(Gitlab::Database::LoadBalancing::Session.current).not_to receive(:use_primary!)
+
+ described_class.bulk_stick(:user, ids)
+ end
+ end
+
+ context 'when sticking is enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:configured?).and_return(true)
+
+ lb = double(:lb, primary_write_location: 'foo')
+
+ allow(described_class).to receive(:load_balancer).and_return(lb)
+ end
+
+ it 'sticks an entity to the primary', :aggregate_failures do
+ ids.each do |id|
+ expect(described_class).to receive(:set_write_location_for)
+ .with(:user, id, 'foo')
+ end
+
+ expect(Gitlab::Database::LoadBalancing::Session.current)
+ .to receive(:use_primary!)
+
+ subject
+ end
+ end
+ end
+
+ describe '.stick' do
+ it_behaves_like 'sticking' do
+ let(:ids) { [42] }
+ subject { described_class.stick(:user, ids.first) }
+ end
+ end
+
+ describe '.bulk_stick' do
+ it_behaves_like 'sticking' do
+ let(:ids) { [42, 43] }
+ subject { described_class.bulk_stick(:user, ids) }
+ end
+ end
+
+ describe '.mark_primary_write_location' do
+ context 'when enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ allow(Gitlab::Database::LoadBalancing).to receive(:configured?).and_return(true)
+ end
+
+ it 'updates the write location with the load balancer' do
+ lb = double(:lb, primary_write_location: 'foo')
+
+ allow(described_class).to receive(:load_balancer).and_return(lb)
+
+ expect(described_class).to receive(:set_write_location_for)
+ .with(:user, 42, 'foo')
+
+ described_class.mark_primary_write_location(:user, 42)
+ end
+ end
+
+ context 'when load balancing is configured but not enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+ allow(Gitlab::Database::LoadBalancing).to receive(:configured?).and_return(true)
+ end
+
+ it 'updates the write location with the main ActiveRecord connection' do
+ allow(described_class).to receive(:load_balancer).and_return(nil)
+ expect(ActiveRecord::Base).to receive(:connection).and_call_original
+ expect(described_class).to receive(:set_write_location_for)
+ .with(:user, 42, anything)
+
+ described_class.mark_primary_write_location(:user, 42)
+ end
+
+ context 'when write location is nil' do
+ before do
+ allow(Gitlab::Database).to receive(:get_write_location).and_return(nil)
+ end
+
+ it 'does not update the write location' do
+ expect(described_class).not_to receive(:set_write_location_for)
+
+ described_class.mark_primary_write_location(:user, 42)
+ end
+ end
+ end
+
+ context 'when load balancing is disabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+ allow(Gitlab::Database::LoadBalancing).to receive(:configured?).and_return(false)
+ end
+
+ it 'updates the write location with the main ActiveRecord connection' do
+ expect(described_class).not_to receive(:set_write_location_for)
+
+ described_class.mark_primary_write_location(:user, 42)
+ end
+ end
+ end
+
+ describe '.unstick' do
+ it 'removes the sticking data from Redis' do
+ described_class.set_write_location_for(:user, 4, 'foo')
+ described_class.unstick(:user, 4)
+
+ expect(described_class.last_write_location_for(:user, 4)).to be_nil
+ end
+ end
+
+ describe '.last_write_location_for' do
+ it 'returns the last WAL write location for a user' do
+ described_class.set_write_location_for(:user, 4, 'foo')
+
+ expect(described_class.last_write_location_for(:user, 4)).to eq('foo')
+ end
+ end
+
+ describe '.redis_key_for' do
+ it 'returns a String' do
+ expect(described_class.redis_key_for(:user, 42))
+ .to eq('database-load-balancing/write-location/user/42')
+ end
+ end
+
+ describe '.load_balancer' do
+ it 'returns a the load balancer' do
+ proxy = double(:proxy)
+
+ expect(Gitlab::Database::LoadBalancing).to receive(:proxy)
+ .and_return(proxy)
+
+ expect(proxy).to receive(:load_balancer)
+
+ described_class.load_balancer
+ end
+ end
+
+ describe '.select_caught_up_replicas' do
+ let(:lb) { double(:lb) }
+
+ before do
+ allow(described_class).to receive(:load_balancer).and_return(lb)
+ end
+
+ context 'with no write location' do
+ before do
+ allow(described_class).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(described_class).not_to receive(:select_caught_up_hosts)
+ expect(described_class.select_caught_up_replicas(:project, 42)).to be false
+ end
+ end
+
+ context 'with write location' do
+ before do
+ allow(described_class).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_caught_up_hosts).and_return(true)
+ expect(described_class).to receive(:unstick).with(:project, 42)
+ expect(described_class.select_caught_up_replicas(:project, 42)).to be true
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/load_balancing_spec.rb b/spec/lib/gitlab/database/load_balancing_spec.rb
new file mode 100644
index 00000000000..e7de7f2b43b
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing_spec.rb
@@ -0,0 +1,834 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::LoadBalancing do
+ include_context 'clear DB Load Balancing configuration'
+
+ before do
+ stub_env('ENABLE_LOAD_BALANCING_FOR_FOSS', 'true')
+ end
+
+ describe '.proxy' do
+ context 'when configured' do
+ before do
+ allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+ subject.configure_proxy
+ end
+
+ it 'returns the connection proxy' do
+ expect(subject.proxy).to be_an_instance_of(subject::ConnectionProxy)
+ end
+ end
+
+ context 'when not configured' do
+ it 'returns nil' do
+ expect(subject.proxy).to be_nil
+ end
+
+ it 'tracks an error to sentry' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ an_instance_of(subject::ProxyNotConfiguredError)
+ )
+
+ subject.proxy
+ end
+ end
+ end
+
+ describe '.configuration' do
+ it 'returns a Hash' do
+ lb_config = { 'hosts' => %w(foo) }
+
+ original_db_config = Gitlab::Database.config
+ modified_db_config = original_db_config.merge(load_balancing: lb_config)
+ expect(Gitlab::Database).to receive(:config).and_return(modified_db_config)
+
+ expect(described_class.configuration).to eq(lb_config)
+ end
+ end
+
+ describe '.max_replication_difference' do
+ context 'without an explicitly configured value' do
+ it 'returns the default value' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return({})
+
+ expect(described_class.max_replication_difference).to eq(8.megabytes)
+ end
+ end
+
+ context 'with an explicitly configured value' do
+ it 'returns the configured value' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return({ 'max_replication_difference' => 4 })
+
+ expect(described_class.max_replication_difference).to eq(4)
+ end
+ end
+ end
+
+ describe '.max_replication_lag_time' do
+ context 'without an explicitly configured value' do
+ it 'returns the default value' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return({})
+
+ expect(described_class.max_replication_lag_time).to eq(60)
+ end
+ end
+
+ context 'with an explicitly configured value' do
+ it 'returns the configured value' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return({ 'max_replication_lag_time' => 4 })
+
+ expect(described_class.max_replication_lag_time).to eq(4)
+ end
+ end
+ end
+
+ describe '.replica_check_interval' do
+ context 'without an explicitly configured value' do
+ it 'returns the default value' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return({})
+
+ expect(described_class.replica_check_interval).to eq(60)
+ end
+ end
+
+ context 'with an explicitly configured value' do
+ it 'returns the configured value' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return({ 'replica_check_interval' => 4 })
+
+ expect(described_class.replica_check_interval).to eq(4)
+ end
+ end
+ end
+
+ describe '.hosts' do
+ it 'returns a list of hosts' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return({ 'hosts' => %w(foo bar baz) })
+
+ expect(described_class.hosts).to eq(%w(foo bar baz))
+ end
+ end
+
+ describe '.pool_size' do
+ it 'returns a Fixnum' do
+ expect(described_class.pool_size).to be_a_kind_of(Integer)
+ end
+ end
+
+ describe '.enable?' do
+ before do
+ clear_load_balancing_configuration
+ allow(described_class).to receive(:hosts).and_return(%w(foo))
+ end
+
+ it 'returns false when no hosts are specified' do
+ allow(described_class).to receive(:hosts).and_return([])
+
+ expect(described_class.enable?).to eq(false)
+ end
+
+ it 'returns false when Sidekiq is being used' do
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+
+ expect(described_class.enable?).to eq(false)
+ end
+
+ it 'returns false when running inside a Rake task' do
+ allow(Gitlab::Runtime).to receive(:rake?).and_return(true)
+
+ expect(described_class.enable?).to eq(false)
+ end
+
+ it 'returns true when load balancing should be enabled' do
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(false)
+
+ expect(described_class.enable?).to eq(true)
+ end
+
+ it 'returns true when service discovery is enabled' do
+ allow(described_class).to receive(:hosts).and_return([])
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(false)
+
+ allow(described_class)
+ .to receive(:service_discovery_enabled?)
+ .and_return(true)
+
+ expect(described_class.enable?).to eq(true)
+ end
+
+ context 'when ENABLE_LOAD_BALANCING_FOR_SIDEKIQ environment variable is set' do
+ before do
+ stub_env('ENABLE_LOAD_BALANCING_FOR_SIDEKIQ', 'true')
+ end
+
+ it 'returns true when Sidekiq is being used' do
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+
+ expect(described_class.enable?).to eq(true)
+ end
+ end
+ end
+
+ describe '.configured?' do
+ before do
+ clear_load_balancing_configuration
+ end
+
+ it 'returns true when Sidekiq is being used' do
+ allow(described_class).to receive(:hosts).and_return(%w(foo))
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+ expect(described_class.configured?).to eq(true)
+ end
+
+ it 'returns true when service discovery is enabled in Sidekiq' do
+ allow(described_class).to receive(:hosts).and_return([])
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+
+ allow(described_class)
+ .to receive(:service_discovery_enabled?)
+ .and_return(true)
+
+ expect(described_class.configured?).to eq(true)
+ end
+
+ it 'returns false when neither service discovery nor hosts are configured' do
+ allow(described_class).to receive(:hosts).and_return([])
+
+ allow(described_class)
+ .to receive(:service_discovery_enabled?)
+ .and_return(false)
+
+ expect(described_class.configured?).to eq(false)
+ end
+ end
+
+ describe '.configure_proxy' do
+ it 'configures the connection proxy' do
+ allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+
+ described_class.configure_proxy
+
+ expect(ActiveRecord::Base.singleton_class).to have_received(:prepend)
+ .with(Gitlab::Database::LoadBalancing::ActiveRecordProxy)
+ end
+ end
+
+ describe '.active_record_models' do
+ it 'returns an Array' do
+ expect(described_class.active_record_models).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '.service_discovery_enabled?' do
+ it 'returns true if service discovery is enabled' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return('discover' => { 'record' => 'foo' })
+
+ expect(described_class.service_discovery_enabled?).to eq(true)
+ end
+
+ it 'returns false if service discovery is disabled' do
+ expect(described_class.service_discovery_enabled?).to eq(false)
+ end
+ end
+
+ describe '.service_discovery_configuration' do
+ context 'when no configuration is provided' do
+ it 'returns a default configuration Hash' do
+ expect(described_class.service_discovery_configuration).to eq(
+ nameserver: 'localhost',
+ port: 8600,
+ record: nil,
+ record_type: 'A',
+ interval: 60,
+ disconnect_timeout: 120,
+ use_tcp: false
+ )
+ end
+ end
+
+ context 'when configuration is provided' do
+ it 'returns a Hash including the custom configuration' do
+ allow(described_class)
+ .to receive(:configuration)
+ .and_return('discover' => { 'record' => 'foo', 'record_type' => 'SRV' })
+
+ expect(described_class.service_discovery_configuration).to eq(
+ nameserver: 'localhost',
+ port: 8600,
+ record: 'foo',
+ record_type: 'SRV',
+ interval: 60,
+ disconnect_timeout: 120,
+ use_tcp: false
+ )
+ end
+ end
+ end
+
+ describe '.start_service_discovery' do
+ it 'does not start if service discovery is disabled' do
+ expect(Gitlab::Database::LoadBalancing::ServiceDiscovery)
+ .not_to receive(:new)
+
+ described_class.start_service_discovery
+ end
+
+ it 'starts service discovery if enabled' do
+ allow(described_class)
+ .to receive(:service_discovery_enabled?)
+ .and_return(true)
+
+ instance = double(:instance)
+
+ expect(Gitlab::Database::LoadBalancing::ServiceDiscovery)
+ .to receive(:new)
+ .with(an_instance_of(Hash))
+ .and_return(instance)
+
+ expect(instance)
+ .to receive(:start)
+
+ described_class.start_service_discovery
+ end
+ end
+
+ describe '.db_role_for_connection' do
+ let(:connection) { double(:conneciton) }
+
+ context 'when the load balancing is not configured' do
+ before do
+ allow(described_class).to receive(:enable?).and_return(false)
+ end
+
+ it 'returns primary' do
+ expect(described_class.db_role_for_connection(connection)).to be(:primary)
+ end
+ end
+
+ context 'when the load balancing is configured' do
+ let(:proxy) { described_class::ConnectionProxy.new(%w(foo)) }
+ let(:load_balancer) { described_class::LoadBalancer.new(%w(foo)) }
+
+ before do
+ allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+
+ allow(described_class).to receive(:enable?).and_return(true)
+ allow(described_class).to receive(:proxy).and_return(proxy)
+ allow(proxy).to receive(:load_balancer).and_return(load_balancer)
+
+ subject.configure_proxy(proxy)
+ end
+
+ context 'when the load balancer returns :replica' do
+ it 'returns :replica' do
+ allow(load_balancer).to receive(:db_role_for_connection).and_return(:replica)
+
+ expect(described_class.db_role_for_connection(connection)).to be(:replica)
+
+ expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
+ end
+ end
+
+ context 'when the load balancer returns :primary' do
+ it 'returns :primary' do
+ allow(load_balancer).to receive(:db_role_for_connection).and_return(:primary)
+
+ expect(described_class.db_role_for_connection(connection)).to be(:primary)
+
+ expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
+ end
+ end
+
+ context 'when the load balancer returns nil' do
+ it 'returns nil' do
+ allow(load_balancer).to receive(:db_role_for_connection).and_return(nil)
+
+ expect(described_class.db_role_for_connection(connection)).to be(nil)
+
+ expect(load_balancer).to have_received(:db_role_for_connection).with(connection)
+ end
+ end
+ end
+ end
+
+ # For such an important module like LoadBalancing, full mocking is not
+ # enough. This section implements some integration tests to test a full flow
+ # of the load balancer.
+ # - A real model with a table backed behind is defined
+ # - The load balancing module is set up for this module only, as to prevent
+ # breaking other tests. The replica configuration is cloned from the test
+ # configuraiton.
+ # - In each test, we listen to the SQL queries (via sql.active_record
+ # instrumentation) while triggering real queries from the defined model.
+ # - We assert the desinations (replica/primary) of the queries in order.
+ describe 'LoadBalancing integration tests', :delete do
+ before(:all) do
+ ActiveRecord::Schema.define do
+ create_table :load_balancing_test, force: true do |t|
+ t.string :name, null: true
+ end
+ end
+ end
+
+ after(:all) do
+ ActiveRecord::Schema.define do
+ drop_table :load_balancing_test, force: true
+ end
+ end
+
+ shared_context 'LoadBalancing setup' do
+ let(:development_db_config) { ActiveRecord::Base.configurations.configs_for(env_name: 'development').first.configuration_hash }
+ let(:hosts) { [development_db_config[:host]] }
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = "load_balancing_test"
+ end
+ end
+
+ before do
+ # Preloading testing class
+ model.singleton_class.prepend ::Gitlab::Database::LoadBalancing::ActiveRecordProxy
+
+ # Setup load balancing
+ clear_load_balancing_configuration
+ allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+ subject.configure_proxy(::Gitlab::Database::LoadBalancing::ConnectionProxy.new(hosts))
+
+ original_db_config = Gitlab::Database.config
+ modified_db_config = original_db_config.merge(load_balancing: { hosts: hosts })
+ allow(Gitlab::Database).to receive(:config).and_return(modified_db_config)
+
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+ end
+
+ where(:queries, :include_transaction, :expected_results) do
+ [
+ # Read methods
+ [-> { model.first }, false, [:replica]],
+ [-> { model.find_by(id: 123) }, false, [:replica]],
+ [-> { model.where(name: 'hello').to_a }, false, [:replica]],
+
+ # Write methods
+ [-> { model.create!(name: 'test1') }, false, [:primary]],
+ [
+ -> {
+ instance = model.create!(name: 'test1')
+ instance.update!(name: 'test2')
+ },
+ false, [:primary, :primary]
+ ],
+ [-> { model.update_all(name: 'test2') }, false, [:primary]],
+ [
+ -> {
+ instance = model.create!(name: 'test1')
+ instance.destroy!
+ },
+ false, [:primary, :primary]
+ ],
+ [-> { model.delete_all }, false, [:primary]],
+
+ # Custom query
+ [-> { model.connection.exec_query('SELECT 1').to_a }, false, [:primary]],
+
+ # Reads after a write
+ [
+ -> {
+ model.first
+ model.create!(name: 'test1')
+ model.first
+ model.find_by(name: 'test1')
+ },
+ false, [:replica, :primary, :primary, :primary]
+ ],
+
+ # Inside a transaction
+ [
+ -> {
+ model.transaction do
+ model.find_by(name: 'test1')
+ model.create!(name: 'test1')
+ instance = model.find_by(name: 'test1')
+ instance.update!(name: 'test2')
+ end
+ model.find_by(name: 'test1')
+ },
+ true, [:primary, :primary, :primary, :primary, :primary, :primary, :primary]
+ ],
+
+ # Nested transaction
+ [
+ -> {
+ model.transaction do
+ model.transaction do
+ model.create!(name: 'test1')
+ end
+ model.update_all(name: 'test2')
+ end
+ model.find_by(name: 'test1')
+ },
+ true, [:primary, :primary, :primary, :primary, :primary]
+ ],
+
+ # Read-only transaction
+ [
+ -> {
+ model.transaction do
+ model.first
+ model.where(name: 'test1').to_a
+ end
+ },
+ true, [:primary, :primary, :primary, :primary]
+ ],
+
+ # use_primary
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary do
+ model.first
+ model.where(name: 'test1').to_a
+ end
+ model.first
+ },
+ false, [:primary, :primary, :replica]
+ ],
+
+ # use_primary!
+ [
+ -> {
+ model.first
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary!
+ model.where(name: 'test1').to_a
+ },
+ false, [:replica, :primary]
+ ],
+
+ # use_replicas_for_read_queries does not affect read queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.where(name: 'test1').to_a
+ end
+ },
+ false, [:replica]
+ ],
+
+ # use_replicas_for_read_queries does not affect write queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.create!(name: 'test1')
+ end
+ },
+ false, [:primary]
+ ],
+
+ # use_replicas_for_read_queries does not affect ambiguous queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.connection.exec_query("SELECT 1")
+ end
+ },
+ false, [:primary]
+ ],
+
+ # use_replicas_for_read_queries ignores use_primary! for read queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary!
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.where(name: 'test1').to_a
+ end
+ },
+ false, [:replica]
+ ],
+
+ # use_replicas_for_read_queries adheres use_primary! for write queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary!
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.create!(name: 'test1')
+ end
+ },
+ false, [:primary]
+ ],
+
+ # use_replicas_for_read_queries adheres use_primary! for ambiguous queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary!
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.connection.exec_query('SELECT 1')
+ end
+ },
+ false, [:primary]
+ ],
+
+ # use_replicas_for_read_queries ignores use_primary blocks
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary do
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.where(name: 'test1').to_a
+ end
+ end
+ },
+ false, [:replica]
+ ],
+
+ # use_replicas_for_read_queries ignores a session already performed write
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.write!
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ model.where(name: 'test1').to_a
+ end
+ },
+ false, [:replica]
+ ],
+
+ # fallback_to_replicas_for_ambiguous_queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.first
+ model.where(name: 'test1').to_a
+ end
+ },
+ false, [:replica, :replica]
+ ],
+
+ # fallback_to_replicas_for_ambiguous_queries for read-only transaction
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.transaction do
+ model.first
+ model.where(name: 'test1').to_a
+ end
+ end
+ },
+ false, [:replica, :replica]
+ ],
+
+ # A custom read query inside fallback_to_replicas_for_ambiguous_queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.connection.exec_query("SELECT 1")
+ end
+ },
+ false, [:replica]
+ ],
+
+ # A custom read query inside a transaction fallback_to_replicas_for_ambiguous_queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.transaction do
+ model.connection.exec_query("SET LOCAL statement_timeout = 5000")
+ model.count
+ end
+ end
+ },
+ true, [:replica, :replica, :replica, :replica]
+ ],
+
+ # fallback_to_replicas_for_ambiguous_queries after a write
+ [
+ -> {
+ model.create!(name: 'Test1')
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.connection.exec_query("SELECT 1")
+ end
+ },
+ false, [:primary, :primary]
+ ],
+
+ # fallback_to_replicas_for_ambiguous_queries after use_primary!
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary!
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.connection.exec_query("SELECT 1")
+ end
+ },
+ false, [:primary]
+ ],
+
+ # fallback_to_replicas_for_ambiguous_queries inside use_primary
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary do
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.connection.exec_query("SELECT 1")
+ end
+ end
+ },
+ false, [:primary]
+ ],
+
+ # use_primary inside fallback_to_replicas_for_ambiguous_queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary do
+ model.connection.exec_query("SELECT 1")
+ end
+ end
+ },
+ false, [:primary]
+ ],
+
+ # A write query inside fallback_to_replicas_for_ambiguous_queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.connection.exec_query("SELECT 1")
+ model.delete_all
+ model.connection.exec_query("SELECT 1")
+ end
+ },
+ false, [:replica, :primary, :primary]
+ ],
+
+ # use_replicas_for_read_queries incorporates with fallback_to_replicas_for_ambiguous_queries
+ [
+ -> {
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries do
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.connection.exec_query('SELECT 1')
+ model.where(name: 'test1').to_a
+ end
+ end
+ },
+ false, [:replica, :replica]
+ ]
+ ]
+ end
+
+ with_them do
+ include_context 'LoadBalancing setup'
+
+ it 'redirects queries to the right roles' do
+ roles = []
+
+ subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
+ payload = event.payload
+
+ assert =
+ if payload[:name] == 'SCHEMA'
+ false
+ elsif payload[:name] == 'SQL' # Custom query
+ true
+ else
+ keywords = %w[load_balancing_test]
+ keywords += %w[begin commit] if include_transaction
+ keywords.any? { |keyword| payload[:sql].downcase.include?(keyword) }
+ end
+
+ if assert
+ db_role = ::Gitlab::Database::LoadBalancing.db_role_for_connection(payload[:connection])
+ roles << db_role
+ end
+ end
+
+ self.instance_exec(&queries)
+
+ expect(roles).to eql(expected_results)
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+ end
+
+ context 'custom connection handling' do
+ where(:queries, :expected_role) do
+ [
+ # Reload cache. The schema loading queries should be handled by
+ # primary.
+ [
+ -> {
+ model.connection.clear_cache!
+ model.connection.schema_cache.add('users')
+ model.connection.pool.release_connection
+ },
+ :primary
+ ],
+
+ # Call model's connection method
+ [
+ -> {
+ connection = model.connection
+ connection.select_one('SELECT 1')
+ connection.pool.release_connection
+ },
+ :replica
+ ],
+
+ # Retrieve connection via #retrieve_connection
+ [
+ -> {
+ connection = model.retrieve_connection
+ connection.select_one('SELECT 1')
+ connection.pool.release_connection
+ },
+ :primary
+ ]
+ ]
+ end
+
+ with_them do
+ include_context 'LoadBalancing setup'
+
+ it 'redirects queries to the right roles' do
+ roles = []
+
+ subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
+ role = ::Gitlab::Database::LoadBalancing.db_role_for_connection(event.payload[:connection])
+ roles << role if role.present?
+ end
+
+ self.instance_exec(&queries)
+
+ expect(roles).to all(eql(expected_role))
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+ end
+ end
+
+ context 'a write inside a transaction inside fallback_to_replicas_for_ambiguous_queries block' do
+ include_context 'LoadBalancing setup'
+
+ it 'raises an exception' do
+ expect do
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ model.transaction do
+ model.first
+ model.create!(name: 'hello')
+ end
+ end
+ end.to raise_error(Gitlab::Database::LoadBalancing::ConnectionProxy::WriteInsideReadOnlyTransactionError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 40720628a89..f0ea07646fb 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -2001,6 +2001,41 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#ensure_batched_background_migration_is_finished' do
+ let(:configuration) do
+ {
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: :events,
+ column_name: :id,
+ job_arguments: [[:id], [:id_convert_to_bigint]]
+ }
+ end
+
+ subject(:ensure_batched_background_migration_is_finished) { model.ensure_batched_background_migration_is_finished(**configuration) }
+
+ it 'raises an error when migration exists and is not marked as finished' do
+ create(:batched_background_migration, configuration.merge(status: :active))
+
+ expect { ensure_batched_background_migration_is_finished }
+ .to raise_error "Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active': #{configuration}"
+ end
+
+ it 'does not raise error when migration exists and is marked as finished' do
+ create(:batched_background_migration, configuration.merge(status: :finished))
+
+ expect { ensure_batched_background_migration_is_finished }
+ .not_to raise_error
+ end
+
+ it 'logs a warning when migration does not exist' do
+ expect(Gitlab::AppLogger).to receive(:warn)
+ .with("Could not find batched background migration for the given configuration: #{configuration}")
+
+ expect { ensure_batched_background_migration_is_finished }
+ .not_to raise_error
+ end
+ end
+
describe '#index_exists_by_name?' do
it 'returns true if an index exists' do
ActiveRecord::Base.connection.execute(
diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
index c6d456964cf..e096e7f6e91 100644
--- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
@@ -242,6 +242,98 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
end
end
+ describe '#requeue_background_migration_jobs_by_range_at_intervals' do
+ let!(:job_class_name) { 'TestJob' }
+ let!(:pending_job_1) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [1, 2]) }
+ let!(:pending_job_2) { create(:background_migration_job, class_name: job_class_name, status: :pending, arguments: [3, 4]) }
+ let!(:successful_job_1) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [5, 6]) }
+ let!(:successful_job_2) { create(:background_migration_job, class_name: job_class_name, status: :succeeded, arguments: [7, 8]) }
+
+ around do |example|
+ freeze_time do
+ Sidekiq::Testing.fake! do
+ example.run
+ end
+ end
+ end
+
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes) }
+
+ it 'returns the expected duration' do
+ expect(subject).to eq(20.minutes)
+ end
+
+ context 'when nothing is queued' do
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals('FakeJob', 10.minutes) }
+
+ it 'returns expected duration of zero when nothing gets queued' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ it 'queues pending jobs' do
+ subject
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([job_class_name, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([job_class_name, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(10.minutes.from_now.to_f)
+ end
+
+ context 'with batch_size option' do
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes, batch_size: 1) }
+
+ it 'returns the expected duration' do
+ expect(subject).to eq(20.minutes)
+ end
+
+ it 'queues pending jobs' do
+ subject
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([job_class_name, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to be_nil
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([job_class_name, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(10.minutes.from_now.to_f)
+ end
+
+ it 'retrieve jobs in batches' do
+ jobs = double('jobs')
+ expect(Gitlab::Database::BackgroundMigrationJob).to receive(:pending) { jobs }
+ allow(jobs).to receive(:where).with(class_name: job_class_name) { jobs }
+ expect(jobs).to receive(:each_batch).with(of: 1)
+
+ subject
+ end
+ end
+
+ context 'with initial_delay option' do
+ let_it_be(:initial_delay) { 3.minutes }
+
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals(job_class_name, 10.minutes, initial_delay: initial_delay) }
+
+ it 'returns the expected duration' do
+ expect(subject).to eq(23.minutes)
+ end
+
+ it 'queues pending jobs' do
+ subject
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([job_class_name, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(3.minutes.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([job_class_name, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(13.minutes.from_now.to_f)
+ end
+
+ context 'when nothing is queued' do
+ subject { model.requeue_background_migration_jobs_by_range_at_intervals('FakeJob', 10.minutes) }
+
+ it 'returns expected duration of zero when nothing gets queued' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+ end
+
describe '#perform_background_migration_inline?' do
it 'returns true in a test environment' do
stub_rails_env('test')
@@ -269,6 +361,38 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
allow(Gitlab::Database::PgClass).to receive(:for_table).and_call_original
end
+ context 'when such migration already exists' do
+ it 'does not create duplicate migration' do
+ create(
+ :batched_background_migration,
+ job_class_name: 'MyJobClass',
+ table_name: :projects,
+ column_name: :id,
+ interval: 10.minutes,
+ min_value: 5,
+ max_value: 1005,
+ batch_class_name: 'MyBatchClass',
+ batch_size: 200,
+ sub_batch_size: 20,
+ job_arguments: [[:id], [:id_convert_to_bigint]]
+ )
+
+ expect do
+ model.queue_batched_background_migration(
+ 'MyJobClass',
+ :projects,
+ :id,
+ [:id], [:id_convert_to_bigint],
+ job_interval: 5.minutes,
+ batch_min_value: 5,
+ batch_max_value: 1000,
+ batch_class_name: 'MyBatchClass',
+ batch_size: 100,
+ sub_batch_size: 10)
+ end.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }
+ end
+ end
+
it 'creates the database record for the migration' do
expect(Gitlab::Database::PgClass).to receive(:for_table).with(:projects).and_return(pgclass_info)
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 79ddb450d7a..4f1d6302331 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
@@ -580,7 +580,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
it 'idempotently cleans up after failed background migrations' do
expect(partitioned_model.count).to eq(0)
- partitioned_model.insert!(record2.attributes)
+ partitioned_model.insert(record2.attributes, unique_by: [:id, :created_at])
expect_next_instance_of(Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable) do |backfill|
allow(backfill).to receive(:transaction_open?).and_return(false)
diff --git a/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
new file mode 100644
index 00000000000..e9c512f94bb
--- /dev/null
+++ b/spec/lib/gitlab/database/postgresql_adapter/type_map_cache_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::PostgresqlAdapter::TypeMapCache do
+ let(:db_config) { ActiveRecord::Base.configurations.configs_for(env_name: 'test', name: 'primary').configuration_hash }
+ let(:adapter_class) { ActiveRecord::ConnectionAdapters::PostgreSQLAdapter }
+
+ before do
+ adapter_class.type_map_cache.clear
+ end
+
+ describe '#initialize_type_map' do
+ it 'caches loading of types in memory' do
+ recorder_without_cache = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection.disconnect! }
+ expect(recorder_without_cache.log).to include(a_string_matching(/FROM pg_type/)).twice
+
+ recorder_with_cache = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection.disconnect! }
+
+ expect(recorder_with_cache.count).to be < recorder_without_cache.count
+
+ # There's still one pg_type query left here because `#add_pg_decoders` executes another pg_type query
+ # in https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L912.
+ # This query is much cheaper because it only returns very few records.
+ expect(recorder_with_cache.log).to include(a_string_matching(/FROM pg_type/)).once
+ end
+
+ it 'only reuses the cache if the connection parameters are exactly the same' do
+ initialize_connection.disconnect!
+
+ other_config = db_config.dup
+ other_config[:connect_timeout] = db_config[:connect_timeout].to_i + 10
+
+ recorder = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection(other_config).disconnect! }
+
+ expect(recorder.log).to include(a_string_matching(/FROM pg_type/)).twice
+ end
+ end
+
+ describe '#reload_type_map' do
+ it 'clears the cache and executes the type map query again' do
+ initialize_connection.disconnect!
+
+ connection = initialize_connection
+ recorder = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { connection.reload_type_map }
+
+ expect(recorder.log).to include(a_string_matching(/FROM pg_type/)).once
+ end
+ end
+
+ # Based on https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L36-L41
+ def initialize_connection(config = db_config)
+ conn_params = config.symbolize_keys.compact
+
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
+
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
+ conn_params.slice!(*valid_conn_param_keys)
+
+ adapter_class.new(
+ adapter_class.new_client(conn_params),
+ ActiveRecord::Base.logger,
+ conn_params,
+ config
+ )
+ end
+end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index b08f39fc92a..df2c506e163 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -242,10 +242,10 @@ RSpec.describe Gitlab::Database::WithLockRetries do
let(:timing_configuration) { [[0.015.seconds, 0.025.seconds], [0.015.seconds, 0.025.seconds]] } # 15ms, 25ms
it 'executes `SET LOCAL lock_timeout` using the configured timeout value in milliseconds' do
- expect(ActiveRecord::Base.connection).to receive(:execute).with("SAVEPOINT active_record_1").and_call_original
- expect(ActiveRecord::Base.connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:execute).with("RESET idle_in_transaction_session_timeout; RESET lock_timeout").and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:execute).with("SAVEPOINT active_record_1", "TRANSACTION").and_call_original
expect(ActiveRecord::Base.connection).to receive(:execute).with("SET LOCAL lock_timeout TO '15ms'").and_call_original
- expect(ActiveRecord::Base.connection).to receive(:execute).with("RELEASE SAVEPOINT active_record_1").and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:execute).with("RELEASE SAVEPOINT active_record_1", "TRANSACTION").and_call_original
subject.run { }
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 663c8d69328..847f7ec2d74 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -65,6 +65,28 @@ RSpec.describe Gitlab::Database do
end
end
+ describe '.disable_prepared_statements' do
+ around do |example|
+ original_config = ::Gitlab::Database.config
+
+ example.run
+
+ ActiveRecord::Base.establish_connection(original_config)
+ end
+
+ it 'disables prepared statements' do
+ ActiveRecord::Base.establish_connection(::Gitlab::Database.config.merge(prepared_statements: true))
+ expect(ActiveRecord::Base.connection.prepared_statements).to eq(true)
+
+ expect(ActiveRecord::Base).to receive(:establish_connection)
+ .with(a_hash_including({ 'prepared_statements' => false })).and_call_original
+
+ described_class.disable_prepared_statements
+
+ expect(ActiveRecord::Base.connection.prepared_statements).to eq(false)
+ end
+ end
+
describe '.postgresql?' do
subject { described_class.postgresql? }
@@ -103,10 +125,10 @@ RSpec.describe Gitlab::Database do
expect(described_class.postgresql_minimum_supported_version?).to eq(false)
end
- it 'returns true when using PostgreSQL 11' do
+ it 'returns false when using PostgreSQL 11' do
allow(described_class).to receive(:version).and_return('11')
- expect(described_class.postgresql_minimum_supported_version?).to eq(true)
+ expect(described_class.postgresql_minimum_supported_version?).to eq(false)
end
it 'returns true when using PostgreSQL 12' do
@@ -307,7 +329,7 @@ RSpec.describe Gitlab::Database do
expect(pool)
.to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool)
- expect(pool.spec.config[:pool]).to eq(5)
+ expect(pool.db_config.pool).to eq(5)
ensure
pool.disconnect!
end
@@ -317,7 +339,7 @@ RSpec.describe Gitlab::Database do
pool = described_class.create_connection_pool(5, '127.0.0.1')
begin
- expect(pool.spec.config[:host]).to eq('127.0.0.1')
+ expect(pool.db_config.host).to eq('127.0.0.1')
ensure
pool.disconnect!
end
@@ -327,8 +349,8 @@ RSpec.describe Gitlab::Database do
pool = described_class.create_connection_pool(5, '127.0.0.1', 5432)
begin
- expect(pool.spec.config[:host]).to eq('127.0.0.1')
- expect(pool.spec.config[:port]).to eq(5432)
+ expect(pool.db_config.host).to eq('127.0.0.1')
+ expect(pool.db_config.configuration_hash[:port]).to eq(5432)
ensure
pool.disconnect!
end
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
index 4c56911e665..9e94a63ea4b 100644
--- a/spec/lib/gitlab/diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -238,17 +238,7 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
subject { cache.key }
it 'returns cache key' do
- is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true:true")
- end
-
- context 'when the `introduce_marker_ranges` feature flag is disabled' do
- before do
- stub_feature_flags(introduce_marker_ranges: false)
- end
-
- it 'returns the original version of the cache' do
- is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:false:true:true")
- end
+ is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true")
end
context 'when the `use_marker_ranges` feature flag is disabled' do
@@ -257,7 +247,7 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
end
it 'returns the original version of the cache' do
- is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:false:true")
+ is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:false:true")
end
end
@@ -267,7 +257,7 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
end
it 'returns the original version of the cache' do
- is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:true:false")
+ is_expected.to eq("highlighted-diff-files:#{cache.diffable.cache_key}:2:#{cache.diff_options}:true:false")
end
end
end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 94d3b2ad0b3..94b28c38fa2 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -56,26 +56,6 @@ RSpec.describe Gitlab::Diff::Highlight do
expect(subject[5].rich_text).to eq(code)
end
- context 'when introduce_marker_ranges is false' do
- before do
- stub_feature_flags(introduce_marker_ranges: false)
- end
-
- it 'keeps the old bevavior (without mode classes)' do
- code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left">RuntimeError</span></span><span class="p"><span class="idiff">,</span></span><span class="idiff right"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
-
- expect(subject[5].rich_text).to eq(code)
- end
-
- context 'when use_marker_ranges feature flag is false too' do
- it 'does not affect the result' do
- code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class="idiff left">RuntimeError</span></span><span class="p"><span class="idiff">,</span></span><span class="idiff right"> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
-
- expect(subject[5].rich_text).to eq(code)
- end
- end
- end
-
context 'when no diff_refs' do
before do
allow(diff_file).to receive(:diff_refs).and_return(nil)
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index 1a7d837af73..dd230140b30 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -128,6 +128,14 @@ RSpec.describe Gitlab::Email::Handler::CreateIssueHandler do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
end
end
+
+ context 'when project ID is invalid' do
+ it 'raises a ProjectNotFound' do
+ handler = described_class.new(email_raw, "gitlabhq-gitlabhq-#{Gitlab::Database::MAX_INT_VALUE}-#{user.incoming_email_token}-issue")
+
+ expect { handler.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
end
def email_fixture(path)
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
index 6d26b3e1064..3a60564d8d2 100644
--- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
end
context 'when using service desk key' do
- let_it_be(:service_desk_settings) { create(:service_desk_setting, project: project, project_key: 'mykey') }
+ let_it_be(:service_desk_key) { 'mykey' }
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
@@ -176,6 +176,10 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
end
+ before_all do
+ create(:service_desk_setting, project: project, project_key: service_desk_key)
+ end
+
it_behaves_like 'a new issue request'
context 'when there is no project with the key' do
@@ -193,6 +197,20 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
end
end
+
+ context 'when there are multiple projects with same key' do
+ let_it_be(:project_with_same_key) { create(:project, group: group, service_desk_enabled: true) }
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: project_with_same_key.full_path_slug.to_s) }
+
+ before do
+ create(:service_desk_setting, project: project_with_same_key, project_key: service_desk_key)
+ end
+
+ it 'process email for project with matching slug' do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+ expect(Issue.last.project).to eq(project_with_same_key)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb
index 42d84b3e4de..277f1158f8b 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/base_spec.rb
@@ -4,12 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
let(:series) { 0 }
let(:test_class) { Gitlab::Email::Message::InProductMarketing::Create }
describe 'initialize' do
- subject { test_class.new(group: group, series: series) }
+ subject { test_class.new(group: group, user: user, series: series) }
context 'when series does not exist' do
let(:series) { 3 }
@@ -29,13 +30,13 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
end
describe '#logo_path' do
- subject { test_class.new(group: group, series: series).logo_path }
+ subject { test_class.new(group: group, user: user, series: series).logo_path }
it { is_expected.to eq('mailers/in_product_marketing/create-0.png') }
end
describe '#unsubscribe' do
- subject { test_class.new(group: group, series: series).unsubscribe }
+ subject { test_class.new(group: group, user: user, series: series).unsubscribe }
before do
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
@@ -55,7 +56,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
end
describe '#cta_link' do
- subject(:cta_link) { test_class.new(group: group, series: series).cta_link }
+ subject(:cta_link) { test_class.new(group: group, user: user, series: series).cta_link }
it 'renders link' do
expect(CGI.unescapeHTML(cta_link)).to include(Gitlab::Routing.url_helpers.group_email_campaigns_url(group, track: :create, series: series))
@@ -63,7 +64,7 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Base do
end
describe '#progress' do
- subject { test_class.new(group: group, series: series).progress }
+ subject { test_class.new(group: group, user: user, series: series).progress }
before do
allow(Gitlab).to receive(:com?).and_return(is_gitlab_com)
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb
index be8a33b18bd..35470ef3555 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/create_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Create do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
- subject(:message) { described_class.new(group: group, series: series)}
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do
where(series: [0, 1, 2])
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb
new file mode 100644
index 00000000000..b742eff3f56
--- /dev/null
+++ b/spec/lib/gitlab/email/message/in_product_marketing/experience_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::Message::InProductMarketing::Experience do
+ let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
+
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
+
+ describe 'public methods' do
+ context 'with series 0' do
+ let(:series) { 0 }
+
+ it 'returns value for series', :aggregate_failures do
+ expect(message.subject_line).to be_present
+ expect(message.tagline).to be_nil
+ expect(message.title).to be_present
+ expect(message.subtitle).to be_present
+ expect(message.body_line1).to be_present
+ expect(message.body_line2).to be_present
+ expect(message.cta_text).to be_nil
+ end
+
+ describe '#feedback_link' do
+ let(:member_count) { 2 }
+ let(:user_access) { GroupMember::DEVELOPER }
+ let(:preferred_language) { 'en' }
+
+ before do
+ allow(message).to receive(:onboarding_progress).and_return(1)
+ allow(group).to receive(:member_count).and_return(member_count)
+ allow(group).to receive(:max_member_access_for_user).and_return(user_access)
+ allow(user).to receive(:preferred_language).and_return(preferred_language)
+ end
+
+ subject do
+ uri = URI.parse(message.feedback_link(1))
+ Rack::Utils.parse_query(uri.query).with_indifferent_access[:show_invite_link]
+ end
+
+ it { is_expected.to eq('true') }
+
+ context 'with only one member' do
+ let(:member_count) { 1 }
+
+ it { is_expected.to eq('false') }
+ end
+
+ context 'with less than developer access' do
+ let(:user_access) { GroupMember::GUEST }
+
+ it { is_expected.to eq('false') }
+ end
+
+ context 'with preferred language other than English' do
+ let(:preferred_language) { 'nl' }
+
+ it { is_expected.to eq('false') }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb
index 6251128f560..f72994fcce1 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/team_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Team do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
- subject(:message) { described_class.new(group: group, series: series)}
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do
where(series: [0, 1])
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb
index 2c435490765..5f7639a9ed6 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/trial_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe Gitlab::Email::Message::InProductMarketing::Trial do
using RSpec::Parameterized::TableSyntax
let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
- subject(:message) { described_class.new(group: group, series: series)}
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do
where(series: [0, 1, 2])
diff --git a/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb b/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb
index 73252c0dbdf..a7da2e9553d 100644
--- a/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb
+++ b/spec/lib/gitlab/email/message/in_product_marketing/verify_spec.rb
@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Email::Message::InProductMarketing::Verify do
let_it_be(:group) { build(:group) }
+ let_it_be(:user) { build(:user) }
- subject(:message) { described_class.new(group: group, series: series)}
+ subject(:message) { described_class.new(group: group, user: user, series: series)}
describe "public methods" do
context 'with series 0' do
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 9b05c12ef57..2c1fe529a5d 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -5,9 +5,13 @@ require 'spec_helper'
RSpec.describe Gitlab::Email::Receiver do
include_context :email_shared_context
- shared_examples 'correctly finds the mail key' do
- specify do
+ shared_examples 'correctly finds the mail key and adds metric event' do
+ let(:metric_transaction) { double('Gitlab::Metrics::WebTransaction') }
+
+ specify :aggregate_failures do
expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler)
+ expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
+ expect(metric_transaction).to receive(:add_event).with(handler.metrics_event, handler.metrics_params)
receiver.execute
end
@@ -30,7 +34,7 @@ RSpec.describe Gitlab::Email::Receiver do
context 'when in a Delivered-To header' do
let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
- it_behaves_like 'correctly finds the mail key'
+ it_behaves_like 'correctly finds the mail key and adds metric event'
it 'parses the metadata' do
expect(metadata[:delivered_to]). to eq(["incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com", "support@example.com"])
@@ -40,7 +44,7 @@ RSpec.describe Gitlab::Email::Receiver do
context 'when in an Envelope-To header' do
let(:email_raw) { fixture_file('emails/envelope_to_header.eml') }
- it_behaves_like 'correctly finds the mail key'
+ it_behaves_like 'correctly finds the mail key and adds metric event'
it 'parses the metadata' do
expect(metadata[:envelope_to]). to eq(["incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com"])
@@ -50,7 +54,7 @@ RSpec.describe Gitlab::Email::Receiver do
context 'when in an X-Envelope-To header' do
let(:email_raw) { fixture_file('emails/x_envelope_to_header.eml') }
- it_behaves_like 'correctly finds the mail key'
+ it_behaves_like 'correctly finds the mail key and adds metric event'
it 'parses the metadata' do
expect(metadata[:x_envelope_to]). to eq(["incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com"])
@@ -60,7 +64,7 @@ RSpec.describe Gitlab::Email::Receiver do
context 'when enclosed with angle brackets in an Envelope-To header' do
let(:email_raw) { fixture_file('emails/envelope_to_header_with_angle_brackets.eml') }
- it_behaves_like 'correctly finds the mail key'
+ it_behaves_like 'correctly finds the mail key and adds metric event'
end
end
diff --git a/spec/lib/gitlab/emoji_spec.rb b/spec/lib/gitlab/emoji_spec.rb
index ada37f25d1e..8f855489c12 100644
--- a/spec/lib/gitlab/emoji_spec.rb
+++ b/spec/lib/gitlab/emoji_spec.rb
@@ -91,7 +91,16 @@ RSpec.describe Gitlab::Emoji do
it 'returns emoji image tag' do
emoji_image = described_class.emoji_image_tag('emoji_one', 'src_url')
- expect(emoji_image).to eq( "<img class='emoji' title=':emoji_one:' alt=':emoji_one:' src='src_url' height='20' width='20' align='absmiddle' />")
+ expect(emoji_image).to eq("<img class=\"emoji\" src=\"src_url\" title=\":emoji_one:\" alt=\":emoji_one:\" height=\"20\" width=\"20\" align=\"absmiddle\" />")
+ end
+
+ it 'escapes emoji image attrs to prevent XSS' do
+ xss_payload = "<script>alert(1)</script>"
+ escaped_xss_payload = html_escape(xss_payload)
+
+ emoji_image = described_class.emoji_image_tag(xss_payload, 'http://aaa#' + xss_payload)
+
+ expect(emoji_image).to eq("<img class=\"emoji\" src=\"http://aaa##{escaped_xss_payload}\" title=\":#{escaped_xss_payload}:\" alt=\":#{escaped_xss_payload}:\" height=\"20\" width=\"20\" align=\"absmiddle\" />")
end
end
diff --git a/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb
index 584eadb24a7..210829056c8 100644
--- a/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb
+++ b/spec/lib/gitlab/error_tracking/processor/context_payload_processor_spec.rb
@@ -4,7 +4,15 @@ require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::Processor::ContextPayloadProcessor do
describe '.call' do
- let(:event) { Raven::Event.new(payload) }
+ let(:required_options) do
+ {
+ configuration: Raven.configuration,
+ context: Raven.context,
+ breadcrumbs: Raven.breadcrumbs
+ }
+ end
+
+ let(:event) { Raven::Event.new(required_options.merge(payload)) }
let(:result_hash) { described_class.call(event).to_hash }
before do
diff --git a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb
index 727b603feda..6076e525f06 100644
--- a/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb
+++ b/spec/lib/gitlab/error_tracking/processor/grpc_error_processor_spec.rb
@@ -4,7 +4,15 @@ require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor do
describe '.call' do
- let(:event) { Raven::Event.from_exception(exception, data) }
+ let(:required_options) do
+ {
+ configuration: Raven.configuration,
+ context: Raven.context,
+ breadcrumbs: Raven.breadcrumbs
+ }
+ end
+
+ let(:event) { Raven::Event.from_exception(exception, required_options.merge(data)) }
let(:result_hash) { described_class.call(event).to_hash }
context 'when there is no GRPC exception' do
diff --git a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb
index c8a362fcf05..af5f11c9362 100644
--- a/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb
+++ b/spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb
@@ -95,7 +95,15 @@ RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do
end
describe '.call' do
- let(:event) { Raven::Event.new(wrapped_value) }
+ let(:required_options) do
+ {
+ configuration: Raven.configuration,
+ context: Raven.context,
+ breadcrumbs: Raven.breadcrumbs
+ }
+ end
+
+ let(:event) { Raven::Event.new(required_options.merge(wrapped_value)) }
let(:result_hash) { described_class.call(event).to_hash }
context 'when there is Sidekiq data' do
diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb
index b3293e6473c..7ad1f52780a 100644
--- a/spec/lib/gitlab/error_tracking_spec.rb
+++ b/spec/lib/gitlab/error_tracking_spec.rb
@@ -204,23 +204,6 @@ RSpec.describe Gitlab::ErrorTracking do
expect(sentry_event.dig('extra', 'sql')).to eq('SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')
end
-
- context 'when SQL cannot be parsed' do
- let(:pg12_query) do
- <<-SQL
- CREATE INDEX CONCURRENTLY my_index ON merge_requests
- USING btree (target_project_id) INCLUDE (id, latest_merge_request_diff_id)
- SQL
- end
-
- let(:exception) { ActiveRecord::StatementInvalid.new(sql: pg12_query) }
-
- it 'injects the raw sql query into extra' do
- track_exception
-
- expect(sentry_event.dig('extra', 'sql')).to eq(pg12_query)
- end
- end
end
context 'when the `ActiveRecord::StatementInvalid` is wrapped in another exception' do
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
index 3122a3b1c07..c4da89e5f5c 100644
--- a/spec/lib/gitlab/etag_caching/middleware_spec.rb
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -33,7 +33,6 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
expect(headers['ETag']).to be_nil
expect(headers['X-Gitlab-From-Cache']).to be_nil
- expect(headers[::Gitlab::Metrics::RequestsRackMiddleware::FEATURE_CATEGORY_HEADER]).to be_nil
end
it 'passes status code from app' do
@@ -41,6 +40,12 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
expect(status).to eq app_status_code
end
+
+ it 'does not set feature category attribute' do
+ expect(Gitlab::ApplicationContext).not_to receive(:push)
+
+ _, _, _ = middleware.call(build_request(path, if_none_match))
+ end
end
context 'when there is no ETag in store for given resource' do
@@ -164,8 +169,15 @@ RSpec.describe Gitlab::EtagCaching::Middleware, :clean_gitlab_redis_shared_state
it 'sets correct headers' do
_, headers, _ = middleware.call(build_request(path, if_none_match))
- expect(headers).to include('X-Gitlab-From-Cache' => 'true',
- ::Gitlab::Metrics::RequestsRackMiddleware::FEATURE_CATEGORY_HEADER => 'issue_tracking')
+ expect(headers).to include('X-Gitlab-From-Cache' => 'true')
+ end
+
+ it "pushes route's feature category to the context" do
+ expect(Gitlab::ApplicationContext).to receive(:push).with(
+ feature_category: 'issue_tracking'
+ )
+
+ _, _, _ = middleware.call(build_request(path, if_none_match))
end
it_behaves_like 'sends a process_action.action_controller notification', 304
diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
index 5419a01ea3e..7a619c9f155 100644
--- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb
+++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb
@@ -196,9 +196,12 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
end
describe '#track_experiment_event', :snowplow do
+ let(:user) { build(:user) }
+
context 'when the experiment is enabled' do
before do
stub_experiment(test_experiment: true)
+ allow(controller).to receive(:current_user).and_return(user)
end
context 'the user is part of the experimental group' do
@@ -213,7 +216,8 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
category: 'Team',
action: 'start',
property: 'experimental_group',
- value: 1
+ value: 1,
+ user: user
)
end
end
@@ -230,7 +234,8 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
category: 'Team',
action: 'start',
property: 'control_group',
- value: 1
+ value: 1,
+ user: user
)
end
end
@@ -247,7 +252,8 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
category: 'Team',
action: 'start',
property: 'control_group',
- value: 1
+ value: 1,
+ user: user
)
end
end
@@ -280,7 +286,8 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
action: 'start',
property: 'control_group',
value: 1,
- label: Digest::MD5.hexdigest('abc')
+ label: Digest::MD5.hexdigest('abc'),
+ user: user
)
end
@@ -294,7 +301,8 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
action: 'start',
property: 'control_group',
value: 1,
- label: Digest::MD5.hexdigest('somestring')
+ label: Digest::MD5.hexdigest('somestring'),
+ user: user
)
end
end
@@ -313,7 +321,8 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do
action: 'start',
property: 'control_group',
value: 1,
- label: cookies.permanent.signed[:experimentation_subject_id]
+ label: cookies.permanent.signed[:experimentation_subject_id],
+ user: user
)
end
end
diff --git a/spec/lib/gitlab/file_hook_spec.rb b/spec/lib/gitlab/file_hook_spec.rb
index 7f40d9ae772..4fc55f7ad7e 100644
--- a/spec/lib/gitlab/file_hook_spec.rb
+++ b/spec/lib/gitlab/file_hook_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::FileHook do
let(:file_hook_source) do
<<~EOS
#!/usr/bin/env ruby
- x = STDIN.read
+ x = $stdin.read
File.write('#{tmp_file.path}', x)
EOS
end
diff --git a/spec/lib/gitlab/git/conflict/resolver_spec.rb b/spec/lib/gitlab/git/conflict/resolver_spec.rb
new file mode 100644
index 00000000000..2783e955c33
--- /dev/null
+++ b/spec/lib/gitlab/git/conflict/resolver_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Git::Conflict::Resolver do
+ let(:repository) { instance_double(Gitlab::Git::Repository) }
+ let(:our_commit_oid) { 'our-commit-oid' }
+ let(:their_commit_oid) { 'their-commit-oid' }
+ let(:gitaly_conflicts_client) { instance_double(Gitlab::GitalyClient::ConflictsService) }
+
+ subject(:resolver) { described_class.new(repository, our_commit_oid, their_commit_oid) }
+
+ describe '#conflicts' do
+ before do
+ allow(repository).to receive(:gitaly_conflicts_client).and_return(gitaly_conflicts_client)
+ end
+
+ it 'returns list of conflicts' do
+ conflicts = [double]
+
+ expect(gitaly_conflicts_client).to receive(:list_conflict_files).and_return(conflicts)
+ expect(resolver.conflicts).to eq(conflicts)
+ end
+
+ context 'when GRPC::FailedPrecondition is raised' do
+ it 'rescues and raises Gitlab::Git::Conflict::Resolver::ConflictSideMissing' do
+ expect(gitaly_conflicts_client).to receive(:list_conflict_files).and_raise(GRPC::FailedPrecondition)
+ expect { resolver.conflicts }.to raise_error(Gitlab::Git::Conflict::Resolver::ConflictSideMissing)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb
index 84c17234ae4..c7bc81573a6 100644
--- a/spec/lib/gitlab/git/remote_repository_spec.rb
+++ b/spec/lib/gitlab/git/remote_repository_spec.rb
@@ -58,45 +58,4 @@ RSpec.describe Gitlab::Git::RemoteRepository, :seed_helper do
it { expect(subject.same_repository?(other_repository)).to eq(result) }
end
end
-
- describe '#fetch_env' do
- let(:remote_repository) { described_class.new(repository) }
-
- let(:gitaly_client) { double(:gitaly_client) }
- let(:address) { 'fake-address' }
- let(:token) { 'fake-token' }
-
- subject { remote_repository.fetch_env }
-
- before do
- allow(remote_repository).to receive(:gitaly_client).and_return(gitaly_client)
-
- expect(gitaly_client).to receive(:address).with(repository.storage).and_return(address)
- expect(gitaly_client).to receive(:token).with(repository.storage).and_return(token)
- end
-
- it { expect(subject).to be_a(Hash) }
- it { expect(subject['GITALY_ADDRESS']).to eq(address) }
- it { expect(subject['GITALY_TOKEN']).to eq(token) }
- it { expect(subject['GITALY_WD']).to eq(Dir.pwd) }
-
- it 'creates a plausible GIT_SSH_COMMAND' do
- git_ssh_command = subject['GIT_SSH_COMMAND']
-
- expect(git_ssh_command).to start_with('/')
- expect(git_ssh_command).to end_with('/gitaly-ssh upload-pack')
- end
-
- it 'creates a plausible GITALY_PAYLOAD' do
- req = Gitaly::SSHUploadPackRequest.decode_json(subject['GITALY_PAYLOAD'])
-
- expect(remote_repository.gitaly_repository).to eq(req.repository)
- end
-
- context 'when the token is blank' do
- let(:token) { '' }
-
- it { expect(subject.keys).not_to include('GITALY_TOKEN') }
- end
- end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 1ddbdda12b5..336bf20d59c 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -133,32 +133,10 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
expect(metadata['ArchivePrefix']).to eq(expected_prefix)
end
- context 'when :include_lfs_blobs_in_archive feature flag is disabled' do
- let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
+ it 'sets ArchivePath to the expected globally-unique path' do
+ expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
- before do
- stub_feature_flags(include_lfs_blobs_in_archive: false)
- end
-
- it 'sets ArchivePath to the expected globally-unique path' do
- # This is really important from a security perspective. Think carefully
- # before changing it: https://gitlab.com/gitlab-org/gitlab-foss/issues/45689
- expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
-
- expect(metadata['ArchivePath']).to eq(expected_path)
- end
- end
-
- context 'when :include_lfs_blobs_in_archive feature flag is enabled' do
- before do
- stub_feature_flags(include_lfs_blobs_in_archive: true)
- end
-
- it 'sets ArchivePath to the expected globally-unique path' do
- expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
-
- expect(metadata['ArchivePath']).to eq(expected_path)
- end
+ expect(metadata['ArchivePath']).to eq(expected_path)
end
context 'path is set' do
@@ -521,7 +499,9 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
no_tags: true,
timeout: described_class::GITLAB_PROJECTS_TIMEOUT,
prune: false,
- check_tags_changed: false
+ check_tags_changed: false,
+ url: nil,
+ refmap: nil
}
expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts)
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 3d6c04fd484..96a44575e24 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -731,6 +731,8 @@ RSpec.describe Gitlab::GitAccess do
context 'when LFS is not enabled' do
it 'does not run LFSIntegrity check' do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+
expect(Gitlab::Checks::LfsIntegrity).not_to receive(:new)
push_access_check
@@ -1004,10 +1006,10 @@ RSpec.describe Gitlab::GitAccess do
expect { access.check('git-receive-pack', changes) }.not_to exceed_query_limit(control_count).with_threshold(2)
end
- it 'raises TimeoutError when #check_single_change_access raises a timeout error' do
+ it 'raises TimeoutError when #check_access! raises a timeout error' do
message = "Push operation timed out\n\nTiming information for debugging purposes:\nRunning checks for ref: wow"
- expect_next_instance_of(Gitlab::Checks::ChangeAccess) do |check|
+ expect_next_instance_of(Gitlab::Checks::SingleChangeAccess) do |check|
expect(check).to receive(:validate!).and_raise(Gitlab::Checks::TimedLogger::TimeoutError)
end
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index 70fc4fe4416..df9dde324a5 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -38,47 +38,26 @@ RSpec.describe Gitlab::GitalyClient::RemoteService do
let(:remote) { 'origin' }
let(:url) { 'http://git.example.com/my-repo.git' }
let(:auth) { 'Basic secret' }
+ let(:expected_params) { { remote_url: url, http_authorization_header: auth } }
- shared_examples 'a find_remote_root_ref call' do
- it 'sends an find_remote_root_ref message and returns the root ref' do
- expect_any_instance_of(Gitaly::RemoteService::Stub)
- .to receive(:find_remote_root_ref)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .with(gitaly_request_with_params(expected_params), kind_of(Hash))
- .and_return(double(ref: 'master'))
-
- expect(client.find_remote_root_ref(remote, url, auth)).to eq 'master'
- end
-
- it 'ensure ref is a valid UTF-8 string' do
- expect_any_instance_of(Gitaly::RemoteService::Stub)
- .to receive(:find_remote_root_ref)
- .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .with(gitaly_request_with_params(expected_params), kind_of(Hash))
- .and_return(double(ref: "an_invalid_ref_\xE5"))
-
- expect(client.find_remote_root_ref(remote, url, auth)).to eq "an_invalid_ref_å"
- end
- end
-
- context 'with inmemory feature enabled' do
- before do
- stub_feature_flags(find_remote_root_refs_inmemory: true)
- end
+ it 'sends an find_remote_root_ref message and returns the root ref' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:find_remote_root_ref)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .with(gitaly_request_with_params(expected_params), kind_of(Hash))
+ .and_return(double(ref: 'master'))
- it_behaves_like 'a find_remote_root_ref call' do
- let(:expected_params) { { remote_url: url, http_authorization_header: auth } }
- end
+ expect(client.find_remote_root_ref(remote, url, auth)).to eq 'master'
end
- context 'with inmemory feature disabled' do
- before do
- stub_feature_flags(find_remote_root_refs_inmemory: false)
- end
+ it 'ensure ref is a valid UTF-8 string' do
+ expect_any_instance_of(Gitaly::RemoteService::Stub)
+ .to receive(:find_remote_root_ref)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .with(gitaly_request_with_params(expected_params), kind_of(Hash))
+ .and_return(double(ref: "an_invalid_ref_\xE5"))
- it_behaves_like 'a find_remote_root_ref call' do
- let(:expected_params) { { remote: remote } }
- end
+ expect(client.find_remote_root_ref(remote, url, auth)).to eq "an_invalid_ref_å"
end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 26ec194a2e7..56c8fe20eca 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -122,67 +122,89 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
describe '#fetch_remote' do
- let(:remote) { 'remote-name' }
-
- it 'sends a fetch_remote_request message' do
- expected_request = gitaly_request_with_params(
- remote: remote,
- ssh_key: '',
- known_hosts: '',
- force: false,
- no_tags: false,
- no_prune: false,
- check_tags_changed: false
- )
+ shared_examples 'a fetch' do
+ it 'sends a fetch_remote_request message' do
+ expected_remote_params = Gitaly::Remote.new(
+ url: url, http_authorization_header: "", mirror_refmaps: [])
+
+ expected_request = gitaly_request_with_params(
+ remote: remote,
+ remote_params: url ? expected_remote_params : nil,
+ ssh_key: '',
+ known_hosts: '',
+ force: false,
+ no_tags: false,
+ no_prune: false,
+ check_tags_changed: false
+ )
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:fetch_remote)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fetch_remote(remote, url: url, refmap: nil, ssh_auth: nil, forced: false, no_tags: false, timeout: 1, check_tags_changed: false)
+ end
- expect_any_instance_of(Gitaly::RepositoryService::Stub)
- .to receive(:fetch_remote)
- .with(expected_request, kind_of(Hash))
- .and_return(double(value: true))
+ context 'SSH auth' do
+ where(:ssh_mirror_url, :ssh_key_auth, :ssh_private_key, :ssh_known_hosts, :expected_params) do
+ false | false | 'key' | 'known_hosts' | {}
+ false | true | 'key' | 'known_hosts' | {}
+ true | false | 'key' | 'known_hosts' | { known_hosts: 'known_hosts' }
+ true | true | 'key' | 'known_hosts' | { ssh_key: 'key', known_hosts: 'known_hosts' }
+ true | true | 'key' | nil | { ssh_key: 'key' }
+ true | true | nil | 'known_hosts' | { known_hosts: 'known_hosts' }
+ true | true | nil | nil | {}
+ true | true | '' | '' | {}
+ end
- client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1, check_tags_changed: false)
+ with_them do
+ let(:ssh_auth) do
+ double(
+ :ssh_auth,
+ ssh_mirror_url?: ssh_mirror_url,
+ ssh_key_auth?: ssh_key_auth,
+ ssh_private_key: ssh_private_key,
+ ssh_known_hosts: ssh_known_hosts
+ )
+ end
+
+ it do
+ expected_remote_params = Gitaly::Remote.new(
+ url: url, http_authorization_header: "", mirror_refmaps: [])
+
+ expected_request = gitaly_request_with_params({
+ remote: remote,
+ remote_params: url ? expected_remote_params : nil,
+ ssh_key: '',
+ known_hosts: '',
+ force: false,
+ no_tags: false,
+ no_prune: false
+ }.update(expected_params))
+
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:fetch_remote)
+ .with(expected_request, kind_of(Hash))
+ .and_return(double(value: true))
+
+ client.fetch_remote(remote, url: url, refmap: nil, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 1)
+ end
+ end
+ end
end
- context 'SSH auth' do
- where(:ssh_mirror_url, :ssh_key_auth, :ssh_private_key, :ssh_known_hosts, :expected_params) do
- false | false | 'key' | 'known_hosts' | {}
- false | true | 'key' | 'known_hosts' | {}
- true | false | 'key' | 'known_hosts' | { known_hosts: 'known_hosts' }
- true | true | 'key' | 'known_hosts' | { ssh_key: 'key', known_hosts: 'known_hosts' }
- true | true | 'key' | nil | { ssh_key: 'key' }
- true | true | nil | 'known_hosts' | { known_hosts: 'known_hosts' }
- true | true | nil | nil | {}
- true | true | '' | '' | {}
+ context 'with remote' do
+ it_behaves_like 'a fetch' do
+ let(:remote) { 'remote-name' }
+ let(:url) { nil }
end
+ end
- with_them do
- let(:ssh_auth) do
- double(
- :ssh_auth,
- ssh_mirror_url?: ssh_mirror_url,
- ssh_key_auth?: ssh_key_auth,
- ssh_private_key: ssh_private_key,
- ssh_known_hosts: ssh_known_hosts
- )
- end
-
- it do
- expected_request = gitaly_request_with_params({
- remote: remote,
- ssh_key: '',
- known_hosts: '',
- force: false,
- no_tags: false,
- no_prune: false
- }.update(expected_params))
-
- expect_any_instance_of(Gitaly::RepositoryService::Stub)
- .to receive(:fetch_remote)
- .with(expected_request, kind_of(Hash))
- .and_return(double(value: true))
-
- client.fetch_remote(remote, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 1)
- end
+ context 'with URL' do
+ it_behaves_like 'a fetch' do
+ let(:remote) { "" }
+ let(:url) { 'https://example.com/git/repo.git' }
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index a8d42f4bccf..16f75691288 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -33,14 +33,6 @@ RSpec.describe Gitlab::GitalyClient do
it { expect(subject.long_timeout).to eq(6.hours) }
end
- context 'running in Unicorn' do
- before do
- allow(Gitlab::Runtime).to receive(:unicorn?).and_return(true)
- end
-
- it { expect(subject.long_timeout).to eq(55) }
- end
-
context 'running in Puma' do
before do
allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
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 8a7867f3841..133d515246a 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
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
- let(:project) { create(:project, import_source: 'foo/bar') }
+ let(:url) { 'https://github.com/foo/bar.git' }
+ let(:project) { create(:project, import_source: 'foo/bar', import_url: url) }
let(:client) { double(:client) }
let(:pull_request) do
@@ -147,14 +148,10 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
end
end
- describe '#update_repository' do
+ shared_examples '#update_repository' do
it 'updates the repository' do
importer = described_class.new(project, client)
- expect(project.repository)
- .to receive(:fetch_remote)
- .with('github', forced: false)
-
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger)
.to receive(:info)
@@ -173,6 +170,28 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do
end
end
+ describe '#update_repository with :fetch_remote_params enabled' do
+ before do
+ stub_feature_flags(fetch_remote_params: true)
+ expect(project.repository)
+ .to receive(:fetch_remote)
+ .with('github', forced: false, url: url, refmap: Gitlab::GithubImport.refmap)
+ end
+
+ it_behaves_like '#update_repository'
+ end
+
+ describe '#update_repository with :fetch_remote_params disabled' do
+ before do
+ stub_feature_flags(fetch_remote_params: false)
+ expect(project.repository)
+ .to receive(:fetch_remote)
+ .with('github', forced: false)
+ end
+
+ it_behaves_like '#update_repository'
+ end
+
describe '#update_repository?' do
let(:importer) { described_class.new(project, client) }
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
index f18064f10aa..08be350f0f9 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb
@@ -27,30 +27,100 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsReviewsImporter do
end
describe '#each_object_to_import', :clean_gitlab_redis_cache do
- it 'fetchs the merged pull requests data' do
- merge_request = create(
- :merged_merge_request,
- iid: 999,
- source_project: project,
- target_project: project
- )
-
- review = double
-
- expect(review)
- .to receive(:merge_request_id=)
- .with(merge_request.id)
-
- allow(client)
- .to receive(:pull_request_reviews)
- .exactly(:once) # ensure to be cached on the second call
- .with('github/repo', merge_request.iid)
- .and_return([review])
-
- expect { |b| subject.each_object_to_import(&b) }
- .to yield_with_args(review)
-
- subject.each_object_to_import {}
+ context 'when github_review_importer_query_only_unimported_merge_requests is enabled' do
+ before do
+ stub_feature_flags(github_review_importer_query_only_unimported_merge_requests: true)
+ end
+
+ let(:merge_request) do
+ create(
+ :merged_merge_request,
+ iid: 999,
+ source_project: project,
+ target_project: project
+ )
+ end
+
+ let(:review) { double(id: 1) }
+
+ it 'fetches the pull requests reviews data' do
+ page = double(objects: [review], number: 1)
+
+ expect(review)
+ .to receive(:merge_request_id=)
+ .with(merge_request.id)
+
+ expect(client)
+ .to receive(:each_page)
+ .exactly(:once) # ensure to be cached on the second call
+ .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 1)
+ .and_yield(page)
+
+ expect { |b| subject.each_object_to_import(&b) }
+ .to yield_with_args(review)
+
+ subject.each_object_to_import {}
+ end
+
+ it 'skips cached pages' do
+ Gitlab::GithubImport::PageCounter
+ .new(project, "merge_request/#{merge_request.id}/pull_request_reviews")
+ .set(2)
+
+ expect(review).not_to receive(:merge_request_id=)
+
+ expect(client)
+ .to receive(:each_page)
+ .exactly(:once) # ensure to be cached on the second call
+ .with(:pull_request_reviews, 'github/repo', merge_request.iid, page: 2)
+
+ subject.each_object_to_import {}
+ end
+
+ it 'skips cached merge requests' do
+ Gitlab::Cache::Import::Caching.set_add(
+ "github-importer/merge_request/already-imported/#{project.id}",
+ merge_request.id
+ )
+
+ expect(review).not_to receive(:merge_request_id=)
+
+ expect(client).not_to receive(:each_page)
+
+ subject.each_object_to_import {}
+ end
+ end
+
+ context 'when github_review_importer_query_only_unimported_merge_requests is disabled' do
+ before do
+ stub_feature_flags(github_review_importer_query_only_unimported_merge_requests: false)
+ end
+
+ it 'fetchs the merged pull requests data' do
+ merge_request = create(
+ :merged_merge_request,
+ iid: 999,
+ source_project: project,
+ target_project: project
+ )
+
+ review = double
+
+ expect(review)
+ .to receive(:merge_request_id=)
+ .with(merge_request.id)
+
+ allow(client)
+ .to receive(:pull_request_reviews)
+ .exactly(:once) # ensure to be cached on the second call
+ .with('github/repo', merge_request.iid)
+ .and_return([review])
+
+ expect { |b| subject.each_object_to_import(&b) }
+ .to yield_with_args(review)
+
+ subject.each_object_to_import {}
+ end
end
end
end
diff --git a/spec/lib/gitlab/github_import/page_counter_spec.rb b/spec/lib/gitlab/github_import/page_counter_spec.rb
index a1305b714b5..568bc8cbbef 100644
--- a/spec/lib/gitlab/github_import/page_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/page_counter_spec.rb
@@ -31,4 +31,15 @@ RSpec.describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
expect(counter.current).to eq(2)
end
end
+
+ describe '#expire!' do
+ it 'expires the current page counter' do
+ counter.set(2)
+
+ counter.expire!
+
+ expect(Gitlab::Cache::Import::Caching.read_integer(counter.cache_key)).to be_nil
+ expect(counter.current).to eq(1)
+ end
+ end
end
diff --git a/spec/lib/gitlab/global_id/deprecations_spec.rb b/spec/lib/gitlab/global_id/deprecations_spec.rb
new file mode 100644
index 00000000000..22a4766c0a0
--- /dev/null
+++ b/spec/lib/gitlab/global_id/deprecations_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GlobalId::Deprecations do
+ include GlobalIDDeprecationHelpers
+
+ let_it_be(:deprecation_1) { described_class::Deprecation.new(old_model_name: 'Foo::Model', new_model_name: 'Bar', milestone: '9.0') }
+ let_it_be(:deprecation_2) { described_class::Deprecation.new(old_model_name: 'Baz', new_model_name: 'Qux::Model', milestone: '10.0') }
+
+ before do
+ stub_global_id_deprecations(deprecation_1, deprecation_2)
+ end
+
+ describe '.deprecated?' do
+ it 'returns a boolean to signal if model name has a deprecation', :aggregate_failures do
+ expect(described_class.deprecated?('Foo::Model')).to eq(true)
+ expect(described_class.deprecated?('Qux::Model')).to eq(false)
+ end
+ end
+
+ describe '.deprecation_for' do
+ it 'returns the deprecation for the model if it exists', :aggregate_failures do
+ expect(described_class.deprecation_for('Foo::Model')).to eq(deprecation_1)
+ expect(described_class.deprecation_for('Qux::Model')).to be_nil
+ end
+ end
+
+ describe '.deprecation_by' do
+ it 'returns the deprecation by the model if it exists', :aggregate_failures do
+ expect(described_class.deprecation_by('Foo::Model')).to be_nil
+ expect(described_class.deprecation_by('Qux::Model')).to eq(deprecation_2)
+ end
+ end
+
+ describe '.apply_to_graphql_name' do
+ it 'returns the corresponding graphql_name of the GID for the new model', :aggregate_failures do
+ expect(described_class.apply_to_graphql_name('FooModelID')).to eq('BarID')
+ expect(described_class.apply_to_graphql_name('BazID')).to eq('QuxModelID')
+ end
+
+ it 'returns the same value if there is no deprecation' do
+ expect(described_class.apply_to_graphql_name('ProjectID')).to eq('ProjectID')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb b/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb
new file mode 100644
index 00000000000..5fb5232a4dd
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/redis/trace_chunks_check_spec.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../simple_check_shared'
+
+RSpec.describe Gitlab::HealthChecks::Redis::TraceChunksCheck do
+ include_examples 'simple_check', 'redis_trace_chunks_ping', 'RedisTraceChunks', 'PONG'
+end
diff --git a/spec/lib/gitlab/health_checks/unicorn_check_spec.rb b/spec/lib/gitlab/health_checks/unicorn_check_spec.rb
deleted file mode 100644
index 1cc44016002..00000000000
--- a/spec/lib/gitlab/health_checks/unicorn_check_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::HealthChecks::UnicornCheck do
- let(:result_class) { Gitlab::HealthChecks::Result }
- let(:readiness) { described_class.readiness }
- let(:metrics) { described_class.metrics }
-
- before do
- described_class.clear_memoization(:http_servers)
- end
-
- shared_examples 'with state' do |(state, message)|
- it "does provide readiness" do
- expect(readiness).to eq(result_class.new('unicorn_check', state, message))
- end
-
- it "does provide metrics" do
- expect(metrics).to include(
- an_object_having_attributes(name: 'unicorn_check_success', value: state ? 1 : 0))
- expect(metrics).to include(
- an_object_having_attributes(name: 'unicorn_check_latency_seconds', value: be >= 0))
- end
- end
-
- context 'when Unicorn is not loaded' do
- before do
- allow(Gitlab::Runtime).to receive(:unicorn?).and_return(false)
- hide_const('Unicorn')
- end
-
- it "does not provide readiness and metrics" do
- expect(readiness).to be_nil
- expect(metrics).to be_nil
- end
- end
-
- context 'when Unicorn is loaded' do
- let(:http_server_class) { Struct.new(:worker_processes) }
-
- before do
- allow(Gitlab::Runtime).to receive(:unicorn?).and_return(true)
- stub_const('Unicorn::HttpServer', http_server_class)
- end
-
- context 'when no servers are running' do
- it_behaves_like 'with state', [false, 'unexpected Unicorn check result: 0']
- end
-
- context 'when servers without workers are running' do
- before do
- http_server_class.new(0)
- end
-
- it_behaves_like 'with state', [false, 'unexpected Unicorn check result: 0']
- end
-
- context 'when servers with workers are running' do
- before do
- http_server_class.new(1)
- end
-
- it_behaves_like 'with state', true
- end
- end
-end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index a5e4d37d306..1f06019c929 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::Highlight do
end
it 'increments the metric for oversized files' do
- expect { result }.to change { over_highlight_size_limit('text highlighter') }.by(1)
+ expect { result }.to change { over_highlight_size_limit('file size: 0.0001') }.by(1)
end
it 'returns plain version for long content' do
@@ -143,9 +143,21 @@ RSpec.describe Gitlab::Highlight do
end
describe 'highlight timeouts' do
- context 'when there is a timeout error while highlighting' do
- let(:result) { described_class.highlight(file_name, content) }
+ let(:result) { described_class.highlight(file_name, content, language: "ruby") }
+
+ context 'when there is an attempt' do
+ it "increments the attempt counter with a defined language" do
+ expect { result }.to change { highlight_attempt_total("ruby") }
+ end
+
+ it "increments the attempt counter with an undefined language" do
+ expect do
+ described_class.highlight(file_name, content)
+ end.to change { highlight_attempt_total("undefined") }
+ end
+ end
+ context 'when there is a timeout error while highlighting' do
before do
allow(Timeout).to receive(:timeout).twice.and_raise(Timeout::Error)
# This is done twice because it's rescued first and then
@@ -177,6 +189,12 @@ RSpec.describe Gitlab::Highlight do
.get(source: source)
end
+ def highlight_attempt_total(source)
+ Gitlab::Metrics
+ .counter(:file_highlighting_attempt, 'Counts the times highlighting has been attempted on a file')
+ .get(source: source)
+ end
+
def over_highlight_size_limit(source)
Gitlab::Metrics
.counter(:over_highlight_size_limit,
diff --git a/spec/lib/gitlab/hook_data/issue_builder_spec.rb b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
index 8f898d898de..8f976bcf09d 100644
--- a/spec/lib/gitlab/hook_data/issue_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issue_builder_spec.rb
@@ -42,8 +42,10 @@ RSpec.describe Gitlab::HookData::IssueBuilder do
it 'includes additional attrs' do
expect(data).to include(:total_time_spent)
+ expect(data).to include(:time_change)
expect(data).to include(:human_time_estimate)
expect(data).to include(:human_total_time_spent)
+ expect(data).to include(:human_time_change)
expect(data).to include(:assignee_ids)
expect(data).to include(:state)
expect(data).to include('labels' => [label.hook_attrs])
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
index 0339faa9fcf..9e6ad35861f 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -57,8 +57,10 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
expect(data).to include(:last_commit)
expect(data).to include(:work_in_progress)
expect(data).to include(:total_time_spent)
+ expect(data).to include(:time_change)
expect(data).to include(:human_time_estimate)
expect(data).to include(:human_total_time_spent)
+ expect(data).to include(:human_time_change)
end
context 'when the MR has an image in the description' do
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 38f1d48798b..451fd6c6f46 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -27,20 +27,30 @@ RSpec.describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
expect(subject.new(url: example_url, http_method: 'whatever')).not_to be_valid
end
- it 'onyl allow urls as upload urls' do
+ it 'only allow urls as upload urls' do
expect(subject.new(url: example_url)).to be_valid
expect(subject.new(url: 'whatever')).not_to be_valid
end
end
describe '#execute' do
- it 'removes the exported project file after the upload' do
- allow(strategy).to receive(:send_file)
- allow(strategy).to receive(:handle_response_error)
+ context 'when upload succeeds' do
+ before do
+ allow(strategy).to receive(:send_file)
+ allow(strategy).to receive(:handle_response_error)
+ end
+
+ it 'does not remove the exported project file after the upload' do
+ expect(project).not_to receive(:remove_exports)
- expect(project).to receive(:remove_exports)
+ strategy.execute(user, project)
+ end
- strategy.execute(user, project)
+ it 'has finished export status' do
+ strategy.execute(user, project)
+
+ expect(project.export_status).to eq(:finished)
+ end
end
context 'when upload fails' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index f81db1413c2..781c55f8d9b 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -342,10 +342,9 @@ container_repositories:
- project
- name
project:
-- external_approval_rules
+- external_status_checks
- taggings
- base_tags
-- tags
- topic_taggings
- topics
- chat_services
@@ -362,11 +361,11 @@ project:
- boards
- last_event
- integrations
-- campfire_service
-- confluence_service
-- datadog_service
-- discord_service
-- drone_ci_service
+- campfire_integration
+- confluence_integration
+- datadog_integration
+- discord_integration
+- drone_ci_integration
- emails_on_push_service
- pipelines_email_service
- mattermost_slash_commands_service
@@ -376,26 +375,25 @@ project:
- pivotaltracker_service
- prometheus_service
- flowdock_service
-- assembla_service
-- asana_service
+- assembla_integration
+- asana_integration
- slack_service
- microsoft_teams_service
- mattermost_service
- hangouts_chat_service
- unify_circuit_service
-- buildkite_service
-- bamboo_service
+- buildkite_integration
+- bamboo_integration
- teamcity_service
- pushover_service
- jira_service
- redmine_service
- youtrack_service
-- custom_issue_tracker_service
-- bugzilla_service
+- custom_issue_tracker_integration
+- bugzilla_integration
- ewm_service
- external_wiki_service
- mock_ci_service
-- mock_deployment_service
- mock_monitoring_service
- forked_to_members
- forked_from_project
@@ -571,6 +569,7 @@ project:
- exported_protected_branches
- incident_management_oncall_schedules
- incident_management_oncall_rotations
+- incident_management_escalation_policies
- debian_distributions
- merge_request_metrics
- security_orchestration_policy_configuration
@@ -624,6 +623,7 @@ metrics_setting:
- project
protected_environments:
- project
+- group
- deploy_access_levels
deploy_access_levels:
- protected_environment
diff --git a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
index df33b4896a4..6a7ff33465d 100644
--- a/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/base/relation_factory_spec.rb
@@ -43,6 +43,15 @@ RSpec.describe Gitlab::ImportExport::Base::RelationFactory do
end
end
+ context 'when author relation' do
+ let(:relation_sym) { :author }
+ let(:relation_hash) { { 'name' => 'User', 'project_id' => project.id } }
+
+ it 'returns author hash unchanged' do
+ expect(subject).to eq(relation_hash)
+ end
+ end
+
context 'when #setup_models is not implemented' do
it 'raises NotImplementedError' do
expect { subject }.to raise_error(NotImplementedError)
diff --git a/spec/lib/gitlab/import_export/command_line_util_spec.rb b/spec/lib/gitlab/import_export/command_line_util_spec.rb
index 4000e303816..39a10f87083 100644
--- a/spec/lib/gitlab/import_export/command_line_util_spec.rb
+++ b/spec/lib/gitlab/import_export/command_line_util_spec.rb
@@ -42,6 +42,8 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
filename = File.basename(tempfile.path)
subject.gzip(dir: path, filename: filename)
+
+ expect(File.exist?("#{tempfile.path}.gz")).to eq(true)
end
context 'when exception occurs' do
@@ -50,4 +52,25 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
end
end
end
+
+ describe '#gunzip' do
+ it 'decompresses specified file' do
+ tmpdir = Dir.mktmpdir
+ filename = 'labels.ndjson.gz'
+ gz_filepath = "spec/fixtures/bulk_imports/gz/#{filename}"
+ FileUtils.copy_file(gz_filepath, File.join(tmpdir, filename))
+
+ subject.gunzip(dir: tmpdir, filename: filename)
+
+ expect(File.exist?(File.join(tmpdir, 'labels.ndjson'))).to eq(true)
+
+ FileUtils.remove_entry(tmpdir)
+ end
+
+ context 'when exception occurs' do
+ it 'raises an exception' do
+ expect { subject.gunzip(dir: path, filename: 'test') }.to raise_error(Gitlab::ImportExport::Error)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb
index 96c467e78d6..fe3b638d20f 100644
--- a/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb
+++ b/spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb
@@ -24,6 +24,14 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
it 'returns true' do
expect(subject.valid?).to eq(true)
end
+
+ context 'when waiter thread no longer exists' do
+ it 'does not raise exception' do
+ allow(Process).to receive(:getpgid).and_raise(Errno::ESRCH)
+
+ expect(subject.valid?).to eq(true)
+ end
+ end
end
context 'when file exceeds allowed decompressed size' do
diff --git a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
index 908896e4891..c52daa8ccfd 100644
--- a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeSaver do
end
it 'fails to export a group' do
- allow_next_instance_of(Gitlab::ImportExport::JSON::NdjsonWriter) do |ndjson_writer|
+ allow_next_instance_of(Gitlab::ImportExport::Json::NdjsonWriter) do |ndjson_writer|
allow(ndjson_writer).to receive(:write_relation_array).and_raise(RuntimeError, 'exception')
end
diff --git a/spec/lib/gitlab/import_export/import_failure_service_spec.rb b/spec/lib/gitlab/import_export/import_failure_service_spec.rb
index c8bb067d40c..51f1fc9c6a2 100644
--- a/spec/lib/gitlab/import_export/import_failure_service_spec.rb
+++ b/spec/lib/gitlab/import_export/import_failure_service_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::ImportExport::ImportFailureService do
let(:importable) { create(:merge_request) }
it 'raise exception' do
- expect { subject }.to raise_exception(ActiveRecord::AssociationNotFoundError, "Association named 'import_failures' was not found on MergeRequest; perhaps you misspelled it?")
+ expect { subject }.to raise_exception(ActiveRecord::AssociationNotFoundError, /Association named 'import_failures' was not found on MergeRequest/)
end
end
end
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
index e092891f236..793b3ebfb9e 100644
--- a/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_relative 'shared_example'
-RSpec.describe Gitlab::ImportExport::JSON::LegacyReader::File do
+RSpec.describe Gitlab::ImportExport::Json::LegacyReader::File do
it_behaves_like 'import/export json legacy reader' do
let(:valid_path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
let(:data) { valid_path }
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
index e47122b6151..57d66dc0f50 100644
--- a/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_relative 'shared_example'
-RSpec.describe Gitlab::ImportExport::JSON::LegacyReader::Hash do
+RSpec.describe Gitlab::ImportExport::Json::LegacyReader::Hash do
it_behaves_like 'import/export json legacy reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
diff --git a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
index eb7a2d4aa8b..ab2c4cc2059 100644
--- a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::JSON::LegacyWriter do
+RSpec.describe Gitlab::ImportExport::Json::LegacyWriter do
let(:path) { "#{Dir.tmpdir}/legacy_writer_spec/test.json" }
subject do
diff --git a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
index b477ac45577..0ca4c4ccc87 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::JSON::NdjsonReader do
+RSpec.describe Gitlab::ImportExport::Json::NdjsonReader do
include ImportExport::CommonUtil
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/light/tree' }
diff --git a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
index 2a5e802bdc5..9be95591ae9 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe Gitlab::ImportExport::JSON::NdjsonWriter do
+RSpec.describe Gitlab::ImportExport::Json::NdjsonWriter do
include ImportExport::CommonUtil
let(:path) { "#{Dir.tmpdir}/ndjson_writer_spec/tree" }
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 a0b2faaecfe..deb22de9160 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 do
let_it_be(:user) { create(:user) }
let_it_be(:release) { create(:release) }
let_it_be(:group) { create(:group) }
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::ImportExport::JSON::StreamingSerializer do
end
let(:exportable_path) { 'project' }
- let(:json_writer) { instance_double('Gitlab::ImportExport::JSON::LegacyWriter') }
+ let(:json_writer) { instance_double('Gitlab::ImportExport::Json::LegacyWriter') }
let(:hash) { { name: exportable.name, description: exportable.description }.stringify_keys }
let(:include) { [] }
let(:custom_orderer) { nil }
diff --git a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
index 454cc74b9d4..3b7ed7cb32b 100644
--- a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::ImportExport::LegacyRelationTreeSaver do
stub_feature_flags(export_reduce_relation_batch_size: true)
end
- include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::JSON::StreamingSerializer::SMALLER_BATCH_SIZE
+ include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::SMALLER_BATCH_SIZE
end
context 'when export_reduce_relation_batch_size feature flag is disabled' do
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::ImportExport::LegacyRelationTreeSaver do
stub_feature_flags(export_reduce_relation_batch_size: false)
end
- include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::JSON::StreamingSerializer::BATCH_SIZE
+ include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE
end
end
end
diff --git a/spec/lib/gitlab/import_export/project/export_task_spec.rb b/spec/lib/gitlab/import_export/project/export_task_spec.rb
index 7fcd2187a90..3dd1e9257cc 100644
--- a/spec/lib/gitlab/import_export/project/export_task_spec.rb
+++ b/spec/lib/gitlab/import_export/project/export_task_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe Gitlab::ImportExport::Project::ExportTask do
+RSpec.describe Gitlab::ImportExport::Project::ExportTask, :silence_stdout do
let_it_be(:username) { 'root' }
let(:namespace_path) { username }
let_it_be(:user) { create(:user, username: username) }
diff --git a/spec/lib/gitlab/import_export/project/import_task_spec.rb b/spec/lib/gitlab/import_export/project/import_task_spec.rb
index 90f4501acdc..c847224cb9b 100644
--- a/spec/lib/gitlab/import_export/project/import_task_spec.rb
+++ b/spec/lib/gitlab/import_export/project/import_task_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe Gitlab::ImportExport::Project::ImportTask, :request_store do
+RSpec.describe Gitlab::ImportExport::Project::ImportTask, :request_store, :silence_stdout do
let(:username) { 'root' }
let(:namespace_path) { username }
let!(:user) { create(:user, username: username) }
diff --git a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
index f87f79d4462..f6a028383f2 100644
--- a/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/sample/relation_tree_restorer_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Gitlab::ImportExport::Project::Sample::RelationTreeRestorer do
let(:relation_factory) { Gitlab::ImportExport::Project::Sample::RelationFactory }
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' }
- let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
+ let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'initializes relation_factory with date_calculator as parameter' do
expect(Gitlab::ImportExport::Project::Sample::RelationFactory).to receive(:create).with(hash_including(:date_calculator)).at_least(:once).times
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 bc5e6ea7bb3..1b5fba85020 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -600,9 +600,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
setup_import_export_config('light')
setup_reader(reader)
- expect(project)
- .to receive(:merge_requests)
- .and_raise(exception)
+ expect(project).to receive(:merge_requests).and_call_original
+ expect(project).to receive(:merge_requests).and_raise(exception)
end
it 'report post import error' do
@@ -618,12 +617,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
setup_import_export_config('light')
setup_reader(reader)
- expect(project)
- .to receive(:merge_requests)
- .and_raise(exception)
- expect(project)
- .to receive(:merge_requests)
- .and_call_original
+ expect(project).to receive(:merge_requests).and_call_original
+ expect(project).to receive(:merge_requests).and_raise(exception)
+ expect(project).to receive(:merge_requests).and_call_original
expect(restored_project_json).to eq(true)
end
@@ -824,9 +820,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
end
before do
- allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(true)
- allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(false)
- allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:tree_hash) { tree_hash }
+ allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:exist?).and_return(true)
+ allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(false)
+ allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:tree_hash) { tree_hash }
end
context 'no group visibility' do
diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
index d3c14b1f8fe..9325cdac9ed 100644
--- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
context 'using legacy reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
let(:relation_reader) do
- Gitlab::ImportExport::JSON::LegacyReader::File.new(
+ Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.project_relation_names,
allowed_path: 'project'
@@ -117,14 +117,14 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
context 'using ndjson reader' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
- let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
+ let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it_behaves_like 'import project successfully'
end
context 'with invalid relations' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' }
- let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
+ let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
it 'logs the invalid relation and its errors' do
expect(relation_tree_restorer.shared.logger)
@@ -151,7 +151,7 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do
let(:object_builder) { Gitlab::ImportExport::Group::ObjectBuilder }
let(:relation_factory) { Gitlab::ImportExport::Group::RelationFactory }
let(:relation_reader) do
- Gitlab::ImportExport::JSON::LegacyReader::File.new(
+ Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: reader.group_relation_names)
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 70ebff2a54e..2173bee6b4b 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -102,7 +102,6 @@ ProjectLabel:
- template
- description
- priority
-- remove_on_close
Milestone:
- id
- title
@@ -702,6 +701,7 @@ ProjectSetting:
ProtectedEnvironment:
- id
- project_id
+- group_id
- name
- created_at
- updated_at
diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb
index 22f2d4c5077..feeb88397eb 100644
--- a/spec/lib/gitlab/import_export/shared_spec.rb
+++ b/spec/lib/gitlab/import_export/shared_spec.rb
@@ -37,6 +37,28 @@ RSpec.describe Gitlab::ImportExport::Shared do
end
end
+ context 'with a group on disk' do
+ describe '#base_path' do
+ it 'uses hashed storage path' do
+ group = create(:group)
+ subject = described_class.new(group)
+ base_path = %(/tmp/gitlab_exports/@groups/)
+
+ expect(subject.base_path).to match(/#{base_path}\h{2}\/\h{2}\/\h{64}/)
+ end
+ end
+ end
+
+ context 'when exportable type is unsupported' do
+ describe '#base_path' do
+ it 'raises' do
+ subject = described_class.new('test')
+
+ expect { subject.base_path }.to raise_error(Gitlab::ImportExport::Error, 'Unsupported Exportable Type String')
+ end
+ end
+ end
+
describe '#error' do
let(:error) { StandardError.new('Error importing into /my/folder Permission denied @ unlink_internal - /var/opt/gitlab/gitlab-rails/shared/a/b/c/uploads/file') }
diff --git a/spec/lib/gitlab/instrumentation/redis_base_spec.rb b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
index 07be0ccf6e9..a7e08b5a9bd 100644
--- a/spec/lib/gitlab/instrumentation/redis_base_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_base_spec.rb
@@ -18,24 +18,6 @@ RSpec.describe Gitlab::Instrumentation::RedisBase, :request_store do
end
end
- describe '.known_payload_keys' do
- it 'returns generated payload keys' do
- expect(instrumentation_class_a.known_payload_keys).to eq([:redis_instance_a_calls,
- :redis_instance_a_duration_s,
- :redis_instance_a_read_bytes,
- :redis_instance_a_write_bytes])
- end
-
- it 'does not call calculation methods' do
- expect(instrumentation_class_a).not_to receive(:get_request_count)
- expect(instrumentation_class_a).not_to receive(:query_time)
- expect(instrumentation_class_a).not_to receive(:read_bytes)
- expect(instrumentation_class_a).not_to receive(:write_bytes)
-
- instrumentation_class_a.known_payload_keys
- end
- end
-
describe '.payload' do
it 'returns values that are higher than 0' do
allow(instrumentation_class_a).to receive(:get_request_count) { 1 }
diff --git a/spec/lib/gitlab/instrumentation/redis_spec.rb b/spec/lib/gitlab/instrumentation/redis_spec.rb
index e927f39cae2..6cddf958f2a 100644
--- a/spec/lib/gitlab/instrumentation/redis_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_spec.rb
@@ -26,46 +26,6 @@ RSpec.describe Gitlab::Instrumentation::Redis do
it_behaves_like 'aggregation of redis storage data', :read_bytes
it_behaves_like 'aggregation of redis storage data', :write_bytes
- describe '.known_payload_keys' do
- it 'returns all known payload keys' do
- expected_keys = [
- :redis_calls,
- :redis_duration_s,
- :redis_read_bytes,
- :redis_write_bytes,
- :redis_action_cable_calls,
- :redis_action_cable_duration_s,
- :redis_action_cable_read_bytes,
- :redis_action_cable_write_bytes,
- :redis_cache_calls,
- :redis_cache_duration_s,
- :redis_cache_read_bytes,
- :redis_cache_write_bytes,
- :redis_queues_calls,
- :redis_queues_duration_s,
- :redis_queues_read_bytes,
- :redis_queues_write_bytes,
- :redis_shared_state_calls,
- :redis_shared_state_duration_s,
- :redis_shared_state_read_bytes,
- :redis_shared_state_write_bytes
- ]
-
- expect(described_class.known_payload_keys).to eq(expected_keys)
- end
-
- it 'does not call storage calculation methods' do
- described_class::STORAGES.each do |storage|
- expect(storage).not_to receive(:get_request_count)
- expect(storage).not_to receive(:query_time)
- expect(storage).not_to receive(:read_bytes)
- expect(storage).not_to receive(:write_bytes)
- end
-
- described_class.known_payload_keys
- end
- end
-
describe '.payload', :request_store do
before do
Gitlab::Redis::Cache.with { |redis| redis.set('cache-test', 321) }
@@ -108,7 +68,8 @@ RSpec.describe Gitlab::Instrumentation::Redis do
.to contain_exactly(details_row.merge(storage: 'ActionCable'),
details_row.merge(storage: 'Cache'),
details_row.merge(storage: 'Queues'),
- details_row.merge(storage: 'SharedState'))
+ details_row.merge(storage: 'SharedState'),
+ details_row.merge(storage: 'TraceChunks'))
end
end
end
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 488324ccddc..28ae90d4947 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -117,6 +117,42 @@ RSpec.describe Gitlab::InstrumentationHelper do
end
end
end
+
+ context 'when load balancing is enabled' do
+ include_context 'clear DB Load Balancing configuration'
+
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ end
+
+ it 'includes DB counts' do
+ subject
+
+ expect(payload).to include(db_replica_count: 0,
+ db_replica_cached_count: 0,
+ db_primary_count: 0,
+ db_primary_cached_count: 0,
+ db_primary_wal_count: 0,
+ db_replica_wal_count: 0)
+ end
+ end
+
+ context 'when load balancing is disabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+ end
+
+ it 'does not include DB counts' do
+ subject
+
+ expect(payload).not_to include(db_replica_count: 0,
+ db_replica_cached_count: 0,
+ db_primary_count: 0,
+ db_primary_cached_count: 0,
+ db_primary_wal_count: 0,
+ db_replica_wal_count: 0)
+ end
+ end
end
describe '.queue_duration_for_job' do
diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb
index 42c4b315edf..f9f57752b0a 100644
--- a/spec/lib/gitlab/json_spec.rb
+++ b/spec/lib/gitlab/json_spec.rb
@@ -411,7 +411,7 @@ RSpec.describe Gitlab::Json do
end
describe Gitlab::Json::LimitedEncoder do
- subject { described_class.encode(obj, limit: 8.kilobytes) }
+ subject { described_class.encode(obj, limit: 10.kilobytes) }
context 'when object size is acceptable' do
let(:obj) { { test: true } }
@@ -431,6 +431,16 @@ RSpec.describe Gitlab::Json do
end
end
+ context 'when object contains ASCII-8BIT encoding' do
+ let(:obj) { [{ a: "\x8F" }] * 1000 }
+
+ it 'does not raise encoding error' do
+ expect { subject }.not_to raise_error
+ expect(subject).to be_a(String)
+ expect(subject.size).to eq(10001)
+ end
+ end
+
context 'when json_limited_encoder is disabled' do
let(:obj) { [{ test: true }] * 1000 }
diff --git a/spec/lib/gitlab/kas/client_spec.rb b/spec/lib/gitlab/kas/client_spec.rb
new file mode 100644
index 00000000000..7bf2d30ca48
--- /dev/null
+++ b/spec/lib/gitlab/kas/client_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Kas::Client do
+ let_it_be(:project) { create(:project) }
+
+ describe '#initialize' do
+ context 'kas is not enabled' do
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(false)
+ end
+
+ it 'raises a configuration error' do
+ expect { described_class.new }.to raise_error(described_class::ConfigurationError, 'GitLab KAS is not enabled')
+ end
+ end
+
+ context 'internal url is not set' do
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Kas).to receive(:internal_url).and_return(nil)
+ end
+
+ it 'raises a configuration error' do
+ expect { described_class.new }.to raise_error(described_class::ConfigurationError, 'KAS internal URL is not configured')
+ end
+ end
+ end
+
+ describe 'gRPC calls' do
+ let(:token) { instance_double(JSONWebToken::HMACToken, encoded: 'test-token') }
+
+ before do
+ allow(Gitlab::Kas).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Kas).to receive(:internal_url).and_return('grpc://example.kas.internal')
+
+ expect(JSONWebToken::HMACToken).to receive(:new)
+ .with(Gitlab::Kas.secret)
+ .and_return(token)
+
+ expect(token).to receive(:issuer=).with(Settings.gitlab.host)
+ expect(token).to receive(:audience=).with(described_class::JWT_AUDIENCE)
+ end
+
+ describe '#list_agent_config_files' do
+ let(:stub) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub) }
+
+ let(:request) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest) }
+ let(:response) { double(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesResponse, config_files: agent_configurations) }
+
+ let(:repository) { instance_double(Gitlab::Agent::Modserver::Repository) }
+ let(:gitaly_address) { instance_double(Gitlab::Agent::Modserver::GitalyAddress) }
+
+ let(:agent_configurations) { [double] }
+
+ subject { described_class.new.list_agent_config_files(project: project) }
+
+ before do
+ expect(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub).to receive(:new)
+ .with('example.kas.internal', :this_channel_is_insecure, timeout: described_class::TIMEOUT)
+ .and_return(stub)
+
+ expect(Gitlab::Agent::Modserver::Repository).to receive(:new)
+ .with(project.repository.gitaly_repository.to_h)
+ .and_return(repository)
+
+ expect(Gitlab::Agent::Modserver::GitalyAddress).to receive(:new)
+ .with(Gitlab::GitalyClient.connection_data(project.repository_storage))
+ .and_return(gitaly_address)
+
+ expect(Gitlab::Agent::ConfigurationProject::Rpc::ListAgentConfigFilesRequest).to receive(:new)
+ .with(repository: repository, gitaly_address: gitaly_address)
+ .and_return(request)
+
+ expect(stub).to receive(:list_agent_config_files)
+ .with(request, metadata: { 'authorization' => 'bearer test-token' })
+ .and_return(response)
+ end
+
+ it { expect(subject).to eq(agent_configurations) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb
index e323f76b42e..c9d40f785b8 100644
--- a/spec/lib/gitlab/kas_spec.rb
+++ b/spec/lib/gitlab/kas_spec.rb
@@ -65,6 +65,12 @@ RSpec.describe Gitlab::Kas do
end
end
+ describe '.internal_url' do
+ it 'returns gitlab_kas internal_url config' do
+ expect(described_class.internal_url).to eq(Gitlab.config.gitlab_kas.internal_url)
+ end
+ end
+
describe '.version' do
it 'returns gitlab_kas version config' do
version_file = Rails.root.join(described_class::VERSION_FILE)
diff --git a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb
deleted file mode 100644
index 435c296d5f1..00000000000
--- a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe Gitlab::Kubernetes::Helm::Parsers::ListV2 do
- let(:valid_file_contents) do
- <<~EOF
- {
- "Next": "",
- "Releases": [
- {
- "Name": "certmanager",
- "Revision": 2,
- "Updated": "Sun Mar 29 06:55:42 2020",
- "Status": "DEPLOYED",
- "Chart": "cert-manager-v0.10.1",
- "AppVersion": "v0.10.1",
- "Namespace": "gitlab-managed-apps"
- },
- {
- "Name": "certmanager-crds",
- "Revision": 2,
- "Updated": "Sun Mar 29 06:55:32 2020",
- "Status": "DEPLOYED",
- "Chart": "cert-manager-crds-v0.2.0",
- "AppVersion": "release-0.10",
- "Namespace": "gitlab-managed-apps"
- },
- {
- "Name": "certmanager-issuer",
- "Revision": 1,
- "Updated": "Tue Feb 18 10:04:04 2020",
- "Status": "FAILED",
- "Chart": "cert-manager-issuer-v0.1.0",
- "AppVersion": "",
- "Namespace": "gitlab-managed-apps"
- },
- {
- "Name": "runner",
- "Revision": 2,
- "Updated": "Sun Mar 29 07:01:01 2020",
- "Status": "DEPLOYED",
- "Chart": "gitlab-runner-0.14.0",
- "AppVersion": "12.8.0",
- "Namespace": "gitlab-managed-apps"
- }
- ]
- }
- EOF
- end
-
- describe '#initialize' do
- it 'initializes without error' do
- expect do
- described_class.new(valid_file_contents)
- end.not_to raise_error
- end
-
- it 'raises an error on invalid JSON' do
- expect do
- described_class.new('')
- end.to raise_error(described_class::ParserError)
- end
- end
-
- describe '#releases' do
- subject(:list_v2) { described_class.new(valid_file_contents) }
-
- it 'returns list of releases' do
- expect(list_v2.releases).to match([
- a_hash_including('Name' => 'certmanager', 'Status' => 'DEPLOYED'),
- a_hash_including('Name' => 'certmanager-crds', 'Status' => 'DEPLOYED'),
- a_hash_including('Name' => 'certmanager-issuer', 'Status' => 'FAILED'),
- a_hash_including('Name' => 'runner', 'Status' => 'DEPLOYED')
- ])
- end
-
- context 'empty Releases' do
- let(:valid_file_contents) { '{}' }
-
- it 'returns an empty array' do
- expect(list_v2.releases).to eq([])
- end
- end
-
- context 'invalid Releases' do
- let(:invalid_file_contents) do
- '{ "Releases" : ["a", "b"] }'
- end
-
- subject(:list_v2) { described_class.new(invalid_file_contents) }
-
- it 'raises an error' do
- expect do
- list_v2.releases
- end.to raise_error(described_class::ParserError, 'Invalid format for Releases')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/mail_room/mail_room_spec.rb b/spec/lib/gitlab/mail_room/mail_room_spec.rb
index ab9a9a035f1..a42da4ad3e0 100644
--- a/spec/lib/gitlab/mail_room/mail_room_spec.rb
+++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb
@@ -33,6 +33,10 @@ RSpec.describe Gitlab::MailRoom do
described_class.instance_variable_set(:@enabled_configs, nil)
end
+ after do
+ described_class.instance_variable_set(:@enabled_configs, nil)
+ end
+
describe '#enabled_configs' do
before do
allow(described_class).to receive(:load_yaml).and_return(configs)
diff --git a/spec/lib/gitlab/markdown_cache/field_data_spec.rb b/spec/lib/gitlab/markdown_cache/field_data_spec.rb
index 76d8cbe6b7d..6d4b57254f2 100644
--- a/spec/lib/gitlab/markdown_cache/field_data_spec.rb
+++ b/spec/lib/gitlab/markdown_cache/field_data_spec.rb
@@ -12,4 +12,11 @@ RSpec.describe Gitlab::MarkdownCache::FieldData do
it 'translates a markdown field name into a html field name' do
expect(field_data.html_field(:description)).to eq("description_html")
end
+
+ describe '#key?' do
+ specify do
+ expect(field_data.key?(:description)).to be_truthy
+ expect(field_data.key?(:something_else)).to be_falsy
+ 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 1f7daaa308d..9d5c4bdf9e2 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -7,8 +7,16 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
subject { described_class.new(app) }
+ around do |example|
+ # Simulate application context middleware
+ # In fact, this middleware cleans up the contexts after a request lifecycle
+ ::Gitlab::ApplicationContext.with_context({}) do
+ example.run
+ end
+ end
+
describe '#call' do
- let(:status) { 100 }
+ let(:status) { 200 }
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:stack_result) { [status, {}, 'body'] }
@@ -71,6 +79,17 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
end
end
+ context '@app.call returns an error code' do
+ let(:status) { '500' }
+
+ it 'tracks count but not duration' do
+ expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '500', feature_category: 'unknown')
+ expect(described_class).not_to receive(:http_request_duration_seconds)
+
+ subject.call(env)
+ end
+ end
+
context '@app.call throws exception' do
let(:http_request_duration_seconds) { double('http_request_duration_seconds') }
let(:http_requests_total) { double('http_requests_total') }
@@ -91,9 +110,9 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
end
context 'feature category header' do
- context 'when a feature category header is present' do
+ context 'when a feature category context is present' do
before do
- allow(app).to receive(:call).and_return([200, { described_class::FEATURE_CATEGORY_HEADER => 'issue_tracking' }, nil])
+ ::Gitlab::ApplicationContext.push(feature_category: 'issue_tracking')
end
it 'adds the feature category to the labels for http_requests_total' do
@@ -113,11 +132,20 @@ RSpec.describe Gitlab::Metrics::RequestsRackMiddleware, :aggregate_failures do
end
end
- context 'when the feature category header is an empty string' do
+ context 'when application raises an exception when the feature category context is present' do
before do
- allow(app).to receive(:call).and_return([200, { described_class::FEATURE_CATEGORY_HEADER => '' }, nil])
+ ::Gitlab::ApplicationContext.push(feature_category: 'issue_tracking')
+ allow(app).to receive(:call).and_raise(StandardError)
end
+ it 'adds the feature category to the labels for http_requests_total' do
+ expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: 'undefined', feature_category: 'issue_tracking')
+
+ expect { subject.call(env) }.to raise_error(StandardError)
+ end
+ end
+
+ context 'when the feature category context is not available' do
it 'sets the feature category to unknown' do
expect(described_class).to receive_message_chain(:http_requests_total, :increment).with(method: 'get', status: '200', feature_category: 'unknown')
expect(described_class).not_to receive(:http_health_requests_total)
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
deleted file mode 100644
index 7971a7cabd5..00000000000
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Metrics::Samplers::UnicornSampler do
- subject { described_class.new(1.second) }
-
- it_behaves_like 'metrics sampler', 'UNICORN_SAMPLER'
-
- describe '#sample' do
- let(:unicorn) { Module.new }
- let(:raindrops) { double('raindrops') }
- let(:stats) { double('stats') }
-
- before do
- stub_const('Unicorn', unicorn)
- stub_const('Raindrops::Linux', raindrops)
- allow(raindrops).to receive(:unix_listener_stats).and_return({})
- allow(raindrops).to receive(:tcp_listener_stats).and_return({})
- end
-
- context 'unicorn listens on unix sockets' do
- let(:socket_address) { '/some/sock' }
- let(:sockets) { [socket_address] }
-
- before do
- allow(unicorn).to receive(:listener_names).and_return(sockets)
- end
-
- it 'samples socket data' do
- expect(raindrops).to receive(:unix_listener_stats).with(sockets)
-
- subject.sample
- end
-
- context 'stats collected' do
- before do
- allow(stats).to receive(:active).and_return('active')
- allow(stats).to receive(:queued).and_return('queued')
- allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats })
- end
-
- it 'updates metrics type unix and with addr' do
- labels = { socket_type: 'unix', socket_address: socket_address }
-
- expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
- expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
-
- subject.sample
- end
- end
- end
-
- context 'unicorn listens on tcp sockets' do
- let(:tcp_socket_address) { '0.0.0.0:8080' }
- let(:tcp_sockets) { [tcp_socket_address] }
-
- before do
- allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
- end
-
- it 'samples socket data' do
- expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets)
-
- subject.sample
- end
-
- context 'stats collected' do
- before do
- allow(stats).to receive(:active).and_return('active')
- allow(stats).to receive(:queued).and_return('queued')
- allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats })
- end
-
- it 'updates metrics type unix and with addr' do
- labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
-
- expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
- expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
-
- subject.sample
- end
- end
- end
-
- context 'unicorn workers' do
- before do
- allow(unicorn).to receive(:listener_names).and_return([])
- end
-
- context 'without http server' do
- it "does set unicorn_workers to 0" do
- expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 0)
-
- subject.sample
- end
- end
-
- context 'with http server' do
- let(:http_server_class) { Struct.new(:worker_processes) }
- let!(:http_server) { http_server_class.new(5) }
-
- before do
- stub_const('Unicorn::HttpServer', http_server_class)
- end
-
- it "sets additional metrics" do
- expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 5)
-
- subject.sample
- end
- end
- end
- end
-
- describe '#start' do
- context 'when enabled' do
- before do
- allow(subject).to receive(:enabled?).and_return(true)
- end
-
- it 'creates new thread' do
- expect(Thread).to receive(:new)
-
- subject.start
- end
- end
-
- context 'when disabled' do
- before do
- allow(subject).to receive(:enabled?).and_return(false)
- end
-
- it "doesn't create new thread" do
- expect(Thread).not_to receive(:new)
-
- subject.start
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 6bfcfa21289..cffa62c3a52 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -150,4 +150,140 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do
it_behaves_like 'track generic sql events'
end
end
+
+ context 'Database Load Balancing enabled' do
+ let(:payload) { { sql: 'SELECT * FROM users WHERE id = 10', connection: connection } }
+
+ let(:event) do
+ double(
+ :event,
+ name: 'sql.active_record',
+ duration: 2,
+ payload: payload
+ )
+ end
+
+ # Emulate Marginalia pre-pending comments
+ def sql(query, comments: true)
+ if comments && !%w[BEGIN COMMIT].include?(query)
+ "/*application:web,controller:badges,action:pipeline,correlation_id:01EYN39K9VMJC56Z7808N7RSRH*/ #{query}"
+ else
+ query
+ end
+ end
+
+ shared_examples 'track sql events for each role' do
+ where(:name, :sql_query, :record_query, :record_write_query, :record_cached_query, :record_wal_query) do
+ 'SQL' | 'SELECT * FROM users WHERE id = 10' | true | false | false | false
+ 'SQL' | 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' | true | false | false | false
+ 'SQL' | 'SELECT * FROM users WHERE id = 10 FOR UPDATE' | true | true | false | false
+ 'SQL' | 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' | true | true | false | false
+ 'SQL' | 'DELETE FROM users where id = 10' | true | true | false | false
+ 'SQL' | 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' | true | true | false | false
+ 'SQL' | 'UPDATE users SET admin = true WHERE id = 10' | true | true | false | false
+ 'SQL' | 'SELECT pg_current_wal_insert_lsn()::text AS location' | true | false | false | true
+ 'SQL' | 'SELECT pg_last_wal_replay_lsn()::text AS location' | true | false | false | true
+ 'CACHE' | 'SELECT * FROM users WHERE id = 10' | true | false | true | false
+ 'SCHEMA' | "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass" | false | false | false | false
+ nil | 'BEGIN' | false | false | false | false
+ nil | 'COMMIT' | false | false | false | false
+ end
+
+ with_them do
+ let(:payload) { { name: name, sql: sql(sql_query, comments: comments), connection: connection } }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ end
+
+ context 'query using a connection to a replica' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).and_return(:replica)
+ end
+
+ it 'queries connection db role' do
+ subscriber.sql(event)
+
+ if record_query
+ expect(Gitlab::Database::LoadBalancing).to have_received(:db_role_for_connection).with(connection)
+ end
+ end
+
+ it_behaves_like 'record ActiveRecord metrics', :replica
+ it_behaves_like 'store ActiveRecord info in RequestStore', :replica
+ end
+
+ context 'query using a connection to a primary' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).and_return(:primary)
+ end
+
+ it 'queries connection db role' do
+ subscriber.sql(event)
+
+ if record_query
+ expect(Gitlab::Database::LoadBalancing).to have_received(:db_role_for_connection).with(connection)
+ end
+ end
+
+ it_behaves_like 'record ActiveRecord metrics', :primary
+ it_behaves_like 'store ActiveRecord info in RequestStore', :primary
+ end
+
+ context 'query using a connection to an unknown source' do
+ let(:transaction) { double('Gitlab::Metrics::WebTransaction') }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).and_return(nil)
+
+ allow(::Gitlab::Metrics::WebTransaction).to receive(:current).and_return(transaction)
+ allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(nil)
+
+ allow(transaction).to receive(:increment)
+ allow(transaction).to receive(:observe)
+ end
+
+ it 'does not record DB role metrics' do
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_primary_count_total".to_sym, any_args)
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_replica_count_total".to_sym, any_args)
+
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_primary_cached_count_total".to_sym, any_args)
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_replica_cached_count_total".to_sym, any_args)
+
+ expect(transaction).not_to receive(:observe).with("gitlab_sql_primary_duration_seconds".to_sym, any_args)
+ expect(transaction).not_to receive(:observe).with("gitlab_sql_replica_duration_seconds".to_sym, any_args)
+
+ subscriber.sql(event)
+ end
+
+ it 'does not store DB roles into into RequestStore' do
+ Gitlab::WithRequestStore.with_request_store do
+ subscriber.sql(event)
+
+ expect(described_class.db_counter_payload).to include(
+ db_primary_cached_count: 0,
+ db_primary_count: 0,
+ db_primary_duration_s: 0,
+ db_replica_cached_count: 0,
+ db_replica_count: 0,
+ db_replica_duration_s: 0
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'without Marginalia comments' do
+ let(:comments) { false }
+
+ it_behaves_like 'track sql events for each role'
+ end
+
+ context 'with Marginalia comments' do
+ let(:comments) { true }
+
+ it_behaves_like 'track sql events for each role'
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index d4e5a1a94f2..2ff8efcd7cb 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -12,32 +12,6 @@ RSpec.describe Gitlab::Metrics::Transaction do
}
end
- describe '#duration' do
- it 'returns the duration of a transaction in seconds' do
- transaction.run { }
-
- expect(transaction.duration).to be > 0
- end
- end
-
- describe '#run' do
- it 'yields the supplied block' do
- expect { |b| transaction.run(&b) }.to yield_control
- end
-
- it 'stores the transaction in the current thread' do
- transaction.run do
- expect(described_class.current).to eq(transaction)
- end
- end
-
- it 'removes the transaction from the current thread upon completion' do
- transaction.run { }
-
- expect(described_class.current).to be_nil
- end
- end
-
describe '#method_call_for' do
it 'returns a MethodCall' do
method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
@@ -46,6 +20,10 @@ RSpec.describe Gitlab::Metrics::Transaction do
end
end
+ describe '#run' do
+ specify { expect { transaction.run }.to raise_error(NotImplementedError) }
+ end
+
describe '#add_event' do
let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil, base_labels: {}) }
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 6ee9564ef75..5261d04c879 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -38,16 +38,6 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
end
- describe '#duration' do
- include_context 'transaction observe metrics'
-
- it 'returns the duration of a transaction in seconds' do
- transaction.run { sleep(0.5) }
-
- expect(transaction.duration).to be >= 0.5
- end
- end
-
describe '#run' do
include_context 'transaction observe metrics'
@@ -58,6 +48,9 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
it 'stores the transaction in the current thread' do
transaction.run do
expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
+ expect(described_class.current).to eq(transaction)
+
+ ['200', {}, '']
end
end
@@ -65,6 +58,33 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
transaction.run { }
expect(Thread.current[described_class::THREAD_KEY]).to be_nil
+ expect(described_class.current).to be_nil
+ end
+
+ it 'records the duration of the transaction if the request was successful' do
+ expect(transaction).to receive(:observe).with(:gitlab_transaction_duration_seconds, instance_of(Float))
+
+ transaction.run { ['200', {}, ''] }
+ end
+
+ it 'does not record the duration of the transaction if the request failed' do
+ expect(transaction).not_to receive(:observe).with(:gitlab_transaction_duration_seconds, instance_of(Float))
+
+ transaction.run { ['500', {}, ''] }
+ end
+
+ it 'does not record the duration of the transaction if it raised' do
+ expect(transaction).not_to receive(:observe).with(:gitlab_transaction_duration_seconds, instance_of(Float))
+
+ expect do
+ transaction.run { raise 'broken' }
+ end.to raise_error('broken')
+ end
+
+ it 'returns the rack response' do
+ response = ['500', {}, '']
+
+ expect(transaction.run { response }).to eq(response)
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index db5a23e2328..366843a4c03 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -92,6 +92,26 @@ RSpec.describe Gitlab::Metrics do
end
end
+ describe '.record_status_for_duration?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :should_record) do
+ 100 | false
+ 200 | true
+ 401 | true
+ nil | false
+ 500 | false
+ 503 | false
+ '100' | false
+ '201' | true
+ 'nothing' | false
+ end
+
+ with_them do
+ specify { expect(described_class.record_duration_for_status?(status)).to be(should_record) }
+ end
+ end
+
describe '.add_event' do
context 'without a transaction' do
it 'does nothing' 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 26f9ea3a637..966b23bf51a 100644
--- a/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
+++ b/spec/lib/gitlab/nav/top_nav_menu_item_spec.rb
@@ -11,10 +11,10 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem do
active: true,
icon: 'icon',
href: 'href',
- method: 'method',
view: 'view',
css_class: 'css_class',
- data: {}
+ data: {},
+ emoji: 'smile'
}
expect(described_class.build(**item)).to eq(item)
diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
index 132a0e9ca78..8a26e153385 100644
--- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
+++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
@@ -108,7 +108,6 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
let(:expected_next_page_link) { %Q(<#{incoming_api_projects_url}?#{query.merge(page_token: branch2.name).to_query}>; rel="next") }
it 'uses keyset pagination and adds link headers' do
- expect(request_context).to receive(:header).with('Links', expected_next_page_link)
expect(request_context).to receive(:header).with('Link', expected_next_page_link)
pager.paginate(finder)
@@ -119,7 +118,6 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
let(:branches) { [branch1] }
it 'uses keyset pagination without link headers' do
- expect(request_context).not_to receive(:header).with('Links', anything)
expect(request_context).not_to receive(:header).with('Link', anything)
pager.paginate(finder)
diff --git a/spec/lib/gitlab/pagination/keyset/paginator_spec.rb b/spec/lib/gitlab/pagination/keyset/paginator_spec.rb
new file mode 100644
index 00000000000..3c9a8913876
--- /dev/null
+++ b/spec/lib/gitlab/pagination/keyset/paginator_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pagination::Keyset::Paginator do
+ let_it_be(:project_1) { create(:project, created_at: 10.weeks.ago) }
+ let_it_be(:project_2) { create(:project, created_at: 2.weeks.ago) }
+ let_it_be(:project_3) { create(:project, created_at: 3.weeks.ago) }
+ let_it_be(:project_4) { create(:project, created_at: 5.weeks.ago) }
+ let_it_be(:project_5) { create(:project, created_at: 2.weeks.ago) }
+
+ describe 'pagination' do
+ let(:per_page) { 10 }
+ let(:cursor) { nil }
+ let(:scope) { Project.order(created_at: :asc, id: :asc) }
+ let(:expected_order) { [project_1, project_4, project_3, project_2, project_5] }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: cursor, per_page: per_page) }
+
+ context 'when per_page is greater than the record count' do
+ it { expect(paginator.records).to eq(expected_order) }
+ it { is_expected.not_to have_next_page }
+ it { is_expected.not_to have_previous_page }
+
+ it 'has no next and previous cursor values' do
+ expect(paginator.cursor_for_next_page).to be_nil
+ expect(paginator.cursor_for_previous_page).to be_nil
+ end
+ end
+
+ context 'when 0 records are returned' do
+ let(:scope) { Project.where(id: non_existing_record_id).order(created_at: :asc, id: :asc) }
+
+ it { expect(paginator.records).to be_empty }
+ it { is_expected.not_to have_next_page }
+ it { is_expected.not_to have_previous_page }
+ end
+
+ context 'when page size is smaller than the record count' do
+ let(:per_page) { 2 }
+
+ it { expect(paginator.records).to eq(expected_order.first(2)) }
+ it { is_expected.to have_next_page }
+ it { is_expected.not_to have_previous_page }
+
+ it 'has next page cursor' do
+ expect(paginator.cursor_for_next_page).not_to be_nil
+ end
+
+ it 'does not have previous page cursor' do
+ expect(paginator.cursor_for_previous_page).to be_nil
+ end
+
+ context 'when on the second page' do
+ let(:cursor) { scope.keyset_paginate(per_page: per_page).cursor_for_next_page }
+
+ it { expect(paginator.records).to eq(expected_order[2...4]) }
+ it { is_expected.to have_next_page }
+ it { is_expected.to have_previous_page }
+
+ context 'and then going back to the first page' do
+ let(:previous_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_previous_page }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: previous_page_cursor, per_page: per_page) }
+
+ it { expect(paginator.records).to eq(expected_order.first(2)) }
+ it { is_expected.to have_next_page }
+ it { is_expected.not_to have_previous_page }
+ end
+ end
+
+ context 'when jumping to the last page' do
+ let(:cursor) { scope.keyset_paginate(per_page: per_page).cursor_for_last_page }
+
+ it { expect(paginator.records).to eq(expected_order.last(2)) }
+ it { is_expected.not_to have_next_page }
+ it { is_expected.to have_previous_page }
+
+ context 'when paginating backwards' do
+ let(:previous_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_previous_page }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: previous_page_cursor, per_page: per_page) }
+
+ it { expect(paginator.records).to eq(expected_order[-4...-2]) }
+ it { is_expected.to have_next_page }
+ it { is_expected.to have_previous_page }
+ end
+
+ context 'when jumping to the first page' do
+ let(:first_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_first_page }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: first_page_cursor, per_page: per_page) }
+
+ it { expect(paginator.records).to eq(expected_order.first(2)) }
+ it { is_expected.to have_next_page }
+ it { is_expected.not_to have_previous_page }
+ end
+ end
+ end
+
+ describe 'default keyset direction parameter' do
+ let(:cursor_converter_class) { Gitlab::Pagination::Keyset::Paginator::Base64CursorConverter }
+ let(:per_page) { 2 }
+
+ it 'exposes the direction parameter in the cursor' do
+ cursor = paginator.cursor_for_next_page
+
+ expect(cursor_converter_class.parse(cursor)[:_kd]).to eq(described_class::FORWARD_DIRECTION)
+ end
+ end
+ end
+
+ context 'when unsupported order is given' do
+ it 'raises error' do
+ scope = Project.order(path: :asc, name: :asc, id: :desc) # Cannot build 3 column order automatically
+
+ expect { scope.keyset_paginate }.to raise_error(/does not support keyset pagination/)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/pagination/keyset/request_context_spec.rb b/spec/lib/gitlab/pagination/keyset/request_context_spec.rb
index d4255176a4e..619d8cca28c 100644
--- a/spec/lib/gitlab/pagination/keyset/request_context_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/request_context_spec.rb
@@ -57,14 +57,15 @@ RSpec.describe Gitlab::Pagination::Keyset::RequestContext do
subject { described_class.new(request_context).apply_headers(next_page) }
- it 'sets Links header with same host/path as the original request' do
+ it 'sets Link header with same host/path as the original request' do
orig_uri = URI.parse(request_context.request.url)
- expect(request_context).to receive(:header).twice do |name, header|
+ expect(request_context).to receive(:header).once do |name, header|
first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures
uri = URI.parse(first_link)
+ expect(name).to eq('Link')
expect(uri.host).to eq(orig_uri.host)
expect(uri.path).to eq(orig_uri.path)
end
@@ -72,14 +73,15 @@ RSpec.describe Gitlab::Pagination::Keyset::RequestContext do
subject
end
- it 'sets Links header with a link to the next page' do
+ it 'sets Link header with a link to the next page' do
orig_uri = URI.parse(request_context.request.url)
- expect(request_context).to receive(:header).twice do |name, header|
+ expect(request_context).to receive(:header).once do |name, header|
first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures
query = CGI.parse(URI.parse(first_link).query)
+ expect(name).to eq('Link')
expect(query.except('id_after')).to eq(CGI.parse(orig_uri.query).except('id_after'))
expect(query['id_after']).to eq(['42'])
end
@@ -90,14 +92,15 @@ RSpec.describe Gitlab::Pagination::Keyset::RequestContext do
context 'with descending order' do
let(:next_page) { double('next page', order_by: { id: :desc }, lower_bounds: { id: 42 }) }
- it 'sets Links header with a link to the next page' do
+ it 'sets Link header with a link to the next page' do
orig_uri = URI.parse(request_context.request.url)
- expect(request_context).to receive(:header).twice do |name, header|
+ expect(request_context).to receive(:header).once do |name, header|
first_link, _ = /<([^>]+)>; rel="next"/.match(header).captures
query = CGI.parse(URI.parse(first_link).query)
+ expect(name).to eq('Link')
expect(query.except('id_before')).to eq(CGI.parse(orig_uri.query).except('id_before'))
expect(query['id_before']).to eq(['42'])
end
diff --git a/spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb b/spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb
deleted file mode 100644
index ca74f7573f3..00000000000
--- a/spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Patch::ActionDispatchJourneyFormatter do
- let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- let(:url) { Gitlab::Routing.url_helpers.project_pipeline_url(project, pipeline) }
- let(:expected_path) { "#{project.full_path}/-/pipelines/#{pipeline.id}" }
-
- context 'custom implementation of #missing_keys' do
- before do
- expect_any_instance_of(Gitlab::Patch::ActionDispatchJourneyFormatter).to receive(:missing_keys)
- end
-
- it 'generates correct url' do
- expect(url).to end_with(expected_path)
- end
- end
-
- context 'original implementation of #missing_keys' do
- before do
- allow_any_instance_of(Gitlab::Patch::ActionDispatchJourneyFormatter).to receive(:missing_keys) do |instance, route, parts|
- instance.send(:old_missing_keys, route, parts) # test the old implementation
- end
- end
-
- it 'generates correct url' do
- expect(url).to end_with(expected_path)
- end
- end
-end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index cd89674af0f..54c866b4a6d 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe Gitlab::PathRegex do
let(:deprecated_routes) do
# profile was deprecated in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51646
- %w(profile)
+ %w(profile s)
end
let(:ee_top_level_words) do
@@ -183,7 +183,7 @@ RSpec.describe Gitlab::PathRegex do
# We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
it 'does not allow expansion' do
- expect(described_class::TOP_LEVEL_ROUTES.size).to eq(44)
+ expect(described_class::TOP_LEVEL_ROUTES.size).to eq(40)
end
end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 48e2a2e9794..5187c96b511 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -206,8 +206,12 @@ RSpec.describe Gitlab::Profiler do
end
end
- before do
- stub_const('STDOUT', stdout)
+ around do |example|
+ original_stdout = $stdout
+
+ $stdout = stdout # rubocop: disable RSpec/ExpectOutput
+ example.run
+ $stdout = original_stdout # rubocop: disable RSpec/ExpectOutput
end
it 'prints a profile result sorted by total time' do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index a76ad1f6f4c..2f28b8dfce0 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -549,30 +549,39 @@ RSpec.describe Gitlab::ProjectSearchResults do
describe 'user search' do
let(:query) { 'gob' }
- let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
+
+ let_it_be(:user_1) { create(:user, username: 'gob_bluth') }
+ let_it_be(:user_2) { create(:user, username: 'michael_bluth') }
+ let_it_be(:user_3) { create(:user, username: 'gob_2018') }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
subject(:objects) { results.objects('users') }
it 'returns the user belonging to the project matching the search query' do
- user1 = create(:user, username: 'gob_bluth')
- create(:project_member, :developer, user: user1, project: project)
+ create(:project_member, :developer, user: user_1, project: project)
+ create(:project_member, :developer, user: user_2, project: project)
- user2 = create(:user, username: 'michael_bluth')
- create(:project_member, :developer, user: user2, project: project)
+ expect(objects).to contain_exactly(user_1)
+ end
- create(:user, username: 'gob_2018')
+ it 'returns the user belonging to the group matching the search query' do
+ create(:group_member, :developer, user: user_1, group: group)
- expect(objects).to contain_exactly(user1)
+ expect(objects).to contain_exactly(user_1)
end
- it 'returns the user belonging to the group matching the search query' do
- user1 = create(:user, username: 'gob_bluth')
- create(:group_member, :developer, user: user1, group: group)
+ context 'when multiple projects provided' do
+ let_it_be(:project_2) { create(:project, namespace: group) }
+
+ subject(:results) { described_class.new(user, query, project: [project, project_2], repository_ref: repository_ref, filters: filters) }
- create(:user, username: 'gob_2018')
+ it 'returns users belonging to projects matching the search query' do
+ create(:project_member, :developer, user: user_1, project: project)
+ create(:project_member, :developer, user: user_3, project: project_2)
- expect(objects).to contain_exactly(user1)
+ expect(objects).to contain_exactly(user_1, user_3)
+ end
end
end
end
diff --git a/spec/lib/gitlab/prometheus/adapter_spec.rb b/spec/lib/gitlab/prometheus/adapter_spec.rb
index 259202178a2..1eaed65c805 100644
--- a/spec/lib/gitlab/prometheus/adapter_spec.rb
+++ b/spec/lib/gitlab/prometheus/adapter_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::Prometheus::Adapter do
end
context 'with cluster with prometheus available' do
- let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_integrations_prometheus, cluster: cluster) }
it 'returns prometheus service' do
expect(subject.prometheus_adapter).to eq(prometheus_service)
@@ -32,16 +32,12 @@ RSpec.describe Gitlab::Prometheus::Adapter do
context "prometheus service can't execute queries" do
let(:prometheus_service) { double(:prometheus_service, can_query?: false) }
- context 'with cluster with prometheus integration' do
- let!(:prometheus_integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
-
- it 'returns the integration' do
- expect(subject.prometheus_adapter).to eq(prometheus_integration)
- end
+ before do
+ allow(project).to receive(:find_or_initialize_service).with('prometheus').and_return prometheus_service
end
- context 'with cluster with prometheus not available' do
- let!(:prometheus) { create(:clusters_applications_prometheus, :installable, cluster: cluster) }
+ context 'with cluster with prometheus disabled' do
+ let!(:prometheus) { create(:clusters_integrations_prometheus, enabled: false, cluster: cluster) }
it 'returns nil' do
expect(subject.prometheus_adapter).to be_nil
@@ -49,19 +45,11 @@ RSpec.describe Gitlab::Prometheus::Adapter do
end
context 'with cluster with prometheus available' do
- let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_integrations_prometheus, cluster: cluster) }
it 'returns application handling all environments' do
expect(subject.prometheus_adapter).to eq(prometheus)
end
-
- context 'with cluster with prometheus integration' do
- let!(:prometheus_integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
-
- it 'returns the application' do
- expect(subject.prometheus_adapter).to eq(prometheus)
- end
- end
end
context 'with cluster without prometheus installed' do
diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb
index 5f73b84288d..31141ac1139 100644
--- a/spec/lib/gitlab/redis/cache_spec.rb
+++ b/spec/lib/gitlab/redis/cache_spec.rb
@@ -3,20 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::Cache do
- let(:config_file_name) { "config/redis.cache.yml" }
+ let(:instance_specific_config_file) { "config/redis.cache.yml" }
let(:environment_config_file_name) { "GITLAB_REDIS_CACHE_CONFIG_FILE" }
- let(:config_old_format_socket) { "spec/fixtures/config/redis_cache_old_format_socket.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_cache_new_format_socket.yml" }
- let(:old_socket_path) {"/path/to/old/redis.cache.sock" }
- let(:new_socket_path) {"/path/to/redis.cache.sock" }
- let(:config_old_format_host) { "spec/fixtures/config/redis_cache_old_format_host.yml" }
- let(:config_new_format_host) { "spec/fixtures/config/redis_cache_new_format_host.yml" }
- let(:redis_port) { 6380 }
- let(:redis_database) { 10 }
- let(:sentinel_port) { redis_port + 20000 }
- let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_cache_config_with_env.yml"}
- let(:config_env_variable_url) {"TEST_GITLAB_REDIS_CACHE_URL"}
- let(:class_redis_url) { Gitlab::Redis::Cache::DEFAULT_REDIS_CACHE_URL }
include_examples "redis_shared_examples"
+
+ 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:6380' )
+ end
+ end
end
diff --git a/spec/lib/gitlab/redis/queues_spec.rb b/spec/lib/gitlab/redis/queues_spec.rb
index 8a32c991943..2e396cde3bf 100644
--- a/spec/lib/gitlab/redis/queues_spec.rb
+++ b/spec/lib/gitlab/redis/queues_spec.rb
@@ -3,20 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::Queues do
- let(:config_file_name) { "config/redis.queues.yml" }
+ let(:instance_specific_config_file) { "config/redis.queues.yml" }
let(:environment_config_file_name) { "GITLAB_REDIS_QUEUES_CONFIG_FILE" }
- let(:config_old_format_socket) { "spec/fixtures/config/redis_queues_old_format_socket.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_queues_new_format_socket.yml" }
- let(:old_socket_path) {"/path/to/old/redis.queues.sock" }
- let(:new_socket_path) {"/path/to/redis.queues.sock" }
- let(:config_old_format_host) { "spec/fixtures/config/redis_queues_old_format_host.yml" }
- let(:config_new_format_host) { "spec/fixtures/config/redis_queues_new_format_host.yml" }
- let(:redis_port) { 6381 }
- let(:redis_database) { 11 }
- let(:sentinel_port) { redis_port + 20000 }
- let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_queues_config_with_env.yml"}
- let(:config_env_variable_url) {"TEST_GITLAB_REDIS_QUEUES_URL"}
- let(:class_redis_url) { Gitlab::Redis::Queues::DEFAULT_REDIS_QUEUES_URL }
include_examples "redis_shared_examples"
+
+ 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:6381' )
+ end
+ end
end
diff --git a/spec/lib/gitlab/redis/shared_state_spec.rb b/spec/lib/gitlab/redis/shared_state_spec.rb
index bd90e4c750d..d240abfbf5b 100644
--- a/spec/lib/gitlab/redis/shared_state_spec.rb
+++ b/spec/lib/gitlab/redis/shared_state_spec.rb
@@ -3,20 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::SharedState do
- let(:config_file_name) { "config/redis.shared_state.yml" }
+ let(:instance_specific_config_file) { "config/redis.shared_state.yml" }
let(:environment_config_file_name) { "GITLAB_REDIS_SHARED_STATE_CONFIG_FILE" }
- let(:config_old_format_socket) { "spec/fixtures/config/redis_shared_state_old_format_socket.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_shared_state_new_format_socket.yml" }
- let(:old_socket_path) {"/path/to/old/redis.shared_state.sock" }
- let(:new_socket_path) {"/path/to/redis.shared_state.sock" }
- let(:config_old_format_host) { "spec/fixtures/config/redis_shared_state_old_format_host.yml" }
- let(:config_new_format_host) { "spec/fixtures/config/redis_shared_state_new_format_host.yml" }
- let(:redis_port) { 6382 }
- let(:redis_database) { 12 }
- let(:sentinel_port) { redis_port + 20000 }
- let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_shared_state_config_with_env.yml"}
- let(:config_env_variable_url) {"TEST_GITLAB_REDIS_SHARED_STATE_URL"}
- let(:class_redis_url) { Gitlab::Redis::SharedState::DEFAULT_REDIS_SHARED_STATE_URL }
include_examples "redis_shared_examples"
+
+ 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
end
diff --git a/spec/lib/gitlab/redis/trace_chunks_spec.rb b/spec/lib/gitlab/redis/trace_chunks_spec.rb
new file mode 100644
index 00000000000..e974dc519d6
--- /dev/null
+++ b/spec/lib/gitlab/redis/trace_chunks_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Redis::TraceChunks do
+ let(:instance_specific_config_file) { "config/redis.trace_chunks.yml" }
+ let(:environment_config_file_name) { "GITLAB_REDIS_TRACE_CHUNKS_CONFIG_FILE" }
+ let(:shared_state_config_file) { nil }
+
+ before do
+ allow(Gitlab::Redis::SharedState).to receive(:config_file_name).and_return(shared_state_config_file)
+ end
+
+ include_examples "redis_shared_examples"
+
+ describe '.config_file_name' do
+ subject { described_class.config_file_name }
+
+ let(:rails_root) { Dir.mktmpdir('redis_shared_examples') }
+
+ before do
+ # 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)
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ end
+
+ after do
+ FileUtils.rm_rf(rails_root)
+ end
+
+ context 'when there is only a resque.yml' do
+ before do
+ FileUtils.touch(File.join(rails_root, 'config/resque.yml'))
+ end
+
+ it { expect(subject).to eq("#{rails_root}/config/resque.yml") }
+
+ 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 SharedState has a different config file' do
+ let(:shared_state_config_file) { 'shared state config file' }
+
+ it { expect(subject).to eq('shared state config file') }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis/wrapper_spec.rb b/spec/lib/gitlab/redis/wrapper_spec.rb
index ec233c022ee..dd1f0d8b414 100644
--- a/spec/lib/gitlab/redis/wrapper_spec.rb
+++ b/spec/lib/gitlab/redis/wrapper_spec.rb
@@ -3,47 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::Redis::Wrapper do
- let(:config_file_name) { "config/resque.yml" }
- let(:environment_config_file_name) { "GITLAB_REDIS_CONFIG_FILE" }
- let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
- let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
- let(:old_socket_path) {"/path/to/old/redis.sock" }
- let(:new_socket_path) {"/path/to/redis.sock" }
- let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
- let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
- let(:redis_port) { 6379 }
- let(:redis_database) { 99 }
- let(:sentinel_port) { redis_port + 20000 }
- let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml"}
- let(:config_env_variable_url) {"TEST_GITLAB_REDIS_URL"}
- let(:class_redis_url) { Gitlab::Redis::Wrapper::DEFAULT_REDIS_URL }
-
- include_examples "redis_shared_examples" do
- before do
- allow(described_class).to receive(:instrumentation_class) do
- ::Gitlab::Instrumentation::Redis::Cache
- end
- end
- end
-
- describe '.version' do
- it 'returns a version' do
- expect(described_class.version).to be_present
- end
- end
-
describe '.instrumentation_class' do
- it 'raises a NotImplementedError' do
- expect(described_class).to receive(:instrumentation_class).and_call_original
-
- expect { described_class.instrumentation_class }.to raise_error(NotImplementedError)
- end
- end
-
- describe '.config_file_path' do
- it 'returns the absolute path to the configuration file' do
- expect(described_class.config_file_path('foo.yml'))
- .to eq Rails.root.join('config', 'foo.yml').to_s
+ it 'raises a NameError' do
+ expect { described_class.instrumentation_class }.to raise_error(NameError)
end
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 28447d5c2a9..c1c97e87a4c 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -675,9 +675,20 @@ RSpec.describe Gitlab::Regex do
describe '.helm_version_regex' do
subject { described_class.helm_version_regex }
+ it { is_expected.to match('1.2.3') }
+ it { is_expected.to match('1.2.3-beta') }
+ it { is_expected.to match('1.2.3-alpha.3') }
+
it { is_expected.to match('v1.2.3') }
it { is_expected.to match('v1.2.3-beta') }
it { is_expected.to match('v1.2.3-alpha.3') }
+
+ it { is_expected.not_to match('1') }
+ it { is_expected.not_to match('1.2') }
+ it { is_expected.not_to match('1./2.3') }
+ it { is_expected.not_to match('../../../../../1.2.3') }
+ it { is_expected.not_to match('%2e%2e%2f1.2.3') }
+
it { is_expected.not_to match('v1') }
it { is_expected.not_to match('v1.2') }
it { is_expected.not_to match('v1./2.3') }
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
index 881591ae805..9aeb9f11bac 100644
--- a/spec/lib/gitlab/repository_set_cache_spec.rb
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
let(:repository) { project.repository }
let(:namespace) { "#{repository.full_path}:#{project.id}" }
+ let(:gitlab_cache_namespace) { Gitlab::Redis::Cache::CACHE_NAMESPACE }
let(:cache) { described_class.new(repository) }
describe '#cache_key' do
@@ -52,6 +53,24 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
end
end
+ describe '#write' do
+ subject(:write_cache) { cache.write('branch_names', ['main']) }
+
+ it 'writes the value to the cache' do
+ write_cache
+
+ redis_keys = Gitlab::Redis::Cache.with { |redis| redis.scan(0, match: "*") }.last
+ expect(redis_keys).to include("branch_names:#{namespace}:set")
+ expect(cache.fetch('branch_names')).to contain_exactly('main')
+ end
+
+ it 'sets the expiry of the set' do
+ write_cache
+
+ expect(cache.ttl('branch_names')).to be_within(1).of(cache.expires_in.seconds)
+ end
+ end
+
describe '#expire' do
subject { cache.expire(*keys) }
@@ -75,6 +94,12 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
expect(cache.read(:foo)).to be_empty
end
+
+ it 'expires the new key format' do
+ expect_any_instance_of(Redis).to receive(:unlink).with(cache.cache_key(:foo), cache.new_cache_key(:foo)) # rubocop:disable RSpec/AnyInstanceOf
+
+ subject
+ end
end
context 'multiple keys' do
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 0fcb7db7d5f..f51c5dd3d20 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -99,25 +99,6 @@ RSpec.describe Gitlab::Runtime do
end
end
- context "unicorn" do
- before do
- stub_const('::Unicorn', Module.new)
- stub_const('::Unicorn::HttpServer', Class.new)
- stub_env('ACTION_CABLE_IN_APP', 'false')
- end
-
- it_behaves_like "valid runtime", :unicorn, 1
-
- context "when ActionCable in-app mode is enabled" do
- before do
- stub_env('ACTION_CABLE_IN_APP', 'true')
- stub_env('ACTION_CABLE_WORKER_POOL_SIZE', '3')
- end
-
- it_behaves_like "valid runtime", :unicorn, 4
- end
- end
-
context "sidekiq" do
let(:sidekiq_type) { double('::Sidekiq') }
diff --git a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
index 43cbe71dd6b..5347680b253 100644
--- a/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
+++ b/spec/lib/gitlab/sidekiq_cluster/cli_spec.rb
@@ -108,114 +108,101 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do
end
end
- # Remove with https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/646
- context 'with --queue-selector and --experimental-queue-selector' do
- it 'errors' do
- expect(Gitlab::SidekiqCluster).not_to receive(:start)
-
- expect { cli.run(%w(--queue-selector name=foo --experimental-queue-selector name=bar)) }
- .to raise_error(described_class::CommandError)
- end
- end
-
- # Simplify with https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/646
- ['--queue-selector', '--experimental-queue-selector'].each do |flag|
- context "with #{flag}" do
- where do
- {
- 'memory-bound queues' => {
- query: 'resource_boundary=memory',
- included_queues: %w(project_export),
- excluded_queues: %w(merge)
- },
- 'memory- or CPU-bound queues' => {
- query: 'resource_boundary=memory,cpu',
- included_queues: %w(auto_merge:auto_merge_process project_export),
- excluded_queues: %w(merge)
- },
- 'high urgency CI queues' => {
- query: 'feature_category=continuous_integration&urgency=high',
- included_queues: %w(pipeline_cache:expire_job_cache pipeline_cache:expire_pipeline_cache),
- excluded_queues: %w(merge)
- },
- 'CPU-bound high urgency CI queues' => {
- query: 'feature_category=continuous_integration&urgency=high&resource_boundary=cpu',
- included_queues: %w(pipeline_cache:expire_pipeline_cache),
- excluded_queues: %w(pipeline_cache:expire_job_cache merge)
- },
- 'CPU-bound high urgency non-CI queues' => {
- query: 'feature_category!=continuous_integration&urgency=high&resource_boundary=cpu',
- included_queues: %w(new_issue),
- excluded_queues: %w(pipeline_cache:expire_pipeline_cache)
- },
- 'CI and SCM queues' => {
- query: 'feature_category=continuous_integration|feature_category=source_code_management',
- included_queues: %w(pipeline_cache:expire_job_cache merge),
- excluded_queues: %w(mailers)
- }
+ context "with --queue-selector" do
+ where do
+ {
+ 'memory-bound queues' => {
+ query: 'resource_boundary=memory',
+ included_queues: %w(project_export),
+ excluded_queues: %w(merge)
+ },
+ 'memory- or CPU-bound queues' => {
+ query: 'resource_boundary=memory,cpu',
+ included_queues: %w(auto_merge:auto_merge_process project_export),
+ excluded_queues: %w(merge)
+ },
+ 'high urgency CI queues' => {
+ query: 'feature_category=continuous_integration&urgency=high',
+ included_queues: %w(pipeline_cache:expire_job_cache pipeline_cache:expire_pipeline_cache),
+ excluded_queues: %w(merge)
+ },
+ 'CPU-bound high urgency CI queues' => {
+ query: 'feature_category=continuous_integration&urgency=high&resource_boundary=cpu',
+ included_queues: %w(pipeline_cache:expire_pipeline_cache),
+ excluded_queues: %w(pipeline_cache:expire_job_cache merge)
+ },
+ 'CPU-bound high urgency non-CI queues' => {
+ query: 'feature_category!=continuous_integration&urgency=high&resource_boundary=cpu',
+ included_queues: %w(new_issue),
+ excluded_queues: %w(pipeline_cache:expire_pipeline_cache)
+ },
+ 'CI and SCM queues' => {
+ query: 'feature_category=continuous_integration|feature_category=source_code_management',
+ included_queues: %w(pipeline_cache:expire_job_cache merge),
+ excluded_queues: %w(mailers)
}
- end
-
- with_them do
- it 'expands queues by attributes' do
- expect(Gitlab::SidekiqCluster).to receive(:start) do |queues, opts|
- expect(opts).to eq(default_options)
- expect(queues.first).to include(*included_queues)
- expect(queues.first).not_to include(*excluded_queues)
+ }
+ end
- []
- end
+ with_them do
+ it 'expands queues by attributes' do
+ expect(Gitlab::SidekiqCluster).to receive(:start) do |queues, opts|
+ expect(opts).to eq(default_options)
+ expect(queues.first).to include(*included_queues)
+ expect(queues.first).not_to include(*excluded_queues)
- cli.run(%W(#{flag} #{query}))
+ []
end
- it 'works when negated' do
- expect(Gitlab::SidekiqCluster).to receive(:start) do |queues, opts|
- expect(opts).to eq(default_options)
- expect(queues.first).not_to include(*included_queues)
- expect(queues.first).to include(*excluded_queues)
+ cli.run(%W(--queue-selector #{query}))
+ end
- []
- end
+ it 'works when negated' do
+ expect(Gitlab::SidekiqCluster).to receive(:start) do |queues, opts|
+ expect(opts).to eq(default_options)
+ expect(queues.first).not_to include(*included_queues)
+ expect(queues.first).to include(*excluded_queues)
- cli.run(%W(--negate #{flag} #{query}))
+ []
end
+
+ cli.run(%W(--negate --queue-selector #{query}))
end
+ end
- it 'expands multiple queue groups correctly' do
- expect(Gitlab::SidekiqCluster)
- .to receive(:start)
- .with([['chat_notification'], ['project_export']], default_options)
- .and_return([])
+ it 'expands multiple queue groups correctly' do
+ expect(Gitlab::SidekiqCluster)
+ .to receive(:start)
+ .with([['chat_notification'], ['project_export']], default_options)
+ .and_return([])
- cli.run(%W(#{flag} feature_category=chatops&has_external_dependencies=true resource_boundary=memory&feature_category=importers))
- end
+ cli.run(%w(--queue-selector feature_category=chatops&has_external_dependencies=true resource_boundary=memory&feature_category=importers))
+ end
- it 'allows the special * selector' do
- worker_queues = %w(foo bar baz)
+ it 'allows the special * selector' do
+ worker_queues = %w(foo bar baz)
- expect(Gitlab::SidekiqConfig::CliMethods)
- .to receive(:worker_queues).and_return(worker_queues)
+ expect(Gitlab::SidekiqConfig::CliMethods)
+ .to receive(:worker_queues).and_return(worker_queues)
- expect(Gitlab::SidekiqCluster)
- .to receive(:start).with([worker_queues], default_options)
+ expect(Gitlab::SidekiqCluster)
+ .to receive(:start).with([worker_queues], default_options)
- cli.run(%W(#{flag} *))
- end
+ cli.run(%w(--queue-selector *))
+ end
- it 'errors when the selector matches no queues' do
- expect(Gitlab::SidekiqCluster).not_to receive(:start)
+ it 'errors when the selector matches no queues' do
+ expect(Gitlab::SidekiqCluster).not_to receive(:start)
- expect { cli.run(%W(#{flag} has_external_dependencies=true&has_external_dependencies=false)) }
- .to raise_error(described_class::CommandError)
- end
+ expect { cli.run(%w(--queue-selector has_external_dependencies=true&has_external_dependencies=false)) }
+ .to raise_error(described_class::CommandError)
+ end
- it 'errors on an invalid query multiple queue groups correctly' do
- expect(Gitlab::SidekiqCluster).not_to receive(:start)
+ it 'errors on an invalid query multiple queue groups correctly' do
+ expect(Gitlab::SidekiqCluster).not_to receive(:start)
- expect { cli.run(%W(#{flag} unknown_field=chatops)) }
- .to raise_error(Gitlab::SidekiqConfig::WorkerMatcher::QueryError)
- end
+ expect { cli.run(%w(--queue-selector unknown_field=chatops)) }
+ .to raise_error(Gitlab::SidekiqConfig::WorkerMatcher::QueryError)
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 731c509e221..dfdc1420eac 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -228,6 +228,8 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
end
context 'when the job performs database queries' do
+ include_context 'clear DB Load Balancing configuration'
+
before do
allow(Time).to receive(:now).and_return(timestamp)
allow(Process).to receive(:clock_gettime).and_call_original
@@ -248,28 +250,112 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
)
end
- it 'logs the database time' do
- expect(logger).to receive(:info).with(expected_start_payload).ordered
- expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered
+ shared_examples 'performs database queries' do
+ it 'logs the database time', :aggregate_errors do
+ expect(logger).to receive(:info).with(expected_start_payload).ordered
+ expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered
- call_subject(job, 'test_queue') do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ call_subject(job, 'test_queue') do
+ ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ end
+ end
+
+ it 'prevents database time from leaking to the next job', :aggregate_errors do
+ expect(logger).to receive(:info).with(expected_start_payload).ordered
+ expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered
+ expect(logger).to receive(:info).with(expected_start_payload).ordered
+ expect(logger).to receive(:info).with(expected_end_payload).ordered
+
+ call_subject(job.dup, 'test_queue') do
+ ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ end
+
+ Gitlab::SafeRequestStore.clear!
+
+ call_subject(job.dup, 'test_queue') { }
end
end
- it 'prevents database time from leaking to the next job' do
- expect(logger).to receive(:info).with(expected_start_payload).ordered
- expect(logger).to receive(:info).with(expected_end_payload_with_db).ordered
- expect(logger).to receive(:info).with(expected_start_payload).ordered
- expect(logger).to receive(:info).with(expected_end_payload).ordered
+ context 'when load balancing is disabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+ end
+
+ let(:expected_end_payload_with_db) do
+ expected_end_payload.merge(
+ 'db_duration_s' => a_value >= 0.1,
+ 'db_count' => a_value >= 1,
+ 'db_cached_count' => 0,
+ 'db_write_count' => 0
+ )
+ end
+
+ include_examples 'performs database queries'
+ end
+
+ context 'when load balancing is enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ end
- call_subject(job.dup, 'test_queue') do
- ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);')
+ let(:expected_end_payload_with_db) do
+ expected_end_payload.merge(
+ 'db_duration_s' => a_value >= 0.1,
+ 'db_count' => a_value >= 1,
+ 'db_cached_count' => 0,
+ 'db_write_count' => 0,
+ 'db_replica_count' => 0,
+ 'db_replica_cached_count' => 0,
+ 'db_replica_wal_count' => 0,
+ 'db_replica_duration_s' => a_value >= 0,
+ 'db_primary_count' => a_value >= 1,
+ 'db_primary_cached_count' => 0,
+ 'db_primary_wal_count' => 0,
+ 'db_primary_duration_s' => a_value > 0
+ )
end
- Gitlab::SafeRequestStore.clear!
+ let(:end_payload) do
+ start_payload.merge(
+ 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
+ 'job_status' => 'done',
+ 'duration_s' => 0.0,
+ 'completed_at' => timestamp.to_f,
+ 'cpu_s' => 1.111112,
+ 'db_duration_s' => 0.0,
+ 'db_cached_count' => 0,
+ 'db_count' => 0,
+ 'db_write_count' => 0,
+ 'db_replica_count' => 0,
+ 'db_replica_cached_count' => 0,
+ 'db_replica_wal_count' => 0,
+ 'db_replica_duration_s' => 0,
+ 'db_primary_count' => 0,
+ 'db_primary_cached_count' => 0,
+ 'db_primary_wal_count' => 0,
+ 'db_primary_duration_s' => 0
+ )
+ end
+
+ include_examples 'performs database queries'
+ end
+ end
+
+ context 'when the job uses load balancing capabilities' do
+ let(:expected_payload) { { 'database_chosen' => 'retry' } }
+
+ before do
+ allow(Time).to receive(:now).and_return(timestamp)
+ allow(Process).to receive(:clock_gettime).and_call_original
+ end
+
+ it 'logs the database chosen' do
+ expect(logger).to receive(:info).with(start_payload).ordered
+ expect(logger).to receive(:info).with(include(expected_payload)).ordered
- call_subject(job.dup, 'test_queue') { }
+ call_subject(job, 'test_queue') do
+ job[:database_chosen] = 'retry'
+ end
end
end
@@ -303,6 +389,39 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
expect { subject.call(job.dup, 'test_queue') {} }.not_to raise_error
end
end
+
+ context 'when the job payload is compressed' do
+ let(:compressed_args) { "eJyLVspIzcnJV4oFAA88AxE=" }
+ let(:expected_start_payload) do
+ start_payload.merge(
+ 'args' => ['[COMPRESSED]'],
+ 'job_size_bytes' => Sidekiq.dump_json([compressed_args]).bytesize,
+ 'compressed' => true
+ )
+ end
+
+ let(:expected_end_payload) do
+ end_payload.merge(
+ 'args' => ['[COMPRESSED]'],
+ 'job_size_bytes' => Sidekiq.dump_json([compressed_args]).bytesize,
+ 'compressed' => true
+ )
+ end
+
+ it 'logs it in the done log' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(expected_start_payload).ordered
+ expect(logger).to receive(:info).with(expected_end_payload).ordered
+
+ job['args'] = [compressed_args]
+ job['compressed'] = true
+
+ call_subject(job, 'test_queue') do
+ ::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor.decompress(job)
+ end
+ end
+ end
+ end
end
describe '#add_time_keys!' do
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 0285467ecab..a10a8883591 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
@@ -18,14 +18,43 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
describe '#schedule' do
- it 'calls schedule on the strategy' do
- expect do |block|
- expect_next_instance_of(Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::UntilExecuting) do |strategy|
- expect(strategy).to receive(:schedule).with(job, &block)
+ shared_examples 'scheduling with deduplication class' do |strategy_class|
+ it 'calls schedule on the strategy' do
+ expect do |block|
+ expect_next_instance_of("Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies::#{strategy_class}".constantize) do |strategy|
+ expect(strategy).to receive(:schedule).with(job, &block)
+ end
+
+ duplicate_job.schedule(&block)
+ end.to yield_control
+ end
+ end
+
+ it_behaves_like 'scheduling with deduplication class', 'UntilExecuting'
+
+ context 'when the deduplication depends on a FF' do
+ before do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
+
+ allow(AuthorizedProjectsWorker).to receive(:get_deduplication_options).and_return(feature_flag: :my_feature_flag)
+ end
+
+ context 'when the feature flag is enabled' do
+ before do
+ stub_feature_flags(my_feature_flag: true)
end
- duplicate_job.schedule(&block)
- end.to yield_control
+ it_behaves_like 'scheduling with deduplication class', 'UntilExecuting'
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(my_feature_flag: false)
+ end
+
+ it_behaves_like 'scheduling with deduplication class', 'None'
+ end
end
end
@@ -51,6 +80,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
.from([nil, -2])
.to(['123', be_within(1).of(described_class::DUPLICATE_KEY_TTL)])
end
+
+ it "adds the idempotency key to the jobs payload" do
+ expect { duplicate_job.check! }.to change { job['idempotency_key'] }.from(nil).to(idempotency_key)
+ end
end
context 'when there was already a job with same arguments in the same queue' do
@@ -81,14 +114,39 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'when the key exists in redis' do
before do
- set_idempotency_key(idempotency_key, 'existing-key')
+ set_idempotency_key(idempotency_key, 'existing-jid')
end
- it 'removes the key from redis' do
- expect { duplicate_job.delete! }
- .to change { read_idempotency_key_with_ttl(idempotency_key) }
- .from(['existing-key', -1])
- .to([nil, -2])
+ shared_examples 'deleting the duplicate job' do
+ it 'removes the key from redis' do
+ expect { duplicate_job.delete! }
+ .to change { read_idempotency_key_with_ttl(idempotency_key) }
+ .from(['existing-jid', -1])
+ .to([nil, -2])
+ end
+ end
+
+ context 'when the idempotency key is not part of the job' do
+ it_behaves_like 'deleting the duplicate job'
+
+ it 'recalculates the idempotency hash' do
+ expect(duplicate_job).to receive(:idempotency_hash).and_call_original
+
+ duplicate_job.delete!
+ end
+ end
+
+ context 'when the idempotency key is part of the job' do
+ let(:idempotency_key) { 'not the same as what we calculate' }
+ let(:job) { super().merge('idempotency_key' => idempotency_key) }
+
+ it_behaves_like 'deleting the duplicate job'
+
+ it 'does not recalculate the idempotency hash' do
+ expect(duplicate_job).not_to receive(:idempotency_hash)
+
+ duplicate_job.delete!
+ end
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
index eb9ba50cdcd..8cf65e1be5b 100644
--- a/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/instrumentation_logger_spec.rb
@@ -24,58 +24,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::InstrumentationLogger do
stub_const('TestWorker', worker)
end
- describe '.keys' do
- it 'returns all available payload keys' do
- expected_keys = [
- :cpu_s,
- :gitaly_calls,
- :gitaly_duration_s,
- :rugged_calls,
- :rugged_duration_s,
- :elasticsearch_calls,
- :elasticsearch_duration_s,
- :elasticsearch_timed_out_count,
- :mem_objects,
- :mem_bytes,
- :mem_mallocs,
- :redis_calls,
- :redis_duration_s,
- :redis_read_bytes,
- :redis_write_bytes,
- :redis_action_cable_calls,
- :redis_action_cable_duration_s,
- :redis_action_cable_read_bytes,
- :redis_action_cable_write_bytes,
- :redis_cache_calls,
- :redis_cache_duration_s,
- :redis_cache_read_bytes,
- :redis_cache_write_bytes,
- :redis_queues_calls,
- :redis_queues_duration_s,
- :redis_queues_read_bytes,
- :redis_queues_write_bytes,
- :redis_shared_state_calls,
- :redis_shared_state_duration_s,
- :redis_shared_state_read_bytes,
- :redis_shared_state_write_bytes,
- :db_count,
- :db_write_count,
- :db_cached_count,
- :external_http_count,
- :external_http_duration_s,
- :rack_attack_redis_count,
- :rack_attack_redis_duration_s
- ]
-
- expect(described_class.keys).to include(*expected_keys)
- end
- end
-
describe '#call', :request_store do
let(:instrumentation_values) do
{
cpu_s: 10,
- unknown_attribute: 123,
db_count: 0,
db_cached_count: 0,
db_write_count: 0,
@@ -90,12 +42,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::InstrumentationLogger do
end
end
- it 'merges correct instrumentation data in the job' do
+ it 'merges all instrumentation data in the job' do
expect { |b| subject.call(worker, job, queue, &b) }.to yield_control
- expected_values = instrumentation_values.except(:unknown_attribute)
-
- expect(job[:instrumentation]).to eq(expected_values)
+ expect(job[:instrumentation]).to eq(instrumentation_values)
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 95be76ce351..34b4541f339 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -107,5 +107,110 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
let(:job_status) { :done }
let(:labels_with_job_status) { labels.merge(job_status: job_status.to_s) }
end
+
+ context 'DB load balancing' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new }
+
+ let(:queue) { :test }
+ let(:worker_class) { worker.class }
+ let(:job) { {} }
+ let(:job_status) { :done }
+ let(:labels_with_job_status) { default_labels.merge(job_status: job_status.to_s) }
+ let(:default_labels) do
+ { queue: queue.to_s,
+ worker: worker_class.to_s,
+ boundary: "",
+ external_dependencies: "no",
+ feature_category: "",
+ urgency: "low" }
+ end
+
+ before do
+ stub_const('TestWorker', Class.new)
+ TestWorker.class_eval do
+ include Sidekiq::Worker
+ include WorkerAttributes
+ end
+ end
+
+ let(:worker) { TestWorker.new }
+
+ include_context 'server metrics with mocked prometheus'
+
+ context 'when load_balancing is enabled' do
+ let(:load_balancing_metric) { double('load balancing metric') }
+
+ include_context 'clear DB Load Balancing configuration'
+
+ before do
+ allow(::Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric)
+ end
+
+ describe '#initialize' do
+ it 'sets load_balancing metrics' do
+ expect(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric)
+
+ subject
+ end
+ end
+
+ describe '#call' do
+ include_context 'server metrics call'
+
+ context 'when :database_chosen is provided' do
+ where(:database_chosen) do
+ %w[primary retry replica]
+ end
+
+ with_them do
+ context "when #{params[:database_chosen]} is used" do
+ let(:labels_with_load_balancing) do
+ labels_with_job_status.merge(database_chosen: database_chosen, data_consistency: 'delayed')
+ end
+
+ before do
+ job[:database_chosen] = database_chosen
+ job[:data_consistency] = 'delayed'
+ allow(load_balancing_metric).to receive(:increment)
+ end
+
+ it 'increment sidekiq_load_balancing_count' do
+ expect(load_balancing_metric).to receive(:increment).with(labels_with_load_balancing, 1)
+
+ described_class.new.call(worker, job, :test) { nil }
+ end
+ end
+ end
+ end
+
+ context 'when :database_chosen is not provided' do
+ it 'does not increment sidekiq_load_balancing_count' do
+ expect(load_balancing_metric).not_to receive(:increment)
+
+ described_class.new.call(worker, job, :test) { nil }
+ end
+ end
+ end
+ end
+
+ context 'when load_balancing is disabled' do
+ include_context 'clear DB Load Balancing configuration'
+
+ before do
+ allow(::Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(false)
+ end
+
+ describe '#initialize' do
+ it 'doesnt set load_balancing metrics' do
+ expect(Gitlab::Metrics).not_to receive(:counter).with(:sidekiq_load_balancing_count, anything)
+
+ subject
+ end
+ end
+ end
+ end
end
# rubocop: enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/lib/gitlab/sidekiq_middleware/size_limiter/compressor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/size_limiter/compressor_spec.rb
new file mode 100644
index 00000000000..b9b58683459
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/size_limiter/compressor_spec.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Compressor do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:base_payload) do
+ {
+ "class" => "ARandomWorker",
+ "queue" => "a_worker",
+ "retry" => true,
+ "jid" => "d774900367dc8b2962b2479c",
+ "created_at" => 1234567890,
+ "enqueued_at" => 1234567890
+ }
+ end
+
+ describe '.compressed?' do
+ where(:job, :result) do
+ {} | false
+ base_payload.merge("args" => [123, 'hello', ['world']]) | false
+ base_payload.merge("args" => ['eJzLSM3JyQcABiwCFQ=='], 'compressed' => true) | true
+ end
+
+ with_them do
+ it 'returns whether the job payload is compressed' do
+ expect(described_class.compressed?(job)).to eql(result)
+ end
+ end
+ end
+
+ describe '.compress' do
+ where(:args) do
+ [
+ nil,
+ [],
+ ['hello'],
+ [
+ {
+ "job_class" => "SomeWorker",
+ "job_id" => "b4a577edbccf1d805744efa9",
+ "provider_job_id" => nil,
+ "queue_name" => "default",
+ "arguments" => ["some", ["argument"]],
+ "executions" => 0,
+ "locale" => "en",
+ "attempt_number" => 1
+ },
+ nil,
+ 'hello',
+ 12345678901234567890,
+ ['nice']
+ ],
+ [
+ '2021-05-13_09:59:37.57483 rails-background-jobs : {"severity":"ERROR","time":"2021-05-13T09:59:37.574Z"',
+ 'bonne journée - ขอให้มีความสุขในวันนี้ - một ngày mới tốt lành - 좋은 하루 되세요 - ごきげんよう',
+ '🤝 - 🦊'
+ ]
+ ]
+ end
+
+ with_them do
+ let(:payload) { base_payload.merge("args" => args) }
+
+ it 'injects compressed data' do
+ serialized_args = Sidekiq.dump_json(args)
+ described_class.compress(payload, serialized_args)
+
+ expect(payload['args'].length).to be(1)
+ expect(payload['args'].first).to be_a(String)
+ expect(payload['compressed']).to be(true)
+ expect(payload['original_job_size_bytes']).to eql(serialized_args.bytesize)
+ expect do
+ Sidekiq.dump_json(payload)
+ end.not_to raise_error
+ end
+
+ it 'can decompress the payload' do
+ original_payload = payload.deep_dup
+
+ described_class.compress(payload, Sidekiq.dump_json(args))
+ described_class.decompress(payload)
+
+ expect(payload).to eql(original_payload)
+ end
+ end
+ end
+
+ describe '.decompress' do
+ context 'job payload is not compressed' do
+ let(:payload) { base_payload.merge("args" => ['hello']) }
+
+ it 'preserves the payload after decompression' do
+ original_payload = payload.deep_dup
+
+ described_class.decompress(payload)
+
+ expect(payload).to eql(original_payload)
+ end
+ end
+
+ context 'job payload is compressed with a default level' do
+ let(:payload) do
+ base_payload.merge(
+ 'args' => ['eF6LVspIzcnJV9JRKs8vyklRigUAMq0FqQ=='],
+ 'compressed' => true
+ )
+ end
+
+ it 'decompresses and clean up the job payload' do
+ described_class.decompress(payload)
+
+ expect(payload['args']).to eql(%w[hello world])
+ expect(payload).not_to have_key('compressed')
+ end
+ end
+
+ context 'job payload is compressed with a different level' do
+ let(:payload) do
+ base_payload.merge(
+ 'args' => [Base64.strict_encode64(Zlib::Deflate.deflate(Sidekiq.dump_json(%w[hello world]), 9))],
+ 'compressed' => true
+ )
+ end
+
+ it 'decompresses and clean up the job payload' do
+ described_class.decompress(payload)
+
+ expect(payload['args']).to eql(%w[hello world])
+ expect(payload).not_to have_key('compressed')
+ end
+ end
+
+ context 'job payload argument list is malformed' do
+ let(:payload) do
+ base_payload.merge(
+ 'args' => ['eNqLVspIzcnJV9JRKs8vyklRigUAMq0FqQ==', 'something else'],
+ 'compressed' => true
+ )
+ end
+
+ it 'tracks the conflicting exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_exception).with(
+ be_a(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor::PayloadDecompressionConflictError)
+ )
+
+ described_class.decompress(payload)
+
+ expect(payload['args']).to eql(%w[hello world])
+ expect(payload).not_to have_key('compressed')
+ end
+ end
+
+ context 'job payload is not a valid base64 string' do
+ let(:payload) do
+ base_payload.merge(
+ 'args' => ['hello123'],
+ 'compressed' => true
+ )
+ end
+
+ it 'raises an exception' do
+ expect do
+ described_class.decompress(payload)
+ end.to raise_error(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor::PayloadDecompressionError)
+ end
+ end
+
+ context 'job payload compression does not contain a valid Gzip header' do
+ let(:payload) do
+ base_payload.merge(
+ 'args' => ['aGVsbG8='],
+ 'compressed' => true
+ )
+ end
+
+ it 'raises an exception' do
+ expect do
+ described_class.decompress(payload)
+ end.to raise_error(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor::PayloadDecompressionError)
+ end
+ end
+
+ context 'job payload compression does not contain a valid Gzip body' do
+ let(:payload) do
+ base_payload.merge(
+ 'args' => ["eNqLVspIzcnJVw=="],
+ 'compressed' => true
+ )
+ end
+
+ it 'raises an exception' do
+ expect do
+ described_class.decompress(payload)
+ end.to raise_error(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor::PayloadDecompressionError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb b/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb
new file mode 100644
index 00000000000..91b8ef97ab4
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/size_limiter/server_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# rubocop: disable RSpec/MultipleMemoizedHelpers
+RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Server, :clean_gitlab_redis_queues do
+ subject(:middleware) { described_class.new }
+
+ let(:worker) { Class.new }
+ let(:job) do
+ {
+ "class" => "ARandomWorker",
+ "queue" => "a_worker",
+ "args" => %w[Hello World],
+ "created_at" => 1234567890,
+ "enqueued_at" => 1234567890
+ }
+ end
+
+ before do
+ allow(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:compress)
+ end
+
+ it 'yields block' do
+ expect { |b| subject.call(worker, job, :test, &b) }.to yield_control.once
+ end
+
+ it 'calls the Compressor' do
+ expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:decompress).with(job)
+
+ subject.call(worker, job, :test) {}
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb b/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb
index 3140686c908..4fbe59c3c27 100644
--- a/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/size_limiter/validator_spec.rb
@@ -3,6 +3,21 @@
require 'spec_helper'
RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
+ let(:base_payload) do
+ {
+ "class" => "ARandomWorker",
+ "queue" => "a_worker",
+ "retry" => true,
+ "jid" => "d774900367dc8b2962b2479c",
+ "created_at" => 1234567890,
+ "enqueued_at" => 1234567890
+ }
+ end
+
+ def job_payload(args = {})
+ base_payload.merge('args' => args)
+ end
+
let(:worker_class) do
Class.new do
def self.name
@@ -24,8 +39,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'does not log a warning message' do
expect(::Sidekiq.logger).not_to receive(:warn)
- described_class.new(TestSizeLimiterWorker, {}, mode: 'track')
- described_class.new(TestSizeLimiterWorker, {}, mode: 'raise')
+ described_class.new(TestSizeLimiterWorker, job_payload, mode: 'track')
+ described_class.new(TestSizeLimiterWorker, job_payload, mode: 'compress')
end
end
@@ -33,7 +48,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'defaults to track mode and logs a warning message' do
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter mode: invalid. Fallback to track mode.')
- validator = described_class.new(TestSizeLimiterWorker, {}, mode: 'invalid')
+ validator = described_class.new(TestSizeLimiterWorker, job_payload, mode: 'invalid')
expect(validator.mode).to eql('track')
end
@@ -43,7 +58,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'defaults to track mode' do
expect(::Sidekiq.logger).not_to receive(:warn)
- validator = described_class.new(TestSizeLimiterWorker, {})
+ validator = described_class.new(TestSizeLimiterWorker, job_payload)
expect(validator.mode).to eql('track')
end
@@ -53,8 +68,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'does not log a warning message' do
expect(::Sidekiq.logger).not_to receive(:warn)
- described_class.new(TestSizeLimiterWorker, {}, size_limit: 300)
- described_class.new(TestSizeLimiterWorker, {}, size_limit: 0)
+ described_class.new(TestSizeLimiterWorker, job_payload, size_limit: 300)
+ described_class.new(TestSizeLimiterWorker, job_payload, size_limit: 0)
end
end
@@ -62,7 +77,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'defaults to 0 and logs a warning message' do
expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter limit: -1')
- described_class.new(TestSizeLimiterWorker, {}, size_limit: -1)
+ described_class.new(TestSizeLimiterWorker, job_payload, size_limit: -1)
end
end
@@ -70,15 +85,63 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'defaults to 0' do
expect(::Sidekiq.logger).not_to receive(:warn)
- validator = described_class.new(TestSizeLimiterWorker, {})
+ validator = described_class.new(TestSizeLimiterWorker, job_payload)
expect(validator.size_limit).to be(0)
end
end
+
+ context 'when the compression threshold is valid' do
+ it 'does not log a warning message' do
+ expect(::Sidekiq.logger).not_to receive(:warn)
+
+ described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 300)
+ described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 1)
+ end
+ end
+
+ context 'when the compression threshold is negative' do
+ it 'logs a warning message' do
+ expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter compression threshold: -1')
+
+ described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: -1)
+ end
+
+ it 'falls back to the default' do
+ validator = described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: -1)
+
+ expect(validator.compression_threshold).to be(100_000)
+ end
+ end
+
+ context 'when the compression threshold is zero' do
+ it 'logs a warning message' do
+ expect(::Sidekiq.logger).to receive(:warn).with('Invalid Sidekiq size limiter compression threshold: 0')
+
+ described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 0)
+ end
+
+ it 'falls back to the default' do
+ validator = described_class.new(TestSizeLimiterWorker, job_payload, compression_threshold: 0)
+
+ expect(validator.compression_threshold).to be(100_000)
+ end
+ end
+
+ context 'when the compression threshold is empty' do
+ it 'defaults to 100_000' do
+ expect(::Sidekiq.logger).not_to receive(:warn)
+
+ validator = described_class.new(TestSizeLimiterWorker, job_payload)
+
+ expect(validator.compression_threshold).to be(100_000)
+ end
+ end
end
shared_examples 'validate limit job payload size' do
context 'in track mode' do
+ let(:compression_threshold) { nil }
let(:mode) { 'track' }
context 'when size limit negative' do
@@ -87,11 +150,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'does not track jobs' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
- validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
end
it 'does not raise exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
+ expect { validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300)) }.not_to raise_error
end
end
@@ -101,11 +164,13 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'does not track jobs' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
- validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
end
it 'does not raise exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
+ expect do
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
+ end.not_to raise_error
end
end
@@ -117,11 +182,13 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
be_a(Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError)
)
- validate.call(TestSizeLimiterWorker, { a: 'a' * 100 })
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 100))
end
it 'does not raise an exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
+ expect do
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
+ end.not_to raise_error
end
context 'when the worker has big_payload attribute' do
@@ -132,13 +199,17 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'does not track jobs' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
- validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
- validate.call('TestSizeLimiterWorker', { a: 'a' * 300 })
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
+ validate.call('TestSizeLimiterWorker', job_payload(a: 'a' * 300))
end
it 'does not raise an exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
- expect { validate.call('TestSizeLimiterWorker', { a: 'a' * 300 }) }.not_to raise_error
+ expect do
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a' * 300))
+ end.not_to raise_error
+ expect do
+ validate.call('TestSizeLimiterWorker', job_payload(a: 'a' * 300))
+ end.not_to raise_error
end
end
end
@@ -149,63 +220,60 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
it 'does not track job' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
- validate.call(TestSizeLimiterWorker, { a: 'a' })
+ validate.call(TestSizeLimiterWorker, job_payload(a: 'a'))
end
it 'does not raise an exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' }) }.not_to raise_error
+ expect { validate.call(TestSizeLimiterWorker, job_payload(a: 'a')) }.not_to raise_error
end
end
end
- context 'in raise mode' do
- let(:mode) { 'raise' }
-
- context 'when size limit is negative' do
- let(:size_limit) { -1 }
-
- it 'does not raise exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
- end
- end
+ context 'in compress mode' do
+ let(:mode) { 'compress' }
- context 'when size limit is 0' do
- let(:size_limit) { 0 }
+ context 'when job size is less than compression threshold' do
+ let(:size_limit) { 50 }
+ let(:compression_threshold) { 30 }
+ let(:job) { job_payload(a: 'a' * 10) }
- it 'does not raise exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
+ it 'does not raise an exception' do
+ expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).not_to receive(:compress)
+ expect { validate.call(TestSizeLimiterWorker, job_payload(a: 'a')) }.not_to raise_error
end
end
- context 'when job size is bigger than size limit' do
+ context 'when job size is bigger than compression threshold and less than size limit after compressed' do
let(:size_limit) { 50 }
+ let(:compression_threshold) { 30 }
+ let(:args) { { a: 'a' * 300 } }
+ let(:job) { job_payload(args) }
- it 'raises an exception' do
- expect do
- validate.call(TestSizeLimiterWorker, { a: 'a' * 300 })
- end.to raise_error(
- Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError,
- /TestSizeLimiterWorker job exceeds payload size limit/i
- )
- end
-
- context 'when the worker has big_payload attribute' do
- before do
- worker_class.big_payload!
- end
+ it 'does not raise an exception' do
+ expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:compress).with(
+ job, Sidekiq.dump_json(args)
+ ).and_return('a' * 40)
- it 'does not raise an exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' * 300 }) }.not_to raise_error
- expect { validate.call('TestSizeLimiterWorker', { a: 'a' * 300 }) }.not_to raise_error
- end
+ expect do
+ validate.call(TestSizeLimiterWorker, job)
+ end.not_to raise_error
end
end
- context 'when job size is less than size limit' do
+ context 'when job size is bigger than compression threshold and bigger than size limit after compressed' do
let(:size_limit) { 50 }
+ let(:compression_threshold) { 30 }
+ let(:args) { { a: 'a' * 3000 } }
+ let(:job) { job_payload(args) }
it 'does not raise an exception' do
- expect { validate.call(TestSizeLimiterWorker, { a: 'a' }) }.not_to raise_error
+ expect(::Gitlab::SidekiqMiddleware::SizeLimiter::Compressor).to receive(:compress).with(
+ job, Sidekiq.dump_json(args)
+ ).and_return('a' * 60)
+
+ expect do
+ validate.call(TestSizeLimiterWorker, job)
+ end.to raise_error(Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError)
end
end
end
@@ -218,6 +286,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
before do
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_MODE', mode)
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_LIMIT_BYTES', size_limit)
+ stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_COMPRESSION_THRESHOLD_BYTES', compression_threshold)
end
it_behaves_like 'validate limit job payload size'
@@ -226,14 +295,14 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
context 'when creating an instance with the related ENV variables' do
let(:validate) do
->(worker_clas, job) do
- validator = described_class.new(worker_class, job, mode: mode, size_limit: size_limit)
- validator.validate!
+ described_class.new(worker_class, job).validate!
end
end
before do
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_MODE', mode)
stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_LIMIT_BYTES', size_limit)
+ stub_env('GITLAB_SIDEKIQ_SIZE_LIMITER_COMPRESSION_THRESHOLD_BYTES', compression_threshold)
end
it_behaves_like 'validate limit job payload size'
@@ -242,7 +311,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::SizeLimiter::Validator do
context 'when creating an instance with mode and size limit' do
let(:validate) do
->(worker_clas, job) do
- validator = described_class.new(worker_class, job, mode: mode, size_limit: size_limit)
+ validator = described_class.new(
+ worker_class, job,
+ mode: mode, size_limit: size_limit, compression_threshold: compression_threshold
+ )
validator.validate!
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb
index 0efdef0c999..5e4e79e818e 100644
--- a/spec/lib/gitlab/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb
@@ -4,215 +4,212 @@ require 'spec_helper'
require 'sidekiq/testing'
RSpec.describe Gitlab::SidekiqMiddleware do
- before do
- stub_const('TestWorker', Class.new)
+ let(:job_args) { [0.01] }
+ let(:disabled_sidekiq_middlewares) { [] }
+ let(:chain) { Sidekiq::Middleware::Chain.new }
+ let(:queue) { 'test' }
+ let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
+ let(:worker_class) do
+ Class.new do
+ def self.name
+ 'TestWorker'
+ end
- TestWorker.class_eval do
- include Sidekiq::Worker
include ApplicationWorker
- def perform(_arg)
+ def perform(*args)
Gitlab::SafeRequestStore['gitaly_call_actual'] = 1
Gitlab::SafeRequestStore[:gitaly_query_time] = 5
end
end
end
- around do |example|
- Sidekiq::Testing.inline! { example.run }
+ before do
+ stub_const('TestWorker', worker_class)
end
- let(:worker_class) { TestWorker }
- let(:job_args) { [0.01] }
-
- # The test sets up a new server middleware stack, ensuring that the
- # appropriate middlewares, as passed into server_configurator,
- # are invoked.
- # Additionally the test ensure that each middleware is
- # 1) not failing
- # 2) yielding exactly once
- describe '.server_configurator' do
- around do |example|
- with_sidekiq_server_middleware do |chain|
- described_class.server_configurator(
- metrics: metrics,
- arguments_logger: arguments_logger,
- memory_killer: memory_killer
- ).call(chain)
+ shared_examples "a middleware chain" do |load_balancing_enabled|
+ before do
+ allow(::Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(load_balancing_enabled)
+ configurator.call(chain)
+ end
- example.run
+ it "passes through the right middlewares", :aggregate_failures do
+ enabled_sidekiq_middlewares.each do |middleware|
+ expect_next_instances_of(middleware, 1, true) do |middleware_instance|
+ expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original
+ end
end
+
+ expect { |b| chain.invoke(*worker_args, &b) }.to yield_control.once
end
+ end
+
+ shared_examples "a middleware chain for mailer" do |load_balancing_enabled|
+ let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
- let(:middleware_expected_args) { [a_kind_of(worker_class), hash_including({ 'args' => job_args }), anything] }
+ it_behaves_like "a middleware chain", load_balancing_enabled
+ end
+
+ describe '.server_configurator' do
+ let(:configurator) { described_class.server_configurator }
+ let(:worker_args) { [worker_class.new, { 'args' => job_args }, queue] }
+ let(:middleware_expected_args) { [a_kind_of(worker_class), hash_including({ 'args' => job_args }), queue] }
let(:all_sidekiq_middlewares) do
[
- Gitlab::SidekiqMiddleware::Monitor,
- Gitlab::SidekiqMiddleware::BatchLoader,
- Labkit::Middleware::Sidekiq::Server,
- Gitlab::SidekiqMiddleware::InstrumentationLogger,
- Gitlab::SidekiqVersioning::Middleware,
- Gitlab::SidekiqStatus::ServerMiddleware,
- Gitlab::SidekiqMiddleware::ServerMetrics,
- Gitlab::SidekiqMiddleware::ArgumentsLogger,
- Gitlab::SidekiqMiddleware::MemoryKiller,
- Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
- Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata,
- Gitlab::SidekiqMiddleware::WorkerContext::Server,
- Gitlab::SidekiqMiddleware::AdminMode::Server,
- Gitlab::SidekiqMiddleware::DuplicateJobs::Server
+ ::Gitlab::SidekiqMiddleware::Monitor,
+ ::Gitlab::SidekiqMiddleware::ServerMetrics,
+ ::Gitlab::SidekiqMiddleware::ArgumentsLogger,
+ ::Gitlab::SidekiqMiddleware::MemoryKiller,
+ ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
+ ::Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata,
+ ::Gitlab::SidekiqMiddleware::BatchLoader,
+ ::Labkit::Middleware::Sidekiq::Server,
+ ::Gitlab::SidekiqMiddleware::InstrumentationLogger,
+ ::Gitlab::Database::LoadBalancing::SidekiqServerMiddleware,
+ ::Gitlab::SidekiqMiddleware::AdminMode::Server,
+ ::Gitlab::SidekiqVersioning::Middleware,
+ ::Gitlab::SidekiqStatus::ServerMiddleware,
+ ::Gitlab::SidekiqMiddleware::WorkerContext::Server,
+ ::Gitlab::SidekiqMiddleware::DuplicateJobs::Server
]
end
- let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
+ describe "server metrics" do
+ around do |example|
+ with_sidekiq_server_middleware do |chain|
+ described_class.server_configurator(
+ metrics: true,
+ arguments_logger: true,
+ memory_killer: true
+ ).call(chain)
- shared_examples "a server middleware chain" do
- it "passes through the right server middlewares" do
- enabled_sidekiq_middlewares.each do |middleware|
- expect_next_instance_of(middleware) do |middleware_instance|
- expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original
- end
+ Sidekiq::Testing.inline! { example.run }
end
+ end
+ let(:gitaly_histogram) { double(:gitaly_histogram) }
- disabled_sidekiq_middlewares.each do |middleware|
- expect(middleware).not_to receive(:new)
- end
+ before do
+ allow(Gitlab::Metrics).to receive(:histogram).and_call_original
+
+ allow(Gitlab::Metrics).to receive(:histogram)
+ .with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything)
+ .and_return(gitaly_histogram)
+ end
+
+ it "records correct Gitaly duration" do
+ expect(gitaly_histogram).to receive(:observe).with(anything, 5.0)
worker_class.perform_async(*job_args)
end
end
- shared_examples "a server middleware chain for mailer" do
- let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
- let(:job_args) do
- [
- {
- "job_class" => "ActionMailer::MailDeliveryJob",
- "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e",
- "provider_job_id" => nil,
- "queue_name" => "mailers",
- "priority" => nil,
- "arguments" => [
- "Notify",
- "test_email",
- "deliver_now",
- {
- "args" => [
- "test@example.com",
- "subject",
- "body"
- ],
- ActiveJob::Arguments.const_get('RUBY2_KEYWORDS_KEY', false) => ["args"]
- }
- ],
- "executions" => 0,
- "exception_executions" => {},
- "locale" => "en",
- "timezone" => "UTC",
- "enqueued_at" => "2020-07-27T07:43:31Z"
- }
- ]
+ context "all optional middlewares on" do
+ context "when load balancing is enabled" do
+ before do
+ allow(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer, :release_host)
+ end
+
+ it_behaves_like "a middleware chain", true
+ it_behaves_like "a middleware chain for mailer", true
end
- it_behaves_like "a server middleware chain"
- end
+ context "when load balancing is disabled" do
+ let(:disabled_sidekiq_middlewares) do
+ [
+ Gitlab::Database::LoadBalancing::SidekiqServerMiddleware
+ ]
+ end
- context "all optional middlewares off" do
- let(:metrics) { false }
- let(:arguments_logger) { false }
- let(:memory_killer) { false }
- let(:disabled_sidekiq_middlewares) do
- [
- Gitlab::SidekiqMiddleware::ServerMetrics,
- Gitlab::SidekiqMiddleware::ArgumentsLogger,
- Gitlab::SidekiqMiddleware::MemoryKiller
- ]
+ it_behaves_like "a middleware chain", false
+ it_behaves_like "a middleware chain for mailer", false
end
-
- it_behaves_like "a server middleware chain"
- it_behaves_like "a server middleware chain for mailer"
end
- context "all optional middlewares on" do
- let(:metrics) { true }
- let(:arguments_logger) { true }
- let(:memory_killer) { true }
- let(:disabled_sidekiq_middlewares) { [] }
-
- it_behaves_like "a server middleware chain"
- it_behaves_like "a server middleware chain for mailer"
+ context "all optional middlewares off" do
+ let(:configurator) do
+ described_class.server_configurator(
+ metrics: false,
+ arguments_logger: false,
+ memory_killer: false
+ )
+ end
- context "server metrics" do
- let(:gitaly_histogram) { double(:gitaly_histogram) }
+ context "when load balancing is enabled" do
+ let(:disabled_sidekiq_middlewares) do
+ [
+ Gitlab::SidekiqMiddleware::ServerMetrics,
+ Gitlab::SidekiqMiddleware::ArgumentsLogger,
+ Gitlab::SidekiqMiddleware::MemoryKiller
+ ]
+ end
before do
- allow(Gitlab::Metrics).to receive(:histogram).and_call_original
-
- allow(Gitlab::Metrics).to receive(:histogram)
- .with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything)
- .and_return(gitaly_histogram)
+ allow(::Gitlab::Database::LoadBalancing).to receive_message_chain(:proxy, :load_balancer, :release_host)
end
- it "records correct Gitaly duration" do
- expect(gitaly_histogram).to receive(:observe).with(anything, 5.0)
+ it_behaves_like "a middleware chain", true
+ it_behaves_like "a middleware chain for mailer", true
+ end
- worker_class.perform_async(*job_args)
+ context "when load balancing is disabled" do
+ let(:disabled_sidekiq_middlewares) do
+ [
+ Gitlab::SidekiqMiddleware::ServerMetrics,
+ Gitlab::SidekiqMiddleware::ArgumentsLogger,
+ Gitlab::SidekiqMiddleware::MemoryKiller,
+ Gitlab::Database::LoadBalancing::SidekiqServerMiddleware
+ ]
end
+
+ it_behaves_like "a middleware chain", false
+ it_behaves_like "a middleware chain for mailer", false
end
end
end
- # The test sets up a new client middleware stack. The test ensures
- # that each middleware is:
- # 1) not failing
- # 2) yielding exactly once
describe '.client_configurator' do
- let(:chain) { Sidekiq::Middleware::Chain.new }
- let(:job) { { 'args' => job_args } }
- let(:queue) { 'default' }
+ let(:configurator) { described_class.client_configurator }
let(:redis_pool) { Sidekiq.redis_pool }
- let(:middleware_expected_args) { [worker_class_arg, job, queue, redis_pool] }
- let(:expected_middlewares) do
+ let(:middleware_expected_args) { [worker_class, hash_including({ 'args' => job_args }), queue, redis_pool] }
+ let(:worker_args) { [worker_class, { 'args' => job_args }, queue, redis_pool] }
+ let(:all_sidekiq_middlewares) do
[
- ::Gitlab::SidekiqMiddleware::WorkerContext::Client,
- ::Labkit::Middleware::Sidekiq::Client,
- ::Gitlab::SidekiqMiddleware::DuplicateJobs::Client,
- ::Gitlab::SidekiqStatus::ClientMiddleware,
- ::Gitlab::SidekiqMiddleware::AdminMode::Client,
- ::Gitlab::SidekiqMiddleware::SizeLimiter::Client,
- ::Gitlab::SidekiqMiddleware::ClientMetrics
+ ::Gitlab::SidekiqMiddleware::WorkerContext::Client,
+ ::Labkit::Middleware::Sidekiq::Client,
+ ::Gitlab::SidekiqMiddleware::DuplicateJobs::Client,
+ ::Gitlab::SidekiqStatus::ClientMiddleware,
+ ::Gitlab::SidekiqMiddleware::AdminMode::Client,
+ ::Gitlab::SidekiqMiddleware::SizeLimiter::Client,
+ ::Gitlab::SidekiqMiddleware::ClientMetrics,
+ ::Gitlab::Database::LoadBalancing::SidekiqClientMiddleware
]
end
- before do
- described_class.client_configurator.call(chain)
- end
-
- shared_examples "a client middleware chain" do
- # Its possible that a middleware could accidentally omit a yield call
- # this will prevent the full middleware chain from being executed.
- # This test ensures that this does not happen
- it "invokes the chain" do
- expected_middlewares do |middleware|
- expect_any_instance_of(middleware).to receive(:call).with(*middleware_expected_args).once.ordered.and_call_original
- end
-
- expect { |b| chain.invoke(worker_class_arg, job, queue, redis_pool, &b) }.to yield_control.once
+ context "when load balancing is disabled" do
+ let(:disabled_sidekiq_middlewares) do
+ [
+ Gitlab::Database::LoadBalancing::SidekiqClientMiddleware
+ ]
end
- end
- # Sidekiq documentation states that the worker class could be a string
- # or a class reference. We should test for both
- context "handles string worker_class values" do
- let(:worker_class_arg) { worker_class.to_s }
+ it_behaves_like "a middleware chain", false
+ it_behaves_like "a middleware chain for mailer", false
- it_behaves_like "a client middleware chain"
- end
+ # Sidekiq documentation states that the worker class could be a string
+ # or a class reference. We should test for both
+ context "worker_class as string value" do
+ let(:worker_args) { [worker_class.to_s, { 'args' => job_args }, queue, redis_pool] }
+ let(:middleware_expected_args) { [worker_class.to_s, hash_including({ 'args' => job_args }), queue, redis_pool] }
- context "handles string worker_class values" do
- let(:worker_class_arg) { worker_class }
+ it_behaves_like "a middleware chain", false
+ it_behaves_like "a middleware chain for mailer", false
+ end
+ end
- it_behaves_like "a client middleware chain"
+ context "when load balancing is enabled" do
+ it_behaves_like "a middleware chain", true
+ it_behaves_like "a middleware chain for mailer", true
end
end
end
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
index 26c83ed6793..226fdb9c948 100644
--- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -21,6 +21,55 @@ RSpec.describe Gitlab::Template::GitlabCiYmlTemplate do
end
end
+ describe '.find' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:other_project) { create(:project) }
+
+ described_class::TEMPLATES_WITH_LATEST_VERSION.keys.each do |key|
+ it "finds the latest template for #{key}" do
+ result = described_class.find(key, project)
+ expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml")
+ expect(result.content).to be_present
+ end
+
+ context 'when `redirect_to_latest_template` feature flag is disabled' do
+ before do
+ stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => false)
+ end
+
+ it "finds the stable template for #{key}" do
+ result = described_class.find(key, project)
+ expect(result.full_name).to eq("#{key}.gitlab-ci.yml")
+ expect(result.content).to be_present
+ end
+ end
+
+ context 'when `redirect_to_latest_template` feature flag is enabled on the project' do
+ before do
+ stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => project)
+ end
+
+ it "finds the latest template for #{key}" do
+ result = described_class.find(key, project)
+ expect(result.full_name).to eq("#{key}.latest.gitlab-ci.yml")
+ expect(result.content).to be_present
+ end
+ end
+
+ context 'when `redirect_to_latest_template` feature flag is enabled on the other project' do
+ before do
+ stub_feature_flags("redirect_to_latest_template_#{key.underscore.tr('/', '_')}".to_sym => other_project)
+ end
+
+ it "finds the stable template for #{key}" do
+ result = described_class.find(key, project)
+ expect(result.full_name).to eq("#{key}.gitlab-ci.yml")
+ expect(result.content).to be_present
+ end
+ end
+ end
+ end
+
describe '#content' do
it 'loads the full file' do
gitignore = subject.new(Rails.root.join('lib/gitlab/ci/templates/Ruby.gitlab-ci.yml'))
diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb
index 8bbd1263057..ab0611e6b6a 100644
--- a/spec/lib/gitlab/time_tracking_formatter_spec.rb
+++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb
@@ -47,5 +47,11 @@ RSpec.describe Gitlab::TimeTrackingFormatter do
it { expect(subject).to eq('1w 1d 1h 40m') }
end
+
+ context 'handles negative time input' do
+ let(:num_seconds) { -178_800 }
+
+ it { expect(subject).to eq('-1w 1d 1h 40m') }
+ end
end
end
diff --git a/spec/lib/gitlab/tracking/standard_context_spec.rb b/spec/lib/gitlab/tracking/standard_context_spec.rb
index 289818266bd..a0fb6a270a5 100644
--- a/spec/lib/gitlab/tracking/standard_context_spec.rb
+++ b/spec/lib/gitlab/tracking/standard_context_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Tracking::StandardContext do
context 'staging' do
before do
- stub_config_setting(url: 'https://staging.gitlab.com')
+ stub_config_setting(url: Gitlab::Saas.staging_com_url)
end
include_examples 'contains environment', 'staging'
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Tracking::StandardContext do
context 'production' do
before do
- stub_config_setting(url: 'https://gitlab.com')
+ stub_config_setting(url: Gitlab::Saas.com_url)
end
include_examples 'contains environment', 'production'
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Tracking::StandardContext do
context 'org' do
before do
- stub_config_setting(url: 'https://dev.gitlab.org')
+ stub_config_setting(url: Gitlab::Saas.dev_url)
end
include_examples 'contains environment', 'org'
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index 92e51b8ea23..1ed639b2f7d 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
end
it 'has all definitons valid' do
- expect { described_class.definitions }.not_to raise_error(Gitlab::Usage::Metric::InvalidMetricError)
+ expect { described_class.definitions }.not_to raise_error
end
describe '#key' do
@@ -73,6 +73,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
:distribution | 'test'
:tier | %w(test ee)
:name | 'count_<adjective_describing>_boards'
+ :repair_issue_url | nil
:instrumentation_class | 'Metric_Class'
:instrumentation_class | 'metricClass'
@@ -103,6 +104,19 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
end
end
end
+
+ context 'conditional validations' do
+ context 'when metric has broken status' do
+ it 'has to have repair issue url provided' do
+ attributes[:status] = 'broken'
+ attributes.delete(:repair_issue_url)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
+
+ described_class.new(path, attributes).validate!
+ end
+ end
+ end
end
describe 'statuses' do
@@ -153,7 +167,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
is_expected.to be_one
end
- it 'when the same meric is defined multiple times raises exception' do
+ it 'when the same metric is defined multiple times raises exception' do
write_metric(metric1, path, yaml_content)
write_metric(metric2, path, yaml_content)
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
index 0fb3a69df05..8e02f4f562c 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -25,34 +25,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
end
context 'aggregated_metrics_data' do
- shared_examples 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature' do
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
- end
- end
-
- context 'with disabled database_sourced_aggregated_metrics feature flag' do
- before do
- stub_feature_flags(database_sourced_aggregated_metrics: false)
- end
-
- let(:aggregated_metrics) do
- [
- aggregated_metric(name: "gmau_2", source: "database", time_frame: time_frame)
- ]
- end
-
- it 'skips database sourced metrics', :aggregate_failures do
- results = {}
- params = { start_date: start_date, end_date: end_date, recorded_at: recorded_at }
-
- expect(sources::PostgresHll).not_to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3]))
- expect(aggregated_metrics_data).to eq(results)
- end
- end
- end
-
shared_examples 'aggregated_metrics_data' do
context 'no aggregated metric is defined' do
it 'returns empty hash' do
@@ -237,7 +209,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
let(:time_frame) { ['all'] }
it_behaves_like 'database_sourced_aggregated_metrics'
- it_behaves_like 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature'
context 'redis sourced aggregated metrics' do
let(:aggregated_metrics) { [aggregated_metric(name: 'gmau_1', time_frame: time_frame)] }
@@ -278,7 +249,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
it_behaves_like 'database_sourced_aggregated_metrics'
it_behaves_like 'redis_sourced_aggregated_metrics'
- it_behaves_like 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature'
end
describe '.aggregated_metrics_monthly_data' do
@@ -289,7 +259,6 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
it_behaves_like 'database_sourced_aggregated_metrics'
it_behaves_like 'redis_sourced_aggregated_metrics'
- it_behaves_like 'db sourced aggregated metrics without database_sourced_aggregated_metrics feature'
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb
index 52c1ccdcd47..6f03c5a9ae3 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb
@@ -5,5 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBoardsMetric do
let_it_be(:board) { create(:board) }
- it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }, 1
+ let(:expected_value) { 1 }
+ let(:expected_query) { 'SELECT COUNT("boards"."id") FROM "boards"' }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb
index c3b59904f41..183aa03dd8a 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_issues_metric_spec.rb
@@ -5,5 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountIssuesMetric do
let_it_be(:issue) { create(:issue) }
- it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }, 1
+ let(:expected_value) { 1 }
+ let(:expected_query) { 'SELECT COUNT("issues"."id") FROM "issues"' }
+
+ 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_creating_issues_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
index 9f4686ab6cd..3fb4c3a4e3f 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_creating_issues_metric_spec.rb
@@ -8,10 +8,18 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUsersCreatingIssue
let_it_be(:old_issue) { create(:issue, author: author, created_at: 2.months.ago) }
context 'with all time frame' do
- it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }, 1
+ let(:expected_value) { 1 }
+ let(:expected_query) { 'SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"' }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: 'all' }
end
context 'for 28d time frame' do
- it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' }, 1
+ let(:expected_value) { 1 }
+ let(:start) { 30.days.ago.to_s(:db) }
+ let(:finish) { 2.days.ago.to_s(:db) }
+ let(:expected_query) { "SELECT COUNT(DISTINCT \"issues\".\"author_id\") FROM \"issues\" WHERE \"issues\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'" }
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' }
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb
deleted file mode 100644
index 7adba825a13..00000000000
--- a/spec/lib/gitlab/usage/metrics/instrumentations/count_users_using_approve_quick_action_metric_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUsersUsingApproveQuickActionMetric, :clean_gitlab_redis_shared_state do
- before do
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 1.week.ago)
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 2.weeks.ago)
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 2, time: 2.weeks.ago)
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 2, time: 2.months.ago)
- end
-
- it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'redis_hll' }, 2
- it_behaves_like 'a correct instrumented metric value', { time_frame: '7d', data_source: 'redis_hll' }, 1
-end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/hostname_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/hostname_metric_spec.rb
index 83e07200025..95e159a5bf7 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/hostname_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/hostname_metric_spec.rb
@@ -3,5 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::HostnameMetric do
- it_behaves_like 'a correct instrumented metric value', { time_frame: 'none', data_source: 'ruby' }, Gitlab.config.gitlab.host
+ let(:expected_value) { Gitlab.config.gitlab.host }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
new file mode 100644
index 00000000000..347a2c779cb
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric, :clean_gitlab_redis_shared_state do
+ before do
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 1.week.ago)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 1, time: 2.weeks.ago)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 2, time: 2.weeks.ago)
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(:i_quickactions_approve, values: 2, time: 2.months.ago)
+ end
+
+ context 'for 28d' do
+ let(:expected_value) { 2 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', options: { events: ['i_quickactions_approve'] } }
+ end
+
+ context 'for 7d' do
+ let(:expected_value) { 1 }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '7d', options: { events: ['i_quickactions_approve'] } }
+ end
+
+ it 'raise exception if events options is not present' do
+ expect { described_class.new(time_frame: '28d') }.to raise_error(ArgumentError)
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb
index 212dd3dc851..33e7e85611d 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb
@@ -3,5 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::UuidMetric do
- it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }, Gitlab::CurrentSettings.uuid
+ let(:expected_value) { Gitlab::CurrentSettings.uuid }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }
end
diff --git a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
new file mode 100644
index 00000000000..6955fbcaf5a
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do
+ describe '#for' do
+ shared_examples 'name suggestion' do
+ it 'return correct name' do
+ expect(described_class.for(operation, relation: relation, column: column)).to match name_suggestion
+ end
+ end
+
+ context 'for count with nil column' do
+ it_behaves_like 'name suggestion' do
+ let(:operation) { :count }
+ let(:relation) { Board }
+ let(:column) { nil }
+ let(:name_suggestion) { /count_boards/ }
+ end
+ end
+
+ context 'for count with column :id' do
+ it_behaves_like 'name suggestion' do
+ let(:operation) { :count }
+ let(:relation) { Board }
+ let(:column) { :id }
+ let(:name_suggestion) { /count_boards/ }
+ end
+ end
+
+ context 'for count distinct with column defined metrics' do
+ it_behaves_like 'name suggestion' do
+ let(:operation) { :distinct_count }
+ let(:relation) { ZoomMeeting }
+ let(:column) { :issue_id }
+ let(:name_suggestion) { /count_distinct_issue_id_from_zoom_meetings/ }
+ end
+ end
+
+ 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(:operation) { :count }
+ let(:relation) { Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot) }
+ let(:column) { nil }
+ let(:name_suggestion) { /count_<adjective describing\: '\(issues\.author_id != \d+\)'>_issues_<with>_alert_management_alerts/ }
+ end
+ end
+ end
+
+ context 'strips off time period constraint' do
+ it_behaves_like 'name suggestion' do
+ # corresponding metric is collected with distinct_count(::Clusters::Cluster.aws_installed.enabled.where(time_period), :user_id)
+ let(:operation) { :distinct_count }
+ let(:relation) { ::Clusters::Cluster.aws_installed.enabled.where(created_at: 30.days.ago..2.days.ago ) }
+ let(:column) { :user_id }
+ let(:constraints) { /<adjective describing\: '\(clusters.provider_type = \d+ AND \(cluster_providers_aws\.status IN \(\d+\)\) AND clusters\.enabled = TRUE\)'>/ }
+ let(:name_suggestion) { /count_distinct_user_id_from_#{constraints}_clusters_<with>_#{constraints}_cluster_providers_aws/ }
+ end
+ end
+
+ context 'for sum metrics' do
+ it_behaves_like 'name suggestion' do
+ # corresponding metric is collected with sum(JiraImportState.finished, :imported_issues_count)
+ let(:key_path) { 'counts.jira_imports_total_imported_issues_count' }
+ let(:operation) { :sum }
+ let(:relation) { JiraImportState.finished }
+ let(:column) { :imported_issues_count}
+ let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing\: '\(jira_imports\.status = \d+\)'>_jira_imports/ }
+ end
+ end
+
+ context 'for redis metrics' do
+ it_behaves_like 'name suggestion' do
+ # corresponding metric is collected with redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) }
+ let(:operation) { :redis }
+ let(:column) { nil }
+ let(:relation) { nil }
+ let(:name_suggestion) { /<please fill metric name, suggested format is: {subject}_{verb}{ing|ed}_{object} eg: users_creating_epics or merge_requests_viewed_in_single_file_mode>/ }
+ end
+ end
+
+ context 'for alt_usage_data metrics' do
+ it_behaves_like 'name suggestion' do
+ # corresponding metric is collected with alt_usage_data(fallback: nil) { operating_system }
+ let(:operation) { :alt }
+ let(:column) { nil }
+ let(:relation) { nil }
+ let(:name_suggestion) { /<please fill metric name>/ }
+ end
+ end
+ 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 34b073b4729..b4ab9d4861b 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb
@@ -33,24 +33,6 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
end
context 'joined relations' do
- context 'counted attribute comes from joined relation' do
- it_behaves_like 'name suggestion' do
- # corresponding metric is collected with:
- # distinct_count(
- # ::Clusters::Applications::Ingress.modsecurity_enabled.logging
- # .joins(cluster: :deployments)
- # .merge(::Clusters::Cluster.enabled)
- # .merge(Deployment.success),
- # ::Deployment.arel_table[:environment_id]
- # )
- let(:key_path) { 'counts.ingress_modsecurity_logging' }
- let(:name_suggestion) do
- constrains = /'\(clusters_applications_ingress\.modsecurity_enabled = TRUE AND clusters_applications_ingress\.modsecurity_mode = \d+ AND clusters.enabled = TRUE AND deployments.status = \d+\)'/
- /count_distinct_environment_id_from_<adjective describing\: #{constrains}>_deployments_<with>_<adjective describing\: #{constrains}>_clusters_<having>_<adjective describing\: #{constrains}>_clusters_applications_ingress/
- end
- end
- end
-
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)
diff --git a/spec/lib/gitlab/usage/metrics/query_spec.rb b/spec/lib/gitlab/usage/metrics/query_spec.rb
new file mode 100644
index 00000000000..60c8d044a64
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/query_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Query do
+ describe '.count' do
+ it 'returns the raw SQL' do
+ expect(described_class.for(:count, User)).to eq('SELECT COUNT("users"."id") FROM "users"')
+ end
+
+ it 'does not mix a nil column with keyword arguments' do
+ expect(described_class.for(:count, User, nil)).to eq('SELECT COUNT("users"."id") FROM "users"')
+ end
+ end
+
+ describe '.distinct_count' do
+ it 'returns the raw SQL' do
+ expect(described_class.for(:distinct_count, Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"')
+ end
+
+ it 'does not mix a nil column with keyword arguments' do
+ expect(described_class.for(:distinct_count, Issue, nil)).to eq('SELECT COUNT(DISTINCT "issues"."id") FROM "issues"')
+ end
+ end
+
+ describe '.sum' do
+ it 'returns the raw SQL' do
+ expect(described_class.for(:sum, Issue, :weight)).to eq('SELECT SUM("issues"."weight") FROM "issues"')
+ end
+ end
+
+ describe 'estimate_batch_distinct_count' do
+ it 'returns the raw SQL' do
+ expect(described_class.for(:estimate_batch_distinct_count, Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"')
+ end
+ end
+
+ describe '.histogram' do
+ it 'returns the histogram sql' do
+ expect(described_class.for(:histogram, AlertManagement::HttpIntegration.active,
+ :project_id, buckets: 1..2, bucket_size: 101))
+ .to match(/^WITH "count_cte" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+
+ describe 'other' do
+ it 'raise ArgumentError error' do
+ expect { described_class.for(:other, nil) }.to raise_error(ArgumentError, 'other operation not supported')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data/topology_spec.rb b/spec/lib/gitlab/usage_data/topology_spec.rb
index b8462e0290c..737580e3493 100644
--- a/spec/lib/gitlab/usage_data/topology_spec.rb
+++ b/spec/lib/gitlab/usage_data/topology_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe Gitlab::UsageData::Topology do
},
{
name: 'web',
- server: 'unicorn'
+ server: 'puma'
}
]
}
@@ -724,7 +724,7 @@ RSpec.describe Gitlab::UsageData::Topology do
},
# instance 2
{
- 'metric' => { 'instance' => 'instance2:8080', 'job' => 'gitlab-rails', 'server' => 'unicorn' },
+ 'metric' => { 'instance' => 'instance2:8080', 'job' => 'gitlab-rails', 'server' => 'puma' },
'value' => [1000, '1']
}
])
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 6486a5a22ba..041fc2f20a8 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
@@ -352,14 +352,6 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_INCLUDING_CI_CONFIG_ACTION }
end
-
- context 'when FF usage_data_o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile is disabled' do
- before do
- stub_feature_flags(usage_data_o_pipeline_authoring_unique_users_pushing_mr_ciconfigfile: false)
- end
-
- it_behaves_like 'not tracked merge request unique event'
- end
end
context 'when merge request does not include any ci config change' do
@@ -386,4 +378,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_LABELS_CHANGED_ACTION }
end
end
+
+ describe '.track_loading_conflict_ui_action' do
+ subject { described_class.track_loading_conflict_ui_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_LOAD_CONFLICT_UI_ACTION }
+ end
+ end
+
+ describe '.track_resolve_conflict_action' do
+ subject { described_class.track_resolve_conflict_action(user: user) }
+
+ it_behaves_like 'a tracked merge request unique event' do
+ let(:action) { described_class::MR_RESOLVE_CONFLICT_ACTION }
+ end
+ end
end
diff --git a/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb
index c484595ee71..78cc27c8569 100644
--- a/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/package_event_counter_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Gitlab::UsageDataCounters::PackageEventCounter, :clean_gitlab_red
end
it 'includes the right events' do
- expect(described_class::KNOWN_EVENTS.size).to eq 51
+ expect(described_class::KNOWN_EVENTS.size).to eq 52
end
described_class::KNOWN_EVENTS.each do |event|
diff --git a/spec/lib/gitlab/usage_data_queries_spec.rb b/spec/lib/gitlab/usage_data_queries_spec.rb
index 695859c8e6e..438ae3efd11 100644
--- a/spec/lib/gitlab/usage_data_queries_spec.rb
+++ b/spec/lib/gitlab/usage_data_queries_spec.rb
@@ -13,9 +13,7 @@ RSpec.describe Gitlab::UsageDataQueries do
end
it 'does not mix a nil column with keyword arguments' do
- expect(described_class).to receive(:raw_sql).with(User, nil)
-
- described_class.count(User, start: 1, finish: 2)
+ expect(described_class.count(User, nil)).to eq('SELECT COUNT("users"."id") FROM "users"')
end
end
@@ -25,9 +23,7 @@ RSpec.describe Gitlab::UsageDataQueries do
end
it 'does not mix a nil column with keyword arguments' do
- expect(described_class).to receive(:raw_sql).with(Issue, nil, :distinct)
-
- described_class.distinct_count(Issue, nil, start: 1, finish: 2)
+ expect(described_class.distinct_count(Issue, nil, start: 1, finish: 2)).to eq('SELECT COUNT(DISTINCT "issues"."id") FROM "issues"')
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index d4b6ac09261..ea82de186f5 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(described_class.usage_activity_by_stage_package({})).to eq(
projects_with_packages: 2
)
- expect(described_class.usage_activity_by_stage_package(described_class.last_28_days_time_period)).to eq(
+ expect(described_class.usage_activity_by_stage_package(described_class.monthly_time_range_db_params)).to eq(
projects_with_packages: 1
)
end
@@ -135,7 +135,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
project_clusters_disabled: 2,
project_clusters_enabled: 10
)
- expect(described_class.usage_activity_by_stage_configure(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_configure(described_class.monthly_time_range_db_params)).to include(
clusters_applications_cert_managers: 1,
clusters_applications_helm: 1,
clusters_applications_ingress: 1,
@@ -185,7 +185,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
snippets: 2,
suggestions: 2
)
- expect(described_class.usage_activity_by_stage_create(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_create(described_class.monthly_time_range_db_params)).to include(
deploy_keys: 1,
keys: 1,
merge_requests: 1,
@@ -225,7 +225,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
omniauth_providers: ['google_oauth2'],
user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
)
- expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
events: 1,
groups: 1,
users_created: 3,
@@ -252,7 +252,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
unique_users_all_imports: 10
)
- expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
unique_users_all_imports: 5
)
end
@@ -294,7 +294,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
gitlab: 2,
gitlab_migration: 2,
gitlab_project: 2,
- manifest: 2
+ manifest: 2,
+ total: 18
},
issue_imports: {
jira: 2,
@@ -326,7 +327,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
}
)
- expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
{
bulk_imports: {
gitlab_v1: 1,
@@ -341,7 +342,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
gitlab: 1,
gitlab_migration: 1,
gitlab_project: 1,
- manifest: 1
+ manifest: 1,
+ total: 9
},
issue_imports: {
jira: 1,
@@ -371,7 +373,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
csv: Gitlab::UsageData::DEPRECATED_VALUE
},
groups_imported: Gitlab::UsageData::DEPRECATED_VALUE
-
}
)
end
@@ -410,7 +411,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
projects_with_enabled_alert_integrations_histogram: { '1' => 2 }
)
- data_28_days = described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period)
+ data_28_days = described_class.usage_activity_by_stage_monitor(described_class.monthly_time_range_db_params)
expect(data_28_days).to include(
clusters: 1,
clusters_applications_prometheus: 1,
@@ -449,7 +450,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
projects_jira_dvcs_cloud_active: 2,
projects_jira_dvcs_server_active: 2
)
- expect(described_class.usage_activity_by_stage_plan(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_plan(described_class.monthly_time_range_db_params)).to include(
issues: 2,
notes: 1,
projects: 1,
@@ -478,7 +479,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
releases: 2,
successful_deployments: 2
)
- expect(described_class.usage_activity_by_stage_release(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_release(described_class.monthly_time_range_db_params)).to include(
deployments: 1,
failed_deployments: 1,
releases: 1,
@@ -512,7 +513,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
ci_triggers: 2,
clusters_applications_runner: 2
)
- expect(described_class.usage_activity_by_stage_verify(described_class.last_28_days_time_period)).to include(
+ expect(described_class.usage_activity_by_stage_verify(described_class.monthly_time_range_db_params)).to include(
ci_builds: 1,
ci_external_pipelines: 1,
ci_internal_pipelines: 1,
@@ -575,7 +576,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
expect(count_data[:projects_with_tracing_enabled]).to eq(1)
expect(count_data[:projects_with_enabled_alert_integrations]).to eq(1)
- expect(count_data[:projects_with_prometheus_alerts]).to eq(2)
expect(count_data[:projects_with_terraform_reports]).to eq(2)
expect(count_data[:projects_with_terraform_states]).to eq(2)
expect(count_data[:projects_with_alerts_created]).to eq(1)
@@ -706,10 +706,9 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
describe '.system_usage_data_monthly' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, created_at: 3.days.ago) }
before do
- project = create(:project)
env = create(:environment)
create(:package, project: project, created_at: 3.days.ago)
create(:package, created_at: 2.months.ago, project: project)
@@ -742,6 +741,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(counts_monthly[:personal_snippets]).to eq(1)
expect(counts_monthly[:project_snippets]).to eq(1)
expect(counts_monthly[:projects_with_alerts_created]).to eq(1)
+ expect(counts_monthly[:projects]).to eq(1)
expect(counts_monthly[:packages]).to eq(1)
expect(counts_monthly[:promoted_issues]).to eq(1)
end
@@ -966,138 +966,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- describe '.ingress_modsecurity_usage' do
- subject { described_class.ingress_modsecurity_usage }
-
- let(:environment) { create(:environment) }
- let(:project) { environment.project }
- let(:environment_scope) { '*' }
- let(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
- let(:cluster) { create(:cluster, environment_scope: environment_scope, projects: [project]) }
- let(:ingress_mode) { :modsecurity_blocking }
- let!(:ingress) { create(:clusters_applications_ingress, ingress_mode, cluster: cluster) }
-
- context 'when cluster is disabled' do
- let(:cluster) { create(:cluster, :disabled, projects: [project]) }
-
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'when deployment is unsuccessful' do
- let!(:deployment) { create(:deployment, :failed, environment: environment, project: project, cluster: cluster) }
-
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'when deployment is successful' do
- let!(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
-
- context 'when modsecurity is in blocking mode' do
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(1)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'when modsecurity is in logging mode' do
- let(:ingress_mode) { :modsecurity_logging }
-
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(1)
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'when modsecurity is disabled' do
- let(:ingress_mode) { :modsecurity_disabled }
-
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(1)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'when modsecurity is not installed' do
- let(:ingress_mode) { :modsecurity_not_installed }
-
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(1)
- end
- end
-
- context 'with multiple projects' do
- let(:environment_2) { create(:environment) }
- let(:project_2) { environment_2.project }
- let(:cluster_2) { create(:cluster, environment_scope: environment_scope, projects: [project_2]) }
- let!(:ingress_2) { create(:clusters_applications_ingress, :modsecurity_logging, cluster: cluster_2) }
- let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project_2, cluster: cluster_2) }
-
- it 'gathers non-duplicated ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(1)
- expect(subject[:ingress_modsecurity_blocking]).to eq(1)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'with multiple deployments' do
- let!(:deployment_2) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
-
- it 'gathers non-duplicated ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(1)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'with multiple projects' do
- let(:environment_2) { create(:environment) }
- let(:project_2) { environment_2.project }
- let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project_2, cluster: cluster) }
- let(:cluster) { create(:cluster, environment_scope: environment_scope, projects: [project, project_2]) }
-
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(2)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
-
- context 'with multiple environments' do
- let!(:environment_2) { create(:environment, project: project) }
- let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project, cluster: cluster) }
-
- it 'gathers ingress data' do
- expect(subject[:ingress_modsecurity_logging]).to eq(0)
- expect(subject[:ingress_modsecurity_blocking]).to eq(2)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
- end
- end
- end
- end
-
describe '.grafana_embed_usage_data' do
subject { described_class.grafana_embed_usage_data }
@@ -1499,7 +1367,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
"in_product_marketing_email_team_1_sent" => -1,
"in_product_marketing_email_team_1_cta_clicked" => -1,
"in_product_marketing_email_team_2_sent" => -1,
- "in_product_marketing_email_team_2_cta_clicked" => -1
+ "in_product_marketing_email_team_2_cta_clicked" => -1,
+ "in_product_marketing_email_experience_0_sent" => -1
}
expect(subject).to eq(expected_data)
@@ -1537,7 +1406,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
"in_product_marketing_email_team_1_sent" => 0,
"in_product_marketing_email_team_1_cta_clicked" => 0,
"in_product_marketing_email_team_2_sent" => 0,
- "in_product_marketing_email_team_2_cta_clicked" => 0
+ "in_product_marketing_email_team_2_cta_clicked" => 0,
+ "in_product_marketing_email_experience_0_sent" => 0
}
expect(subject).to eq(expected_data)
diff --git a/spec/lib/gitlab/utils/measuring_spec.rb b/spec/lib/gitlab/utils/measuring_spec.rb
index 4931ebf26f0..5dad79b1c5f 100644
--- a/spec/lib/gitlab/utils/measuring_spec.rb
+++ b/spec/lib/gitlab/utils/measuring_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Utils::Measuring do
let(:result) { "result" }
before do
- allow(ActiveSupport::Logger).to receive(:logger_outputs_to?).with(Gitlab::Utils::Measuring.logger, STDOUT).and_return(false)
+ allow(ActiveSupport::Logger).to receive(:logger_outputs_to?).with(Gitlab::Utils::Measuring.logger, $stdout).and_return(false)
end
let(:measurement) { described_class.new(base_log_data) }
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d40ecc7e04e..09f90a3e5b6 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -60,37 +60,6 @@ RSpec.describe Gitlab::Workhorse do
}.deep_stringify_keys)
end
- context 'when include_lfs_blobs_in_archive is disabled' do
- before do
- stub_feature_flags(include_lfs_blobs_in_archive: false)
- end
-
- it 'sets include_lfs_blobs to false' do
- key, command, params = decode_workhorse_header(subject)
-
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to eq({
- 'GitalyServer' => {
- features: { 'gitaly-feature-enforce-requests-limits' => 'true' },
- address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'ArchivePath' => metadata['ArchivePath'],
- 'GetArchiveRequest' => Base64.encode64(
- Gitaly::GetArchiveRequest.new(
- repository: repository.gitaly_repository,
- commit_id: metadata['CommitId'],
- prefix: metadata['ArchivePrefix'],
- format: Gitaly::GetArchiveRequest::Format::ZIP,
- path: path,
- include_lfs_blobs: false
- ).to_proto
- )
- }.deep_stringify_keys)
- end
- end
-
context 'when archive caching is disabled' do
let(:cache_disabled) { true }
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 4df00eaa439..869eaf26772 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -70,20 +70,21 @@ RSpec.describe Gitlab do
end
describe '.com?' do
- it 'is true when on GitLab.com' do
- stub_config_setting(url: 'https://gitlab.com')
+ it "is true when on #{Gitlab::Saas.com_url}" do
+ stub_config_setting(url: Gitlab::Saas.com_url)
expect(described_class.com?).to eq true
end
- it 'is true when on staging' do
- stub_config_setting(url: 'https://staging.gitlab.com')
+ it "is true when on #{Gitlab::Saas.staging_com_url}" do
+ stub_config_setting(url: Gitlab::Saas.staging_com_url)
expect(described_class.com?).to eq true
end
it 'is true when on other gitlab subdomain' do
- stub_config_setting(url: 'https://example.gitlab.com')
+ url_with_subdomain = Gitlab::Saas.com_url.gsub('https://', 'https://example.')
+ stub_config_setting(url: url_with_subdomain)
expect(described_class.com?).to eq true
end
@@ -118,14 +119,14 @@ RSpec.describe Gitlab do
describe '.staging?' do
subject { described_class.staging? }
- it 'is false when on GitLab.com' do
- stub_config_setting(url: 'https://gitlab.com')
+ it "is false when on #{Gitlab::Saas.com_url}" do
+ stub_config_setting(url: Gitlab::Saas.com_url)
expect(subject).to eq false
end
- it 'is true when on staging' do
- stub_config_setting(url: 'https://staging.gitlab.com')
+ it "is true when on #{Gitlab::Saas.staging_com_url}" do
+ stub_config_setting(url: Gitlab::Saas.staging_com_url)
expect(subject).to eq true
end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index b674ae0218f..3dd8f7c413e 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -91,7 +91,6 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
cluster: {
name: cluster_name,
initial_node_count: cluster_size,
- initial_cluster_version: '1.18',
node_config: {
machine_type: machine_type,
oauth_scopes: [
@@ -101,7 +100,6 @@ RSpec.describe GoogleApi::CloudPlatform::Client do
]
},
master_auth: {
- username: 'admin',
client_certificate_config: {
issue_client_certificate: true
}
diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb
index 32755d1103c..5d57a226baf 100644
--- a/spec/lib/mattermost/client_spec.rb
+++ b/spec/lib/mattermost/client_spec.rb
@@ -14,13 +14,13 @@ RSpec.describe Mattermost::Client do
it 'yields an error on malformed JSON' do
bad_json = Struct::Request.new("I'm not json", true)
- expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError)
+ expect { subject.send(:json_response, bad_json) }.to raise_error(::Mattermost::ClientError)
end
it 'shows a client error if the request was unsuccessful' do
bad_request = Struct::Request.new("true", false)
- expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError)
+ expect { subject.send(:json_response, bad_request) }.to raise_error(::Mattermost::ClientError)
end
end
end
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
index 0f2711e0b11..18cd1ff97a6 100644
--- a/spec/lib/mattermost/command_spec.rb
+++ b/spec/lib/mattermost/command_spec.rb
@@ -6,10 +6,10 @@ RSpec.describe Mattermost::Command do
let(:params) { { 'token' => 'token', team_id: 'abc' } }
before do
- session = Mattermost::Session.new(nil)
+ session = ::Mattermost::Session.new(nil)
session.base_uri = 'http://mattermost.example.com'
- allow_any_instance_of(Mattermost::Client).to receive(:with_session)
+ allow_any_instance_of(::Mattermost::Client).to receive(:with_session)
.and_yield(session)
end
@@ -57,7 +57,7 @@ RSpec.describe Mattermost::Command do
end
it 'raises an error with message' do
- expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
+ expect { subject }.to raise_error(::Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
end
end
end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 67ccb48e3a7..e2e1b4c28c7 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Mattermost::Session, type: :request do
context 'without oauth uri' do
it 'makes a request to the oauth uri' do
- expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
end
@@ -49,7 +49,7 @@ RSpec.describe Mattermost::Session, type: :request do
it 'can not create a session' do
expect do
subject.with_session
- end.to raise_error(Mattermost::NoSessionError)
+ end.to raise_error(::Mattermost::NoSessionError)
end
end
@@ -113,13 +113,13 @@ RSpec.describe Mattermost::Session, type: :request do
expect_to_cancel_exclusive_lease(lease_key, 'uuid')
# Cannot set up a session, but we should still cancel the lease
- expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
it 'returns a NoSessionError error without lease' do
stub_exclusive_lease_taken(lease_key)
- expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+ expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
end
end
end
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index e3ef5ff5377..b2db770c9b9 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Mattermost::Team do
session = Mattermost::Session.new(nil)
session.base_uri = 'http://mattermost.example.com'
- allow_any_instance_of(Mattermost::Client).to receive(:with_session)
+ allow_any_instance_of(::Mattermost::Client).to receive(:with_session)
.and_yield(session)
end
@@ -65,7 +65,7 @@ RSpec.describe Mattermost::Team do
end
it 'raises an error with message' do
- expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.')
+ expect { subject }.to raise_error(::Mattermost::Error, 'Cannot list teams.')
end
end
end
@@ -123,7 +123,7 @@ RSpec.describe Mattermost::Team do
end
it 'raises an error with message' do
- expect { subject }.to raise_error(Mattermost::Error, 'A team with that name already exists')
+ expect { subject }.to raise_error(::Mattermost::Error, 'A team with that name already exists')
end
end
end
@@ -169,7 +169,7 @@ RSpec.describe Mattermost::Team do
end
it 'raises an error with message' do
- expect { subject }.to raise_error(Mattermost::Error, "We couldn't find the existing team")
+ expect { subject }.to raise_error(::Mattermost::Error, "We couldn't find the existing team")
end
end
end
diff --git a/spec/lib/peek/views/active_record_spec.rb b/spec/lib/peek/views/active_record_spec.rb
index 9eeeca4de61..e5aae2822ed 100644
--- a/spec/lib/peek/views/active_record_spec.rb
+++ b/spec/lib/peek/views/active_record_spec.rb
@@ -5,16 +5,17 @@ require 'spec_helper'
RSpec.describe Peek::Views::ActiveRecord, :request_store do
subject { Peek.views.find { |v| v.instance_of?(Peek::Views::ActiveRecord) } }
- let(:connection_1) { double(:connection) }
- let(:connection_2) { double(:connection) }
- let(:connection_3) { double(:connection) }
+ let(:connection_replica) { double(:connection_replica) }
+ let(:connection_primary_1) { double(:connection_primary) }
+ let(:connection_primary_2) { double(:connection_primary) }
+ let(:connection_unknown) { double(:connection_unknown) }
let(:event_1) do
{
name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10',
cached: false,
- connection: connection_1
+ connection: connection_primary_1
}
end
@@ -23,7 +24,7 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL',
sql: 'SELECT * FROM users WHERE id = 10',
cached: true,
- connection: connection_2
+ connection: connection_replica
}
end
@@ -32,55 +33,141 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
name: 'SQL',
sql: 'UPDATE users SET admin = true WHERE id = 10',
cached: false,
- connection: connection_3
+ connection: connection_primary_2
+ }
+ end
+
+ let(:event_4) do
+ {
+ name: 'SCHEMA',
+ sql: 'SELECT VERSION()',
+ cached: false,
+ connection: connection_unknown
}
end
before do
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
- allow(connection_1).to receive(:transaction_open?).and_return(false)
- allow(connection_2).to receive(:transaction_open?).and_return(false)
- allow(connection_3).to receive(:transaction_open?).and_return(true)
+ allow(connection_replica).to receive(:transaction_open?).and_return(false)
+ allow(connection_primary_1).to receive(:transaction_open?).and_return(false)
+ allow(connection_primary_2).to receive(:transaction_open?).and_return(true)
+ allow(connection_unknown).to receive(:transaction_open?).and_return(false)
end
- it 'subscribes and store data into peek views' do
- Timecop.freeze(2021, 2, 23, 10, 0) do
- ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
- ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
- ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 3.seconds, '3', event_3)
+ context 'when database load balancing is not enabled' do
+ it 'subscribes and store data into peek views' do
+ Timecop.freeze(2021, 2, 23, 10, 0) do
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 3.seconds, '3', event_3)
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 4.seconds, '4', event_4)
+ end
+
+ expect(subject.results).to match(
+ calls: 4,
+ summary: {
+ "Cached" => 1,
+ "In a transaction" => 1
+ },
+ duration: '10000.00ms',
+ warnings: ["active-record duration: 10000.0 over 3000"],
+ details: contain_exactly(
+ a_hash_including(
+ start: be_a(Time),
+ cached: '',
+ transaction: '',
+ duration: 1000.0,
+ sql: 'SELECT * FROM users WHERE id = 10'
+ ),
+ a_hash_including(
+ start: be_a(Time),
+ cached: 'Cached',
+ transaction: '',
+ duration: 2000.0,
+ sql: 'SELECT * FROM users WHERE id = 10'
+ ),
+ a_hash_including(
+ start: be_a(Time),
+ cached: '',
+ transaction: 'In a transaction',
+ duration: 3000.0,
+ sql: 'UPDATE users SET admin = true WHERE id = 10'
+ ),
+ a_hash_including(
+ start: be_a(Time),
+ cached: '',
+ transaction: '',
+ duration: 4000.0,
+ sql: 'SELECT VERSION()'
+ )
+ )
+ )
+ end
+ end
+
+ context 'when database load balancing is enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_replica).and_return(:replica)
+ allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_primary_1).and_return(:primary)
+ allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_primary_2).and_return(:primary)
+ allow(Gitlab::Database::LoadBalancing).to receive(:db_role_for_connection).with(connection_unknown).and_return(nil)
end
- expect(subject.results).to match(
- calls: 3,
- summary: {
- "Cached" => 1,
- "In a transaction" => 1
- },
- duration: '6000.00ms',
- warnings: ["active-record duration: 6000.0 over 3000"],
- details: contain_exactly(
- a_hash_including(
- start: be_a(Time),
- cached: '',
- transaction: '',
- duration: 1000.0,
- sql: 'SELECT * FROM users WHERE id = 10'
- ),
- a_hash_including(
- start: be_a(Time),
- cached: 'Cached',
- transaction: '',
- duration: 2000.0,
- sql: 'SELECT * FROM users WHERE id = 10'
- ),
- a_hash_including(
- start: be_a(Time),
- cached: '',
- transaction: 'In a transaction',
- duration: 3000.0,
- sql: 'UPDATE users SET admin = true WHERE id = 10'
+ it 'includes db role data' do
+ Timecop.freeze(2021, 2, 23, 10, 0) do
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 1.second, '1', event_1)
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 2.seconds, '2', event_2)
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 3.seconds, '3', event_3)
+ ActiveSupport::Notifications.publish('sql.active_record', Time.current, Time.current + 4.seconds, '4', event_4)
+ end
+
+ expect(subject.results).to match(
+ calls: 4,
+ summary: {
+ "Cached" => 1,
+ "In a transaction" => 1,
+ "Primary" => 2,
+ "Replica" => 1,
+ "Unknown" => 1
+ },
+ duration: '10000.00ms',
+ warnings: ["active-record duration: 10000.0 over 3000"],
+ details: contain_exactly(
+ a_hash_including(
+ start: be_a(Time),
+ cached: '',
+ transaction: '',
+ duration: 1000.0,
+ sql: 'SELECT * FROM users WHERE id = 10',
+ db_role: 'Primary'
+ ),
+ a_hash_including(
+ start: be_a(Time),
+ cached: 'Cached',
+ transaction: '',
+ duration: 2000.0,
+ sql: 'SELECT * FROM users WHERE id = 10',
+ db_role: 'Replica'
+ ),
+ a_hash_including(
+ start: be_a(Time),
+ cached: '',
+ transaction: 'In a transaction',
+ duration: 3000.0,
+ sql: 'UPDATE users SET admin = true WHERE id = 10',
+ db_role: 'Primary'
+ ),
+ a_hash_including(
+ start: be_a(Time),
+ cached: '',
+ transaction: '',
+ duration: 4000.0,
+ sql: 'SELECT VERSION()',
+ db_role: 'Unknown'
+ )
)
)
- )
+ end
end
end
diff --git a/spec/lib/peek/views/memory_spec.rb b/spec/lib/peek/views/memory_spec.rb
new file mode 100644
index 00000000000..1f88aadfc54
--- /dev/null
+++ b/spec/lib/peek/views/memory_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Peek::Views::Memory, :request_store do
+ subject! { described_class.new }
+
+ before do
+ stub_memory_instrumentation
+ end
+
+ context 'with process_action.action_controller notification' do
+ it 'returns empty results when it has not yet fired' do
+ expect(subject.results).to eq({})
+ end
+
+ it 'returns memory instrumentation data when it has fired' do
+ publish_notification
+
+ expect(subject.results[:calls]).to eq('2 MB')
+ expect(subject.results[:details]).to all(have_key(:item_header))
+ expect(subject.results[:details]).to all(have_key(:item_content))
+ expect(subject.results[:summary]).to include('Objects allocated' => '200 k')
+ expect(subject.results[:summary]).to include('Allocator calls' => '500')
+ expect(subject.results[:summary]).to include('Large allocations' => '1 KB')
+ end
+ end
+
+ def stub_memory_instrumentation
+ start_memory = {
+ total_malloc_bytes: 1,
+ total_mallocs: 2,
+ total_allocated_objects: 3
+ }
+ allow(Gitlab::Memory::Instrumentation).to receive(:start_thread_memory_allocations).and_return(start_memory)
+ allow(Gitlab::Memory::Instrumentation).to receive(:measure_thread_memory_allocations).with(start_memory).and_return({
+ mem_total_bytes: 2_097_152,
+ mem_bytes: 1024,
+ mem_mallocs: 500,
+ mem_objects: 200_000
+ })
+ Gitlab::InstrumentationHelper.init_instrumentation_data
+ end
+
+ def publish_notification
+ headers = double
+ allow(headers).to receive(:env).and_return('action_dispatch.request_id': 'req-42')
+
+ ActiveSupport::Notifications.publish(
+ 'process_action.action_controller', Time.current - 1.second, Time.current, 'id', headers: headers
+ )
+ end
+end
diff --git a/spec/lib/prometheus/pid_provider_spec.rb b/spec/lib/prometheus/pid_provider_spec.rb
index f1d7f2ffff5..9fdca2662e7 100644
--- a/spec/lib/prometheus/pid_provider_spec.rb
+++ b/spec/lib/prometheus/pid_provider_spec.rb
@@ -28,66 +28,6 @@ RSpec.describe Prometheus::PidProvider do
end
end
- context 'when running in Unicorn mode' do
- before do
- allow(Gitlab::Runtime).to receive(:unicorn?).and_return(true)
-
- expect(described_class).to receive(:process_name)
- .at_least(:once)
- .and_return(process_name)
- end
-
- context 'when unicorn master is specified in process name' do
- context 'when running in Omnibus' do
- context 'before the process was renamed' do
- let(:process_name) { "/opt/gitlab/embedded/bin/unicorn"}
-
- it { is_expected.to eq 'unicorn_master' }
- end
-
- context 'after the process was renamed' do
- let(:process_name) { "unicorn master -D -E production -c /var/opt/gitlab/gitlab-rails/etc/unicorn.rb /opt/gitlab/embedded/service/gitlab-rails/config.ru" }
-
- it { is_expected.to eq 'unicorn_master' }
- end
- end
-
- context 'when in development env' do
- context 'before the process was renamed' do
- let(:process_name) { "path_to_bindir/bin/unicorn_rails"}
-
- it { is_expected.to eq 'unicorn_master' }
- end
-
- context 'after the process was renamed' do
- let(:process_name) { "unicorn_rails master -c /gitlab_dir/config/unicorn.rb -E development" }
-
- it { is_expected.to eq 'unicorn_master' }
- end
- end
- end
-
- context 'when unicorn worker id is specified in process name' do
- context 'when running in Omnibus' do
- let(:process_name) { "unicorn worker[1] -D -E production -c /var/opt/gitlab/gitlab-rails/etc/unicorn.rb /opt/gitlab/embedded/service/gitlab-rails/config.ru" }
-
- it { is_expected.to eq 'unicorn_1' }
- end
-
- context 'when in development env' do
- let(:process_name) { "unicorn_rails worker[1] -c gitlab_dir/config/unicorn.rb -E development" }
-
- it { is_expected.to eq 'unicorn_1' }
- end
- end
-
- context 'when no specified unicorn master or worker id in process name' do
- let(:process_name) { "bin/unknown_process"}
-
- it { is_expected.to eq "process_#{Process.pid}" }
- end
- end
-
context 'when running in Puma mode' do
before do
allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
diff --git a/spec/lib/security/ci_configuration/sast_build_action_spec.rb b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
index 6a1ea68fdd6..5337e8d9c39 100644
--- a/spec/lib/security/ci_configuration/sast_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/sast_build_action_spec.rb
@@ -12,7 +12,6 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
[
{ 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'test' },
{ 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 4 },
- { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
{ 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec, test, tests, tmp' }
] }
end
@@ -26,7 +25,6 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
[
{ 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'security' },
{ 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 1 },
- { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
{ 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' }
] }
end
@@ -146,8 +144,6 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
subject(:result) { described_class.new(auto_devops_enabled, params_with_analyzer_info, gitlab_ci_content).generate }
it 'writes SAST_EXCLUDED_ANALYZERS' do
- stub_const('Security::CiConfiguration::SastBuildAction::SAST_DEFAULT_ANALYZERS', 'bandit, brakeman, flawfinder')
-
expect(result[:content]).to eq(sast_yaml_with_no_variables_set_but_analyzers)
end
end
@@ -157,9 +153,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
subject(:result) { described_class.new(auto_devops_enabled, params_with_all_analyzers_enabled, gitlab_ci_content).generate }
- it 'does not write SAST_DEFAULT_ANALYZERS or SAST_EXCLUDED_ANALYZERS' do
- stub_const('Security::CiConfiguration::SastBuildAction::SAST_DEFAULT_ANALYZERS', 'brakeman, flawfinder')
-
+ it 'does not write SAST_EXCLUDED_ANALYZERS' do
expect(result[:content]).to eq(sast_yaml_with_no_variables_set)
end
end
@@ -176,7 +170,6 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
[
{ 'field' => 'stage', 'defaultValue' => 'test', 'value' => 'brand_new_stage' },
{ 'field' => 'SEARCH_MAX_DEPTH', 'defaultValue' => 4, 'value' => 5 },
- { 'field' => 'SAST_ANALYZER_IMAGE_TAG', 'defaultValue' => 2, 'value' => 2 },
{ 'field' => 'SAST_EXCLUDED_PATHS', 'defaultValue' => 'spec, test, tests, tmp', 'value' => 'spec,docs' }
] }
end
@@ -227,27 +220,27 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
def existing_gitlab_ci_and_template_array_without_sast
{ "stages" => %w(test security),
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "existing.yml" }] }
end
def existing_gitlab_ci_and_single_template_with_sast_and_default_stage
{ "stages" => %w(test),
"variables" => { "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "test" },
"include" => { "template" => "Security/SAST.gitlab-ci.yml" } }
end
def existing_gitlab_ci_and_single_template_without_sast
{ "stages" => %w(test security),
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "localhost:5000/analyzers" },
- "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => { "template" => "existing.yml" } }
end
def existing_gitlab_ci_with_no_variables
{ "stages" => %w(test security),
- "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
@@ -267,7 +260,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
def existing_gitlab_ci
{ "stages" => %w(test security),
"variables" => { "RANDOM" => "make sure this persists", "SECURE_ANALYZERS_PREFIX" => "bad_prefix" },
- "sast" => { "variables" => { "SAST_ANALYZER_IMAGE_TAG" => 2, "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
+ "sast" => { "variables" => { "SEARCH_MAX_DEPTH" => 1 }, "stage" => "security" },
"include" => [{ "template" => "Security/SAST.gitlab-ci.yml" }] }
end
end
@@ -319,20 +312,6 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
end
end
- describe 'Security::CiConfiguration::SastBuildAction::SAST_DEFAULT_ANALYZERS' do
- subject(:variable) {Security::CiConfiguration::SastBuildAction::SAST_DEFAULT_ANALYZERS}
-
- it 'is sorted alphabetically' do
- sorted_variable = Security::CiConfiguration::SastBuildAction::SAST_DEFAULT_ANALYZERS
- .split(',')
- .map(&:strip)
- .sort
- .join(', ')
-
- expect(variable).to eq(sorted_variable)
- end
- end
-
# stubbing this method allows this spec file to use fast_spec_helper
def fast_auto_devops_stages
auto_devops_template = YAML.safe_load( File.read('lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml') )
@@ -345,7 +324,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
sast:
@@ -364,7 +343,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
sast:
@@ -380,7 +359,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
- security
@@ -402,7 +381,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- build
- test
@@ -437,7 +416,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
- security
@@ -461,7 +440,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
- security
@@ -483,7 +462,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
- security
@@ -506,7 +485,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
- security
@@ -529,7 +508,7 @@ RSpec.describe Security::CiConfiguration::SastBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
- security
diff --git a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
index 31854fcf3a7..f6181c6ef7a 100644
--- a/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
+++ b/spec/lib/security/ci_configuration/secret_detection_build_action_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
- security
@@ -63,7 +63,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
stages:
- test
variables:
@@ -112,7 +112,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
include:
- template: Security/Secret-Detection.gitlab-ci.yml
CI_YML
@@ -132,7 +132,7 @@ RSpec.describe Security::CiConfiguration::SecretDetectionBuildAction do
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
# Note that environment variables can be set in several places
- # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables
+ # See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
include:
- template: Auto-DevOps.gitlab-ci.yml
CI_YML
diff --git a/spec/lib/serializers/json_spec.rb b/spec/lib/serializers/json_spec.rb
index 7054f98a719..0c1801b34f9 100644
--- a/spec/lib/serializers/json_spec.rb
+++ b/spec/lib/serializers/json_spec.rb
@@ -2,7 +2,7 @@
require 'fast_spec_helper'
-RSpec.describe Serializers::JSON do
+RSpec.describe Serializers::Json do
describe '.dump' do
let(:obj) { { key: "value" } }
diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb
index 7dcf1940442..95009aa063f 100644
--- a/spec/lib/sidebars/menu_spec.rb
+++ b/spec/lib/sidebars/menu_spec.rb
@@ -144,4 +144,50 @@ RSpec.describe Sidebars::Menu do
end
end
end
+
+ describe '#container_html_options' do
+ before do
+ allow(menu).to receive(:title).and_return('Foo Menu')
+ end
+
+ context 'when menu can be rendered' do
+ before do
+ allow(menu).to receive(:render?).and_return(true)
+ end
+
+ context 'when menu has renderable items' do
+ before do
+ menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: 'bar' }))
+ end
+
+ it 'contains the special class' do
+ expect(menu.container_html_options[:class]).to eq 'has-sub-items'
+ end
+
+ context 'when menu already has other classes' do
+ it 'appends special class' do
+ allow(menu).to receive(:extra_container_html_options).and_return(class: 'foo')
+
+ expect(menu.container_html_options[:class]).to eq 'foo has-sub-items'
+ end
+ end
+ end
+
+ context 'when menu does not have renderable items' do
+ it 'does not contain the special class' do
+ expect(menu.container_html_options[:class]).to be_nil
+ end
+ end
+ end
+
+ context 'when menu cannot be rendered' do
+ before do
+ allow(menu).to receive(:render?).and_return(false)
+ end
+
+ it 'does not contain special class' do
+ expect(menu.container_html_options[:class]).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
index 0ecb328efd1..e3ae3add4fd 100644
--- a/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/confluence_menu_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe Sidebars::Projects::Menus::ConfluenceMenu do
end
context 'when Confluence integration is present' do
- let!(:confluence) { create(:confluence_service, project: project, active: active) }
+ let!(:confluence) { create(:confluence_integration, project: project, active: active) }
context 'when integration is disabled' do
let(:active) { false }
diff --git a/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb b/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
index 5d62eebca1c..0585eb2254c 100644
--- a/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/external_issue_tracker_menu_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Sidebars::Projects::Menus::ExternalIssueTrackerMenu do
end
context 'when active external issue tracker' do
- let(:external_issue_tracker) { build(:custom_issue_tracker_service, project: project) }
+ let(:external_issue_tracker) { build(:custom_issue_tracker_integration, project: project) }
context 'is present' do
it 'returns true' do
diff --git a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
index ef5ae550551..231e5a850c2 100644
--- a/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb
@@ -27,7 +27,6 @@ RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do
{
class: 'home',
data: {
- track_action: 'click_menu',
track_property: tracking_category,
track_label: 'learn_gitlab'
}
diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
index 731dd5eca23..cc4760e69e5 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
- let(:project) { build(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
index b50bf0f4bf1..748796bc7ee 100644
--- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb
@@ -8,6 +8,20 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+ describe '#container_html_options' do
+ subject { described_class.new(context).container_html_options }
+
+ specify { is_expected.to match(hash_including(class: 'shortcuts-project-information has-sub-items')) }
+
+ context 'when feature flag :sidebar_refactor is disabled' do
+ before do
+ stub_feature_flags(sidebar_refactor: false)
+ end
+
+ specify { is_expected.to match(hash_including(class: 'shortcuts-project rspec-project-link has-sub-items')) }
+ end
+ end
+
describe 'Menu Items' do
subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } }
diff --git a/spec/lib/sidebars/projects/menus/scope_menu_spec.rb b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
new file mode 100644
index 00000000000..f84d458a2e1
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/scope_menu_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::ScopeMenu do
+ let(:project) { build(:project) }
+ let(:user) { project.owner }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
+
+ describe '#container_html_options' do
+ subject { described_class.new(context).container_html_options }
+
+ specify { is_expected.to match(hash_including(class: 'shortcuts-project rspec-project-link')) }
+
+ context 'when feature flag :sidebar_refactor is disabled' do
+ before do
+ stub_feature_flags(sidebar_refactor: false)
+ end
+
+ specify { is_expected.to eq(aria: { label: project.name }) }
+ end
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
new file mode 100644
index 00000000000..6e84beeb274
--- /dev/null
+++ b/spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Sidebars::Projects::Menus::SecurityComplianceMenu do
+ let_it_be(:project) { create(:project) }
+
+ let(:user) { project.owner }
+ let(:show_promotions) { true }
+ let(:show_discover_project_security) { true }
+ let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, show_promotions: show_promotions, show_discover_project_security: show_discover_project_security) }
+
+ describe 'render?' do
+ subject { described_class.new(context).render? }
+
+ context 'when user is not authenticated' do
+ let(:user) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when user is authenticated' do
+ context 'when the Security & Compliance is disabled' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :access_security_and_compliance, project).and_return(false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when the Security & Compliance is not disabled' do
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+end
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 88f2df6cd84..6817f0e6ed6 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
- let(:project) { build(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
diff --git a/spec/lib/sidebars/projects/panel_spec.rb b/spec/lib/sidebars/projects/panel_spec.rb
index 51d37bf69ea..2e79ced7039 100644
--- a/spec/lib/sidebars/projects/panel_spec.rb
+++ b/spec/lib/sidebars/projects/panel_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Sidebars::Projects::Panel do
subject { described_class.new(context).instance_variable_get(:@menus) }
context 'when integration is present and active' do
- let_it_be(:confluence) { create(:confluence_service, active: true) }
+ let_it_be(:confluence) { create(:confluence_integration, active: true) }
let(:project) { confluence.project }
diff --git a/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb b/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb
index 14bf9d61ab5..4536618ba61 100644
--- a/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb
+++ b/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb
@@ -1,13 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'rake_helper'
-
-RSpec.describe SystemCheck::App::HashedStorageAllProjectsCheck do
- before do
- silence_output
- end
+RSpec.describe SystemCheck::App::HashedStorageAllProjectsCheck, :silence_stdout do
describe '#check?' do
it 'fails when at least one project is in legacy storage' do
create(:project, :legacy_storage)
diff --git a/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb b/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb
index 32a2f409858..36c6f0a2be1 100644
--- a/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb
+++ b/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb
@@ -1,13 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'rake_helper'
-
-RSpec.describe SystemCheck::App::HashedStorageEnabledCheck do
- before do
- silence_output
- end
+RSpec.describe SystemCheck::App::HashedStorageEnabledCheck, :silence_stdout do
describe '#check?' do
it 'fails when hashed storage is disabled' do
stub_application_setting(hashed_storage_enabled: false)
diff --git a/spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb b/spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb
new file mode 100644
index 00000000000..d7a77a84472
--- /dev/null
+++ b/spec/lib/system_check/incoming_email/imap_authentication_check_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+MAIL_ROOM_CONFIG_ENABLED_SAMPLE =
+ ":mailboxes:\n"\
+ " \n"\
+ " -\n"\
+ " :host: \"gitlab.example.com\"\n"\
+ " :port: 143\n"\
+ ""
+
+RSpec.describe SystemCheck::IncomingEmail::ImapAuthenticationCheck do
+ subject(:system_check) { described_class.new }
+
+ describe '#load_config' do
+ subject { system_check.send(:load_config) }
+
+ context 'returns no mailbox configurations with mailroom default configuration' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'returns an array of mailbox configurations with mailroom configured' do
+ before do
+ allow(File).to receive(:read).and_return(MAIL_ROOM_CONFIG_ENABLED_SAMPLE)
+ end
+
+ it { is_expected.to eq([{ host: "gitlab.example.com", port: 143 }]) }
+ end
+ end
+end
diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb
index 795dfde9029..e764c2313cd 100644
--- a/spec/lib/system_check/orphans/namespace_check_spec.rb
+++ b/spec/lib/system_check/orphans/namespace_check_spec.rb
@@ -1,15 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'rake_helper'
-RSpec.describe SystemCheck::Orphans::NamespaceCheck do
+RSpec.describe SystemCheck::Orphans::NamespaceCheck, :silence_stdout do
let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } }
before do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces)
- silence_output
end
describe '#multi_check' do
diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb
index 2ab30f4802d..91b48969cc1 100644
--- a/spec/lib/system_check/orphans/repository_check_spec.rb
+++ b/spec/lib/system_check/orphans/repository_check_spec.rb
@@ -1,16 +1,14 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'rake_helper'
-RSpec.describe SystemCheck::Orphans::RepositoryCheck do
+RSpec.describe SystemCheck::Orphans::RepositoryCheck, :silence_stdout do
let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } }
before do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces)
allow(subject).to receive(:fetch_disk_repositories).and_return(disk_repositories)
- # silence_output
end
describe '#multi_check' do
diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb
index c9a09d86e32..96d93146d5e 100644
--- a/spec/lib/system_check/simple_executor_spec.rb
+++ b/spec/lib/system_check/simple_executor_spec.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'rake_helper'
-RSpec.describe SystemCheck::SimpleExecutor do
+RSpec.describe SystemCheck::SimpleExecutor, :silence_stdout do
before do
stub_const('SimpleCheck', Class.new(SystemCheck::BaseCheck))
stub_const('OtherCheck', Class.new(SystemCheck::BaseCheck))
@@ -154,8 +153,6 @@ RSpec.describe SystemCheck::SimpleExecutor do
describe '#execute' do
before do
- silence_output
-
subject << SimpleCheck
subject << OtherCheck
end
diff --git a/spec/lib/system_check_spec.rb b/spec/lib/system_check_spec.rb
index 5ddb2741c4a..8b32eccead6 100644
--- a/spec/lib/system_check_spec.rb
+++ b/spec/lib/system_check_spec.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
-require 'rake_helper'
-RSpec.describe SystemCheck do
+RSpec.describe SystemCheck, :silence_stdout do
before do
stub_const('SimpleCheck', Class.new(SystemCheck::BaseCheck))
stub_const('OtherCheck', Class.new(SystemCheck::BaseCheck))
@@ -19,8 +18,6 @@ RSpec.describe SystemCheck do
false
end
end
-
- silence_output
end
describe '.run' do
diff --git a/spec/mailers/emails/in_product_marketing_spec.rb b/spec/mailers/emails/in_product_marketing_spec.rb
index 3d17e16ef48..74354630ade 100644
--- a/spec/mailers/emails/in_product_marketing_spec.rb
+++ b/spec/mailers/emails/in_product_marketing_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe Emails::InProductMarketing do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
+ let!(:onboarding_progress) { create(:onboarding_progress, namespace: group) }
+
describe '#in_product_marketing_email' do
using RSpec::Parameterized::TableSyntax
@@ -45,29 +47,35 @@ RSpec.describe Emails::InProductMarketing do
end
where(:track, :series) do
- :create | 0
- :create | 1
- :create | 2
- :verify | 0
- :verify | 1
- :verify | 2
- :trial | 0
- :trial | 1
- :trial | 2
- :team | 0
- :team | 1
- :team | 2
+ :create | 0
+ :create | 1
+ :create | 2
+ :verify | 0
+ :verify | 1
+ :verify | 2
+ :trial | 0
+ :trial | 1
+ :trial | 2
+ :team | 0
+ :team | 1
+ :team | 2
+ :experience | 0
end
with_them do
it 'has the correct subject and content' do
- message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, series: series)
+ message = Gitlab::Email::Message::InProductMarketing.for(track).new(group: group, user: user, series: series)
aggregate_failures do
is_expected.to have_subject(message.subject_line)
is_expected.to have_body_text(message.title)
is_expected.to have_body_text(message.subtitle)
- is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
+
+ if track == :experience
+ is_expected.to have_body_text(CGI.unescapeHTML(message.feedback_link(1)))
+ else
+ is_expected.to have_body_text(CGI.unescapeHTML(message.cta_link))
+ end
end
end
end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 0ca202aa7be..365ca892bb1 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -264,7 +264,7 @@ RSpec.describe Emails::Profile do
include_examples 'valid use case'
it_behaves_like 'has the correct subject', /Your SSH key has expired/
- it_behaves_like 'has the correct body text', /Your SSH keys with the following fingerprints have expired/
+ it_behaves_like 'has the correct body text', /SSH keys with the following fingerprints have expired/
end
context 'when invalid' do
@@ -291,7 +291,7 @@ RSpec.describe Emails::Profile do
include_examples 'valid use case'
it_behaves_like 'has the correct subject', /Your SSH key is expiring soon/
- it_behaves_like 'has the correct body text', /Your SSH keys with the following fingerprints are scheduled to expire soon/
+ it_behaves_like 'has the correct body text', /SSH keys with the following fingerprints are scheduled to expire soon/
end
context 'when invalid' do
@@ -337,7 +337,7 @@ RSpec.describe Emails::Profile do
end
it 'mentioned the time' do
- is_expected.to have_body_text current_time.strftime('%Y-%m-%d %l:%M:%S %p %Z')
+ is_expected.to have_body_text current_time.strftime('%Y-%m-%d %H:%M:%S %Z')
end
it 'includes a link to the change password documentation' do
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
index 57fa990d399..995e6c006cd 100644
--- a/spec/mailers/emails/service_desk_spec.rb
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -115,6 +115,16 @@ RSpec.describe Emails::ServiceDesk do
end
end
+ shared_examples 'notification with metric event' do |event_type|
+ it 'adds metric event' do
+ metric_transaction = double('Gitlab::Metrics::WebTransaction', increment: true, observe: true)
+ allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
+ expect(metric_transaction).to receive(:add_event).with(event_type)
+
+ subject.content_type
+ end
+ end
+
describe '.service_desk_thank_you_email' do
let_it_be(:reply_in_subject) { true }
let_it_be(:default_text) do
@@ -124,6 +134,7 @@ RSpec.describe Emails::ServiceDesk do
subject { ServiceEmailClass.service_desk_thank_you_email(issue.id) }
it_behaves_like 'read template from repository', 'thank_you'
+ it_behaves_like 'notification with metric event', :service_desk_thank_you_email
context 'handling template markdown' do
context 'with a simple text' do
@@ -164,6 +175,7 @@ RSpec.describe Emails::ServiceDesk do
subject { ServiceEmailClass.service_desk_new_note_email(issue.id, note.id, email) }
it_behaves_like 'read template from repository', 'new_note'
+ it_behaves_like 'notification with metric event', :service_desk_new_note_email
context 'handling template markdown' do
context 'with a simple text' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index b073b647532..8ee88776107 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -790,7 +790,7 @@ RSpec.describe Notify do
it_behaves_like 'appearance header and footer not enabled'
it_behaves_like 'does not render a manage notifications link'
- context 'when there is an inviter' do
+ context 'when there is an inviter', :aggregate_failures do
it 'contains all the useful information' do
is_expected.to have_subject "#{inviter.name} invited you to join GitLab"
is_expected.to have_body_text project.full_name
@@ -799,21 +799,16 @@ RSpec.describe Notify do
is_expected.to have_link('Join now', href: invite_url(project_member.invite_token, invite_type: Members::InviteEmailExperiment::INVITE_TYPE))
end
- it 'contains invite link for the avatar' do
- stub_experiments('members/invite_email': :avatar)
+ it 'contains invite link for the group activity' do
+ stub_experiments('members/invite_email': :activity)
+ is_expected.to have_content("#{inviter.name} invited you to join the")
+ is_expected.to have_content('Project details')
+ is_expected.to have_content("What's it about?")
is_expected.not_to have_content('You are invited!')
is_expected.not_to have_body_text 'What is a GitLab'
end
- it 'contains invite link for the avatar' do
- stub_experiments('members/invite_email': :permission_info)
-
- is_expected.not_to have_content('You are invited!')
- is_expected.to have_body_text 'What is a GitLab'
- is_expected.to have_body_text 'What can I do with'
- end
-
it 'has invite link for the control group' do
stub_experiments('members/invite_email': :control)
@@ -821,7 +816,7 @@ RSpec.describe Notify do
end
end
- context 'when there is no inviter' do
+ context 'when there is no inviter', :aggregate_failures do
let(:inviter) { nil }
it 'contains all the useful information' do
@@ -831,6 +826,19 @@ RSpec.describe Notify do
is_expected.to have_body_text project_member.invite_token
end
end
+
+ context 'when on gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:dev_env_or_com?).and_return(true)
+ end
+
+ it 'has custom headers' do
+ aggregate_failures do
+ expect(subject).to have_header('X-Mailgun-Tag', 'invite_email')
+ expect(subject).to have_header('X-Mailgun-Variables', { 'invite_token' => project_member.invite_token }.to_json)
+ end
+ end
+ end
end
describe 'project invitation accepted' do
diff --git a/spec/migrations/20190924152703_migrate_issue_trackers_data_spec.rb b/spec/migrations/20190924152703_migrate_issue_trackers_data_spec.rb
index 47f85df01ac..2999332509a 100644
--- a/spec/migrations/20190924152703_migrate_issue_trackers_data_spec.rb
+++ b/spec/migrations/20190924152703_migrate_issue_trackers_data_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe MigrateIssueTrackersData do
services.create!(type: 'JiraService', properties: nil, category: 'issue_tracker')
end
- let!(:bugzilla_service) do
+ let!(:bugzilla_integration) do
services.create!(type: 'BugzillaService', properties: properties, category: 'issue_tracker')
end
@@ -55,7 +55,7 @@ RSpec.describe MigrateIssueTrackersData do
freeze_time do
migrate!
- expect(migration_name).to be_scheduled_delayed_migration(3.minutes, jira_service.id, bugzilla_service.id)
+ expect(migration_name).to be_scheduled_delayed_migration(3.minutes, jira_service.id, bugzilla_integration.id)
expect(migration_name).to be_scheduled_delayed_migration(6.minutes, youtrack_service.id, gitlab_service.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
diff --git a/spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb b/spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb
index 51f798220c3..222a000c134 100644
--- a/spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb
+++ b/spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191125114345_add_admin_mode_protected_path.rb')
+require_migration!('add_admin_mode_protected_path')
RSpec.describe AddAdminModeProtectedPath do
subject(:migration) { described_class.new }
diff --git a/spec/migrations/20191204114127_delete_legacy_triggers_spec.rb b/spec/migrations/20191204114127_delete_legacy_triggers_spec.rb
index 07c69872497..aba3a902888 100644
--- a/spec/migrations/20191204114127_delete_legacy_triggers_spec.rb
+++ b/spec/migrations/20191204114127_delete_legacy_triggers_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191204114127_delete_legacy_triggers.rb')
+require_migration!('delete_legacy_triggers')
RSpec.describe DeleteLegacyTriggers, schema: 2019_11_25_140458 do
let(:ci_trigger_table) { table(:ci_triggers) }
diff --git a/spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb b/spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb
index 1377d5e6ba0..fff0745e8af 100644
--- a/spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb
+++ b/spec/migrations/20200107172020_add_timestamp_softwarelicensespolicy_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200107172020_add_timestamp_softwarelicensespolicy.rb')
+require_migration!('add_timestamp_softwarelicensespolicy')
RSpec.describe AddTimestampSoftwarelicensespolicy do
let(:software_licenses_policy) { table(:software_license_policies) }
diff --git a/spec/migrations/20200122123016_backfill_project_settings_spec.rb b/spec/migrations/20200122123016_backfill_project_settings_spec.rb
index 9ae492a36aa..7fc8eb0e368 100644
--- a/spec/migrations/20200122123016_backfill_project_settings_spec.rb
+++ b/spec/migrations/20200122123016_backfill_project_settings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200122123016_backfill_project_settings.rb')
+require_migration!('backfill_project_settings')
RSpec.describe BackfillProjectSettings, :sidekiq, schema: 20200114113341 do
let(:projects) { table(:projects) }
diff --git a/spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb b/spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb
index a6ef0b29461..9000d4b7fef 100644
--- a/spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb
+++ b/spec/migrations/20200123155929_remove_invalid_jira_data_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200123155929_remove_invalid_jira_data.rb')
+require_migration!('remove_invalid_jira_data')
RSpec.describe RemoveInvalidJiraData do
let(:jira_tracker_data) { table(:jira_tracker_data) }
diff --git a/spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb b/spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb
index 9ddf188b15e..1d3476d6d61 100644
--- a/spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb
+++ b/spec/migrations/20200127090233_remove_invalid_issue_tracker_data_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200127090233_remove_invalid_issue_tracker_data.rb')
+require_migration!('remove_invalid_issue_tracker_data')
RSpec.describe RemoveInvalidIssueTrackerData do
let(:issue_tracker_data) { table(:issue_tracker_data) }
diff --git a/spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb b/spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb
index 9947718d11b..5516e2af3f1 100644
--- a/spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb
+++ b/spec/migrations/20200130145430_reschedule_migrate_issue_trackers_data_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200130145430_reschedule_migrate_issue_trackers_data.rb')
+require_migration!('reschedule_migrate_issue_trackers_data')
RSpec.describe RescheduleMigrateIssueTrackersData do
let(:services) { table(:services) }
@@ -22,7 +22,7 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
services.create!(id: 11, type: 'JiraService', properties: nil, category: 'issue_tracker')
end
- let!(:bugzilla_service) do
+ let!(:bugzilla_integration) do
services.create!(id: 12, type: 'BugzillaService', properties: properties, category: 'issue_tracker')
end
@@ -56,7 +56,7 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
freeze_time do
migrate!
- expect(migration_name).to be_scheduled_delayed_migration(3.minutes, jira_service.id, bugzilla_service.id)
+ expect(migration_name).to be_scheduled_delayed_migration(3.minutes, jira_service.id, bugzilla_integration.id)
expect(migration_name).to be_scheduled_delayed_migration(6.minutes, youtrack_service.id, gitlab_service.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
@@ -70,7 +70,7 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
let!(:valid_issue_tracker_data) do
issue_tracker_data.create!(
- service_id: bugzilla_service.id,
+ service_id: bugzilla_integration.id,
encrypted_issues_url: 'http://url.com',
encrypted_issues_url_iv: 'somevalue'
)
@@ -78,7 +78,7 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
let!(:invalid_issue_tracker_data) do
issue_tracker_data.create!(
- service_id: bugzilla_service.id,
+ service_id: bugzilla_integration.id,
encrypted_issues_url: 'http:url.com',
encrypted_issues_url_iv: nil
)
@@ -86,7 +86,7 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
let!(:valid_jira_tracker_data) do
jira_tracker_data.create!(
- service_id: bugzilla_service.id,
+ service_id: bugzilla_integration.id,
encrypted_url: 'http://url.com',
encrypted_url_iv: 'somevalue'
)
@@ -94,7 +94,7 @@ RSpec.describe RescheduleMigrateIssueTrackersData do
let!(:invalid_jira_tracker_data) do
jira_tracker_data.create!(
- service_id: bugzilla_service.id,
+ service_id: bugzilla_integration.id,
encrypted_url: 'http://url.com',
encrypted_url_iv: nil
)
diff --git a/spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb b/spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb
index f16e7b483e9..6b1126ca53e 100644
--- a/spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb
+++ b/spec/migrations/20200313203550_remove_orphaned_chat_names_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200313203550_remove_orphaned_chat_names.rb')
+require_migration!('remove_orphaned_chat_names')
RSpec.describe RemoveOrphanedChatNames, schema: 20200313202430 do
let(:projects) { table(:projects) }
diff --git a/spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb b/spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb
index 9950cc23889..c6a512a1ec9 100644
--- a/spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb
+++ b/spec/migrations/20200406102120_backfill_deployment_clusters_from_deployments_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200406102120_backfill_deployment_clusters_from_deployments.rb')
+require_migration!('backfill_deployment_clusters_from_deployments')
RSpec.describe BackfillDeploymentClustersFromDeployments, :migration, :sidekiq, schema: 20200227140242 do
describe '#up' do
diff --git a/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb b/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb
index e42a448a01e..e712e555b70 100644
--- a/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb
+++ b/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200511145545_change_variable_interpolation_format_in_common_metrics')
+require_migration!('change_variable_interpolation_format_in_common_metrics')
RSpec.describe ChangeVariableInterpolationFormatInCommonMetrics, :migration do
let(:prometheus_metrics) { table(:prometheus_metrics) }
diff --git a/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb b/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb
index 9d3851ed5b0..f16026884f5 100644
--- a/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb
+++ b/spec/migrations/20200526115436_dedup_mr_metrics_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200526115436_dedup_mr_metrics')
+require_migration!('dedup_mr_metrics')
RSpec.describe DedupMrMetrics, :migration, schema: 20200526013844 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb b/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb
index 9f26b698158..9b72559234e 100644
--- a/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb
+++ b/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb')
+require_migration!('update_index_approval_rule_name_for_code_owners_rule_type')
RSpec.describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do
let(:migration) { described_class.new }
diff --git a/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb b/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb
index 9ff88009d8a..c9f7a66a0b9 100644
--- a/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb
+++ b/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200703125016_backfill_namespace_settings.rb')
+require_migration!('backfill_namespace_settings')
RSpec.describe BackfillNamespaceSettings, :sidekiq, schema: 20200703124823 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb b/spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb
index 0068571ad0d..121b1729dd2 100644
--- a/spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb
+++ b/spec/migrations/20200706035141_adjust_unique_index_alert_management_alerts_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200706035141_adjust_unique_index_alert_management_alerts.rb')
+require_migration!('adjust_unique_index_alert_management_alerts')
RSpec.describe AdjustUniqueIndexAlertManagementAlerts, :migration do
let(:migration) { described_class.new }
diff --git a/spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb b/spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb
index f9a56bf649d..761168ae609 100644
--- a/spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb
+++ b/spec/migrations/20200728080250_replace_unique_index_on_cycle_analytics_stages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200728080250_replace_unique_index_on_cycle_analytics_stages.rb')
+require_migration!('replace_unique_index_on_cycle_analytics_stages')
RSpec.describe ReplaceUniqueIndexOnCycleAnalyticsStages, :migration, schema: 20200728080250 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb b/spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb
index e12519e15b8..5c65d45c6e0 100644
--- a/spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb
+++ b/spec/migrations/20200728182311_add_o_auth_paths_to_protected_paths_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200728182311_add_o_auth_paths_to_protected_paths.rb')
+require_migration!('add_o_auth_paths_to_protected_paths')
RSpec.describe AddOAuthPathsToProtectedPaths do
subject(:migration) { described_class.new }
diff --git a/spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb b/spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb
index a821e4a43df..d166ff3617b 100644
--- a/spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb
+++ b/spec/migrations/20200811130433_create_missing_vulnerabilities_issue_links_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200811130433_create_missing_vulnerabilities_issue_links.rb')
+require_migration!('create_missing_vulnerabilities_issue_links')
RSpec.describe CreateMissingVulnerabilitiesIssueLinks, :migration do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb b/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb
index 20ba2fbccea..69f7525d265 100644
--- a/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb
+++ b/spec/migrations/20200915044225_schedule_migration_to_hashed_storage_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200915044225_schedule_migration_to_hashed_storage.rb')
+require_migration!('schedule_migration_to_hashed_storage')
RSpec.describe ScheduleMigrationToHashedStorage, :sidekiq do
describe '#up' do
diff --git a/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb b/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb
index 1a618712b32..34bd8f1c869 100644
--- a/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb
+++ b/spec/migrations/20200929052138_create_initial_versions_for_pre_versioning_terraform_states_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200929052138_create_initial_versions_for_pre_versioning_terraform_states.rb')
+require_migration!('create_initial_versions_for_pre_versioning_terraform_states')
RSpec.describe CreateInitialVersionsForPreVersioningTerraformStates do
let(:namespace) { table(:namespaces).create!(name: 'terraform', path: 'terraform') }
diff --git a/spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb b/spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb
index 134bea6b666..ef9bc5788c1 100644
--- a/spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb
+++ b/spec/migrations/20201014205300_drop_backfill_jira_tracker_deployment_type_jobs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201014205300_drop_backfill_jira_tracker_deployment_type_jobs.rb')
+require_migration!('drop_backfill_jira_tracker_deployment_type_jobs')
RSpec.describe DropBackfillJiraTrackerDeploymentTypeJobs, :sidekiq, :redis, schema: 2020_10_14_205300 do
subject(:migration) { described_class.new }
diff --git a/spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb b/spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb
index c1fbde69100..f9f6cd9589c 100644
--- a/spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb
+++ b/spec/migrations/20201027002551_migrate_services_to_http_integrations_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201027002551_migrate_services_to_http_integrations.rb')
+require_migration!('migrate_services_to_http_integrations')
RSpec.describe MigrateServicesToHttpIntegrations do
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
diff --git a/spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb b/spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb
index 658b26b1c49..0746ad7e44f 100644
--- a/spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb
+++ b/spec/migrations/20201028182809_backfill_jira_tracker_deployment_type2_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201028182809_backfill_jira_tracker_deployment_type2.rb')
+require_migration!('backfill_jira_tracker_deployment_type2')
RSpec.describe BackfillJiraTrackerDeploymentType2, :sidekiq, schema: 20201028182809 do
let(:services) { table(:services) }
diff --git a/spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb b/spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb
index 8563114c9f9..7a79406ac80 100644
--- a/spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb
+++ b/spec/migrations/20201110161542_cleanup_transfered_projects_shared_runners_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201110161542_cleanup_transfered_projects_shared_runners.rb')
+require_migration!('cleanup_transfered_projects_shared_runners')
RSpec.describe CleanupTransferedProjectsSharedRunners, :sidekiq, schema: 20201110161542 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb b/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb
index ff27bdcf12d..92a716c355b 100644
--- a/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb
+++ b/spec/migrations/20201112130710_schedule_remove_duplicate_vulnerabilities_findings_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201112130710_schedule_remove_duplicate_vulnerabilities_findings.rb')
+require_migration!('schedule_remove_duplicate_vulnerabilities_findings')
RSpec.describe ScheduleRemoveDuplicateVulnerabilitiesFindings, :migration do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb b/spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb
index fce32be4683..dda919d70d9 100644
--- a/spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb
+++ b/spec/migrations/20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201112130715_schedule_recalculate_uuid_on_vulnerabilities_occurrences.rb')
+require_migration!('schedule_recalculate_uuid_on_vulnerabilities_occurrences')
RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences, :migration do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb b/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
index 9a57d9bf84d..289416c22cf 100644
--- a/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
+++ b/spec/migrations/20210112143418_remove_duplicate_services2_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210112143418_remove_duplicate_services2.rb')
+require_migration!('remove_duplicate_services2')
RSpec.describe RemoveDuplicateServices2 do
let_it_be(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb b/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
index 33b2e009c8d..469dbb4f946 100644
--- a/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
+++ b/spec/migrations/20210119122354_alter_vsa_issue_first_mentioned_in_commit_value_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210119122354_alter_vsa_issue_first_mentioned_in_commit_value.rb')
+require_migration!('alter_vsa_issue_first_mentioned_in_commit_value')
RSpec.describe AlterVsaIssueFirstMentionedInCommitValue, schema: 20210114033715 do
let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
diff --git a/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb b/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
index 7e351617ea3..cb48df20d58 100644
--- a/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
+++ b/spec/migrations/20210205174154_remove_bad_dependency_proxy_manifests_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210205174154_remove_bad_dependency_proxy_manifests.rb')
+require_migration!('remove_bad_dependency_proxy_manifests')
RSpec.describe RemoveBadDependencyProxyManifests, schema: 20210128140157 do
let_it_be(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb b/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
index 52678111b48..1932bc00cee 100644
--- a/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
+++ b/spec/migrations/20210210093901_backfill_updated_at_after_repository_storage_move_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210210093901_backfill_updated_at_after_repository_storage_move.rb')
+require_migration!('backfill_updated_at_after_repository_storage_move')
RSpec.describe BackfillUpdatedAtAfterRepositoryStorageMove, :sidekiq do
let_it_be(:projects) { table(:projects) }
diff --git a/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb b/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
index 043884eb7b2..6068df85e2e 100644
--- a/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
+++ b/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210226141517_dedup_issue_metrics.rb')
+require_migration!('dedup_issue_metrics')
RSpec.describe DedupIssueMetrics, :migration, schema: 20210205104425 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb b/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
index e1727cb2a1c..94ed2320c50 100644
--- a/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
+++ b/spec/migrations/20210406144743_backfill_total_tuple_count_for_batched_migrations_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210406144743_backfill_total_tuple_count_for_batched_migrations.rb')
+require_migration!('backfill_total_tuple_count_for_batched_migrations')
RSpec.describe BackfillTotalTupleCountForBatchedMigrations, :migration, schema: 20210406140057 do
let_it_be(:table_name) { 'projects' }
diff --git a/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb b/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
index 4f36a95f9cf..78b6a71c609 100644
--- a/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
+++ b/spec/migrations/20210413132500_reschedule_artifact_expiry_backfill_again_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210413132500_reschedule_artifact_expiry_backfill_again.rb')
+require_migration!('reschedule_artifact_expiry_backfill_again')
RSpec.describe RescheduleArtifactExpiryBackfillAgain, :migration do
let(:migration_class) { Gitlab::BackgroundMigration::BackfillArtifactExpiryDate }
diff --git a/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
new file mode 100644
index 00000000000..ea0a16212dd
--- /dev/null
+++ b/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('schedule_update_jira_tracker_data_deployment_type_based_on_url')
+
+RSpec.describe ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration do
+ let(:services_table) { table(:services) }
+ let(:service_jira_cloud) { services_table.create!(id: 1, type: 'JiraService') }
+ let(:service_jira_server) { services_table.create!(id: 2, type: 'JiraService') }
+
+ before do
+ jira_tracker_data = Class.new(ApplicationRecord) do
+ self.table_name = 'jira_tracker_data'
+
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :username, encryption_options
+ attr_encrypted :password, encryption_options
+ end
+
+ stub_const('JiraTrackerData', jira_tracker_data)
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: 0) }
+ let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: 0) }
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migration' do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(tracker_data_cloud.id, tracker_data_cloud.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(tracker_data_server.id, tracker_data_server.id)
+ end
+end
diff --git a/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb b/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
index 1588cec0258..3b462c884c4 100644
--- a/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
+++ b/spec/migrations/20210423160427_schedule_drop_invalid_vulnerabilities_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210423160427_schedule_drop_invalid_vulnerabilities.rb')
+require_migration!('schedule_drop_invalid_vulnerabilities')
RSpec.describe ScheduleDropInvalidVulnerabilities, :migration do
let_it_be(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
diff --git a/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
index 3e57ffb4729..03ce0a430e5 100644
--- a/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
+++ b/spec/migrations/20210430134202_copy_adoption_snapshot_namespace_spec.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-
+#
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210430134202_copy_adoption_snapshot_namespace.rb')
+require_migration!('copy_adoption_snapshot_namespace')
-RSpec.describe CopyAdoptionSnapshotNamespace, :migration do
+RSpec.describe CopyAdoptionSnapshotNamespace, :migration, schema: 20210430124630 do
let(:namespaces_table) { table(:namespaces) }
let(:segments_table) { table(:analytics_devops_adoption_segments) }
let(:snapshots_table) { table(:analytics_devops_adoption_snapshots) }
- before do
+ it 'updates all snapshots without namespace set' do
namespaces_table.create!(id: 123, name: 'group1', path: 'group1')
namespaces_table.create!(id: 124, name: 'group2', path: 'group2')
@@ -19,9 +19,7 @@ RSpec.describe CopyAdoptionSnapshotNamespace, :migration do
create_snapshot(id: 1, segment_id: 1)
create_snapshot(id: 2, segment_id: 2)
create_snapshot(id: 3, segment_id: 2, namespace_id: 123)
- end
- it 'updates all snapshots without namespace set' do
migrate!
expect(snapshots_table.find(1).namespace_id).to eq 123
diff --git a/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
index a37772db28c..abdfd03f97e 100644
--- a/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
+++ b/spec/migrations/20210430135954_copy_adoption_segments_namespace_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210430135954_copy_adoption_segments_namespace.rb')
+require_migration!('copy_adoption_segments_namespace')
RSpec.describe CopyAdoptionSegmentsNamespace, :migration do
let(:namespaces_table) { table(:namespaces) }
diff --git a/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
index 6e1cc63e42a..4969d82d183 100644
--- a/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
+++ b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20210503105845_add_project_value_stream_id_to_project_stages.rb')
+require_migration!('add_project_value_stream_id_to_project_stages')
RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022 do
let(:stages) { table(:analytics_cycle_analytics_project_stages) }
diff --git a/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb b/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
index 6ffaa26f923..969a2e58947 100644
--- a/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
+++ b/spec/migrations/20210511142748_schedule_drop_invalid_vulnerabilities2_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210511142748_schedule_drop_invalid_vulnerabilities2.rb')
+require_migration!('schedule_drop_invalid_vulnerabilities2')
RSpec.describe ScheduleDropInvalidVulnerabilities2, :migration do
let_it_be(:background_migration_jobs) { table(:background_migration_jobs) }
diff --git a/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb b/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
new file mode 100644
index 00000000000..b7524ee0bff
--- /dev/null
+++ b/spec/migrations/20210514063252_schedule_cleanup_orphaned_lfs_objects_projects_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('schedule_cleanup_orphaned_lfs_objects_projects')
+
+RSpec.describe ScheduleCleanupOrphanedLfsObjectsProjects, schema: 20210511165250 do
+ let(:lfs_objects_projects) { table(:lfs_objects_projects) }
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:lfs_objects) { table(:lfs_objects) }
+
+ let(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
+ let(:another_project) { projects.create!(namespace_id: namespace.id) }
+ let(:lfs_object) { lfs_objects.create!(oid: 'abcdef', size: 1) }
+ let(:another_lfs_object) { lfs_objects.create!(oid: '1abcde', size: 2) }
+
+ describe '#up' do
+ it 'schedules CleanupOrphanedLfsObjectsProjects background jobs' do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+
+ lfs_objects_project1 = lfs_objects_projects.create!(project_id: project.id, lfs_object_id: lfs_object.id)
+ lfs_objects_project2 = lfs_objects_projects.create!(project_id: another_project.id, lfs_object_id: lfs_object.id)
+ lfs_objects_project3 = lfs_objects_projects.create!(project_id: project.id, lfs_object_id: another_lfs_object.id)
+ lfs_objects_project4 = lfs_objects_projects.create!(project_id: another_project.id, lfs_object_id: another_lfs_object.id)
+
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, lfs_objects_project1.id, lfs_objects_project2.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, lfs_objects_project3.id, lfs_objects_project4.id)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb b/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
new file mode 100644
index 00000000000..36d85d1f745
--- /dev/null
+++ b/spec/migrations/20210601073400_fix_total_stage_in_vsa_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('fix_total_stage_in_vsa')
+
+RSpec.describe FixTotalStageInVsa, :migration, schema: 20210518001450 do
+ let(:namespaces) { table(:namespaces) }
+ let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
+ let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
+
+ let!(:group) { namespaces.create!(name: 'ns1', path: 'ns1', type: 'Group') }
+ let!(:group_vs_1) { group_value_streams.create!(name: 'default', group_id: group.id) }
+ let!(:group_vs_2) { group_value_streams.create!(name: 'other', group_id: group.id) }
+ let!(:group_vs_3) { group_value_streams.create!(name: 'another', group_id: group.id) }
+ let!(:group_stage_total) { group_stages.create!(name: 'Total', custom: false, group_id: group.id, group_value_stream_id: group_vs_1.id, start_event_identifier: 1, end_event_identifier: 2) }
+ let!(:group_stage_different_name) { group_stages.create!(name: 'Issue', custom: false, group_id: group.id, group_value_stream_id: group_vs_2.id, start_event_identifier: 1, end_event_identifier: 2) }
+ let!(:group_stage_total_custom) { group_stages.create!(name: 'Total', custom: true, group_id: group.id, group_value_stream_id: group_vs_3.id, start_event_identifier: 1, end_event_identifier: 2) }
+
+ it 'deduplicates issue_metrics table' do
+ migrate!
+
+ group_stage_total.reload
+ group_stage_different_name.reload
+ group_stage_total_custom.reload
+
+ expect(group_stage_total.custom).to eq(true)
+ expect(group_stage_different_name.custom).to eq(false)
+ expect(group_stage_total_custom.custom).to eq(true)
+ end
+end
diff --git a/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
new file mode 100644
index 00000000000..d3154596b26
--- /dev/null
+++ b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('group_protected_environments_add_index_and_constraint')
+
+RSpec.describe GroupProtectedEnvironmentsAddIndexAndConstraint do
+ let(:migration) { described_class.new }
+ let(:protected_environments) { table(:protected_environments) }
+ let(:group) { table(:namespaces).create!(name: 'group', path: 'group') }
+ let(:project) { table(:projects).create!(name: 'project', path: 'project', namespace_id: group.id) }
+
+ describe '#down' do
+ it 'deletes only group-level configurations' do
+ migration.up
+
+ project_protections = [
+ protected_environments.create!(project_id: project.id, name: 'production'),
+ protected_environments.create!(project_id: project.id, name: 'staging')
+ ]
+ protected_environments.create!(group_id: group.id, name: 'production')
+ protected_environments.create!(group_id: group.id, name: 'staging')
+
+ migration.down
+
+ expect(protected_environments.pluck(:id))
+ .to match_array project_protections.map(&:id)
+ end
+ 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
new file mode 100644
index 00000000000..c457be79834
--- /dev/null
+++ b/spec/migrations/20210603222333_remove_builds_email_service_from_services_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!('remove_builds_email_service_from_services')
+
+RSpec.describe RemoveBuildsEmailServiceFromServices 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/add_default_and_free_plans_spec.rb b/spec/migrations/add_default_and_free_plans_spec.rb
index 75787896999..7256e4928af 100644
--- a/spec/migrations/add_default_and_free_plans_spec.rb
+++ b/spec/migrations/add_default_and_free_plans_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191023152913_add_default_and_free_plans.rb')
+require_migration!('add_default_and_free_plans')
RSpec.describe AddDefaultAndFreePlans do
describe 'migrate' do
diff --git a/spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb b/spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb
index 31dee29a39b..f21acbc56df 100644
--- a/spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb
+++ b/spec/migrations/add_default_value_stream_to_groups_with_group_stages_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200701070435_add_default_value_stream_to_groups_with_group_stages.rb')
+require_migration!
RSpec.describe AddDefaultValueStreamToGroupsWithGroupStages, schema: 20200624142207 do
let(:groups) { table(:namespaces) }
diff --git a/spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb b/spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb
index b33320c922a..f90bfcd313c 100644
--- a/spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb
+++ b/spec/migrations/add_deploy_token_type_to_deploy_tokens_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200122161638_add_deploy_token_type_to_deploy_tokens.rb')
+require_migration!
RSpec.describe AddDeployTokenTypeToDeployTokens do
let(:deploy_tokens) { table(:deploy_tokens) }
diff --git a/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb b/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb
index a62fc43df02..3e0bc64bb23 100644
--- a/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb
+++ b/spec/migrations/add_incident_settings_to_all_existing_projects_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200609212701_add_incident_settings_to_all_existing_projects.rb')
+require_migration!
RSpec.describe AddIncidentSettingsToAllExistingProjects, :migration do
let(:project_incident_management_settings) { table(:project_incident_management_settings) }
diff --git a/spec/migrations/add_new_post_eoa_plans_spec.rb b/spec/migrations/add_new_post_eoa_plans_spec.rb
index 5ae227a6617..02360d5a12d 100644
--- a/spec/migrations/add_new_post_eoa_plans_spec.rb
+++ b/spec/migrations/add_new_post_eoa_plans_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210205104425_add_new_post_eoa_plans.rb')
+require_migration!
RSpec.describe AddNewPostEoaPlans do
let(:plans) { table(:plans) }
diff --git a/spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb b/spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb
index 018d48bea66..ab4d6f43797 100644
--- a/spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb
+++ b/spec/migrations/add_partial_index_to_ci_builds_table_on_user_id_name_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200908064229_add_partial_index_to_ci_builds_table_on_user_id_name.rb')
+require_migration!
RSpec.describe AddPartialIndexToCiBuildsTableOnUserIdName do
let(:migration) { described_class.new }
diff --git a/spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb b/spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb
index 6c6c63d8614..bc4c510fea3 100644
--- a/spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb
+++ b/spec/migrations/add_repository_storages_weighted_to_application_settings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200508203901_add_repository_storages_weighted_to_application_settings.rb')
+require_migration!
RSpec.describe AddRepositoryStoragesWeightedToApplicationSettings, :migration do
let(:storages) { { "foo" => {}, "baz" => {} } }
diff --git a/spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb b/spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb
index 1be699f5636..dae0241b895 100644
--- a/spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb
+++ b/spec/migrations/add_temporary_partial_index_on_project_id_to_services_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200114112932_add_temporary_partial_index_on_project_id_to_services.rb')
+require_migration!
RSpec.describe AddTemporaryPartialIndexOnProjectIdToServices do
let(:migration) { described_class.new }
diff --git a/spec/migrations/add_unique_constraint_to_approvals_user_id_and_merge_request_id_spec.rb b/spec/migrations/add_unique_constraint_to_approvals_user_id_and_merge_request_id_spec.rb
index 391bc59cdc0..795de51d387 100644
--- a/spec/migrations/add_unique_constraint_to_approvals_user_id_and_merge_request_id_spec.rb
+++ b/spec/migrations/add_unique_constraint_to_approvals_user_id_and_merge_request_id_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190404143330_add_unique_constraint_to_approvals_user_id_and_merge_request_id.rb')
+require_migration!
RSpec.describe AddUniqueConstraintToApprovalsUserIdAndMergeRequestId do
let(:migration) { described_class.new }
diff --git a/spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb b/spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb
index 6c782af9a31..ea6599fc122 100644
--- a/spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb
+++ b/spec/migrations/backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190628185004_backfill_and_add_not_null_constraint_to_released_at_column_on_releases_table.rb')
+require_migration!
RSpec.describe BackfillAndAddNotNullConstraintToReleasedAtColumnOnReleasesTable do
let(:releases) { table(:releases) }
diff --git a/spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb b/spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb
new file mode 100644
index 00000000000..6c116120f05
--- /dev/null
+++ b/spec/migrations/backfill_clusters_integration_prometheus_enabled_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillClustersIntegrationPrometheusEnabled, :migration do
+ def create_cluster!(label = rand(2**64).to_s)
+ table(:clusters).create!(
+ name: "cluster: #{label}",
+ created_at: 1.day.ago,
+ updated_at: 1.day.ago
+ )
+ end
+
+ def create_clusters_applications_prometheus!(label, status:, cluster_id: nil)
+ table(:clusters_applications_prometheus).create!(
+ cluster_id: cluster_id || create_cluster!(label).id,
+ status: status,
+ version: "#{label}: version",
+ created_at: 1.day.ago, # artificially aged
+ updated_at: 1.day.ago, # artificially aged
+ encrypted_alert_manager_token: "#{label}: token",
+ encrypted_alert_manager_token_iv: "#{label}: iv"
+ )
+ end
+
+ def create_clusters_integration_prometheus!
+ table(:clusters_integration_prometheus).create!(
+ cluster_id: create_cluster!.id,
+ enabled: false,
+ created_at: 1.day.ago,
+ updated_at: 1.day.ago
+ )
+ end
+
+ RSpec::Matchers.define :be_enabled_and_match_application_values do |application|
+ match do |actual|
+ actual.enabled == true &&
+ actual.encrypted_alert_manager_token == application.encrypted_alert_manager_token &&
+ actual.encrypted_alert_manager_token_iv == application.encrypted_alert_manager_token_iv
+ end
+ end
+
+ describe '#up' do
+ it 'backfills the enabled status and alert manager credentials from clusters_applications_prometheus' do
+ status_installed = 3
+ status_externally_installed = 11
+ status_installable = 0
+
+ existing_integration = create_clusters_integration_prometheus!
+ unaffected_existing_integration = create_clusters_integration_prometheus!
+ app_installed = create_clusters_applications_prometheus!('installed', status: status_installed)
+ app_installed_existing_integration = create_clusters_applications_prometheus!('installed, existing integration', status: status_installed, cluster_id: existing_integration.cluster_id)
+ app_externally_installed = create_clusters_applications_prometheus!('externally installed', status: status_externally_installed)
+ app_other_status = create_clusters_applications_prometheus!('other status', status: status_installable)
+
+ migrate!
+
+ integrations = table(:clusters_integration_prometheus).all.index_by(&:cluster_id)
+
+ expect(unaffected_existing_integration.reload).to eq unaffected_existing_integration
+
+ integration_installed = integrations[app_installed.cluster_id]
+ expect(integration_installed).to be_enabled_and_match_application_values(app_installed)
+ expect(integration_installed.updated_at).to be >= 1.minute.ago # recently updated
+ expect(integration_installed.updated_at).to eq(integration_installed.created_at) # recently created
+
+ expect(existing_integration.reload).to be_enabled_and_match_application_values(app_installed_existing_integration)
+ expect(existing_integration.updated_at).to be >= 1.minute.ago # recently updated
+ expect(existing_integration.updated_at).not_to eq(existing_integration.created_at) # but not recently created
+
+ integration_externally_installed = integrations[app_externally_installed.cluster_id]
+ expect(integration_externally_installed).to be_enabled_and_match_application_values(app_externally_installed)
+ expect(integration_externally_installed.updated_at).to be >= 1.minute.ago # recently updated
+ expect(integration_externally_installed.updated_at).to eq(integration_externally_installed.created_at) # recently created
+
+ expect(integrations[app_other_status.cluster_id]).to be_nil
+ end
+ end
+end
diff --git a/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb b/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb
new file mode 100644
index 00000000000..da32e92ebb0
--- /dev/null
+++ b/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillEscalationPoliciesForOncallSchedules do
+ let_it_be(:projects) { table(:projects) }
+ let_it_be(:schedules) { table(:incident_management_oncall_schedules) }
+ let_it_be(:policies) { table(:incident_management_escalation_policies) }
+ let_it_be(:rules) { table(:incident_management_escalation_rules) }
+
+ # Project with no schedules
+ let_it_be(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') }
+ let_it_be(:project_a) { projects.create!(namespace_id: namespace.id) }
+
+ context 'with backfill-able schedules' do
+ # Project with one schedule
+ let_it_be(:project_b) { projects.create!(namespace_id: namespace.id) }
+ let_it_be(:schedule_b1) { schedules.create!(project_id: project_b.id, iid: 1, name: 'Schedule B1') }
+
+ # Project with multiple schedules
+ let_it_be(:project_c) { projects.create!(namespace_id: namespace.id) }
+ let_it_be(:schedule_c1) { schedules.create!(project_id: project_c.id, iid: 1, name: 'Schedule C1') }
+ let_it_be(:schedule_c2) { schedules.create!(project_id: project_c.id, iid: 2, name: 'Schedule C2') }
+
+ # Project with a single schedule which already has a policy
+ let_it_be(:project_d) { projects.create!(namespace_id: namespace.id) }
+ let_it_be(:schedule_d1) { schedules.create!(project_id: project_d.id, iid: 1, name: 'Schedule D1') }
+ let_it_be(:policy_d1) { policies.create!(project_id: project_d.id, name: 'Policy D1') }
+ let_it_be(:rule_d1) { rules.create!(policy_id: policy_d1.id, oncall_schedule_id: schedule_d1.id, status: 2, elapsed_time_seconds: 60) }
+
+ # Project with a multiple schedule, one of which already has a policy
+ let_it_be(:project_e) { projects.create!(namespace_id: namespace.id) }
+ let_it_be(:schedule_e1) { schedules.create!(project_id: project_e.id, iid: 1, name: 'Schedule E1') }
+ let_it_be(:schedule_e2) { schedules.create!(project_id: project_e.id, iid: 2, name: 'Schedule E2') }
+ let_it_be(:policy_e1) { policies.create!(project_id: project_e.id, name: 'Policy E1') }
+ let_it_be(:rule_e1) { rules.create!(policy_id: policy_e1.id, oncall_schedule_id: schedule_e2.id, status: 2, elapsed_time_seconds: 60) }
+
+ # Project with a multiple schedule, with multiple policies
+ let_it_be(:project_f) { projects.create!(namespace_id: namespace.id) }
+ let_it_be(:schedule_f1) { schedules.create!(project_id: project_f.id, iid: 1, name: 'Schedule F1') }
+ let_it_be(:schedule_f2) { schedules.create!(project_id: project_f.id, iid: 2, name: 'Schedule F2') }
+ let_it_be(:policy_f1) { policies.create!(project_id: project_f.id, name: 'Policy F1') }
+ let_it_be(:rule_f1) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f1.id, status: 2, elapsed_time_seconds: 60) }
+ let_it_be(:rule_f2) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f2.id, status: 2, elapsed_time_seconds: 60) }
+ let_it_be(:policy_f2) { policies.create!(project_id: project_f.id, name: 'Policy F2') }
+ let_it_be(:rule_f3) { rules.create!(policy_id: policy_f2.id, oncall_schedule_id: schedule_f2.id, status: 1, elapsed_time_seconds: 10) }
+
+ it 'backfills escalation policies correctly' do
+ expect { migrate! }
+ .to change(policies, :count).by(2)
+ .and change(rules, :count).by(3)
+
+ new_policy_b1, new_policy_c1 = new_polices = policies.last(2)
+ new_rules = rules.last(3)
+
+ expect(new_polices).to all have_attributes(name: 'On-call Escalation Policy')
+ expect(new_policy_b1.description).to eq('Immediately notify Schedule B1')
+ expect(new_policy_c1.description).to eq('Immediately notify Schedule C1')
+ expect(policies.pluck(:project_id)).to eq([
+ project_d.id,
+ project_e.id,
+ project_f.id,
+ project_f.id,
+ project_b.id,
+ project_c.id
+ ])
+
+ expect(new_rules).to all have_attributes(status: 1, elapsed_time_seconds: 0)
+ expect(rules.pluck(:policy_id)).to eq([
+ rule_d1.policy_id,
+ rule_e1.policy_id,
+ rule_f1.policy_id,
+ rule_f2.policy_id,
+ rule_f3.policy_id,
+ new_policy_b1.id,
+ new_policy_c1.id,
+ new_policy_c1.id
+ ])
+ expect(rules.pluck(:oncall_schedule_id)).to eq([
+ rule_d1.oncall_schedule_id,
+ rule_e1.oncall_schedule_id,
+ rule_f1.oncall_schedule_id,
+ rule_f2.oncall_schedule_id,
+ rule_f3.oncall_schedule_id,
+ schedule_b1.id,
+ schedule_c1.id,
+ schedule_c2.id
+ ])
+ end
+ end
+
+ context 'with no schedules' do
+ it 'does nothing' do
+ expect { migrate! }
+ .to not_change(policies, :count)
+ .and not_change(rules, :count)
+ end
+ end
+end
diff --git a/spec/migrations/backfill_imported_snippet_repositories_spec.rb b/spec/migrations/backfill_imported_snippet_repositories_spec.rb
index 356551f176e..7052433c66d 100644
--- a/spec/migrations/backfill_imported_snippet_repositories_spec.rb
+++ b/spec/migrations/backfill_imported_snippet_repositories_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200608072931_backfill_imported_snippet_repositories.rb')
+require_migration!
RSpec.describe BackfillImportedSnippetRepositories do
let(:users) { table(:users) }
diff --git a/spec/migrations/backfill_operations_feature_flags_active_spec.rb b/spec/migrations/backfill_operations_feature_flags_active_spec.rb
index e49b317d850..a28f648c75a 100644
--- a/spec/migrations/backfill_operations_feature_flags_active_spec.rb
+++ b/spec/migrations/backfill_operations_feature_flags_active_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191213184609_backfill_operations_feature_flags_active.rb')
+require_migration!
RSpec.describe BackfillOperationsFeatureFlagsActive do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/backfill_operations_feature_flags_iid_spec.rb b/spec/migrations/backfill_operations_feature_flags_iid_spec.rb
index 1ade08f657e..3c400840f98 100644
--- a/spec/migrations/backfill_operations_feature_flags_iid_spec.rb
+++ b/spec/migrations/backfill_operations_feature_flags_iid_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200117194850_backfill_operations_feature_flags_iid.rb')
+require_migration!
RSpec.describe BackfillOperationsFeatureFlagsIid do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb b/spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb
index 22c93d37816..6e8bcfc050d 100644
--- a/spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb
+++ b/spec/migrations/backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190920194925_backfill_releases_table_updated_at_and_add_not_null_constraints_to_timestamps.rb')
+require_migration!
RSpec.describe BackfillReleasesTableUpdatedAtAndAddNotNullConstraintsToTimestamps do
let(:releases) { table(:releases) }
diff --git a/spec/migrations/backfill_snippet_repositories_spec.rb b/spec/migrations/backfill_snippet_repositories_spec.rb
index b176b8d49ec..64cfc9cc57b 100644
--- a/spec/migrations/backfill_snippet_repositories_spec.rb
+++ b/spec/migrations/backfill_snippet_repositories_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200420094444_backfill_snippet_repositories.rb')
+require_migration!
RSpec.describe BackfillSnippetRepositories do
let(:users) { table(:users) }
diff --git a/spec/migrations/backfill_status_page_published_incidents_spec.rb b/spec/migrations/backfill_status_page_published_incidents_spec.rb
index 674484cdf0a..fa4bb182362 100644
--- a/spec/migrations/backfill_status_page_published_incidents_spec.rb
+++ b/spec/migrations/backfill_status_page_published_incidents_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200421195234_backfill_status_page_published_incidents.rb')
+require_migration!
RSpec.describe BackfillStatusPagePublishedIncidents, :migration do
subject(:migration) { described_class.new }
diff --git a/spec/migrations/backport_enterprise_schema_spec.rb b/spec/migrations/backport_enterprise_schema_spec.rb
index b76b53dc259..de6821001b4 100644
--- a/spec/migrations/backport_enterprise_schema_spec.rb
+++ b/spec/migrations/backport_enterprise_schema_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190402150158_backport_enterprise_schema.rb')
+require_migration!
RSpec.describe BackportEnterpriseSchema, schema: 20190329085614 do
include MigrationsHelpers
diff --git a/spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb b/spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb
index 382ce2e1da4..702f2e6d9bd 100644
--- a/spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb
+++ b/spec/migrations/cap_designs_filename_length_to_new_limit_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200602013901_cap_designs_filename_length_to_new_limit')
+require_migration!
RSpec.describe CapDesignsFilenameLengthToNewLimit, :migration, schema: 20200528125905 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/change_outbound_local_requests_whitelist_default_spec.rb b/spec/migrations/change_outbound_local_requests_whitelist_default_spec.rb
index 53b02425010..24e6f3480f9 100644
--- a/spec/migrations/change_outbound_local_requests_whitelist_default_spec.rb
+++ b/spec/migrations/change_outbound_local_requests_whitelist_default_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190725012225_change_outbound_local_requests_whitelist_default.rb')
+require_migration!
RSpec.describe ChangeOutboundLocalRequestsWhitelistDefault do
let(:application_settings) { table(:application_settings) }
diff --git a/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb b/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb
index 8614bfea7cb..5e3118b0dea 100644
--- a/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb
+++ b/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190516155724_change_packages_size_defaults_in_project_statistics.rb')
+require_migration!
RSpec.describe ChangePackagesSizeDefaultsInProjectStatistics do
let(:project_statistics) { table(:project_statistics) }
diff --git a/spec/migrations/change_web_hook_events_default_spec.rb b/spec/migrations/change_web_hook_events_default_spec.rb
index 3b1a65ece17..aad187187d0 100644
--- a/spec/migrations/change_web_hook_events_default_spec.rb
+++ b/spec/migrations/change_web_hook_events_default_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20210420012444_change_web_hook_events_default.rb')
+require_migration!
RSpec.describe ChangeWebHookEventsDefault do
let(:web_hooks) { table(:web_hooks) }
diff --git a/spec/migrations/clean_grafana_url_spec.rb b/spec/migrations/clean_grafana_url_spec.rb
index caaf44b884d..7a81eb3058b 100644
--- a/spec/migrations/clean_grafana_url_spec.rb
+++ b/spec/migrations/clean_grafana_url_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200214085940_clean_grafana_url.rb')
+require_migration!
RSpec.describe CleanGrafanaUrl do
let(:application_settings_table) { table(:application_settings) }
diff --git a/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb
index 268fadee0af..8de30af13fd 100644
--- a/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb
+++ b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190313092516_clean_up_noteable_id_for_notes_on_commits.rb')
+require_migration!
RSpec.describe CleanUpNoteableIdForNotesOnCommits do
let(:notes) { table(:notes) }
diff --git a/spec/migrations/clean_up_pending_builds_table_spec.rb b/spec/migrations/clean_up_pending_builds_table_spec.rb
new file mode 100644
index 00000000000..9c8d4413337
--- /dev/null
+++ b/spec/migrations/clean_up_pending_builds_table_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CleanUpPendingBuildsTable 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', type: 'Ci::Build')
+ builds.create!(id: 2, project_id: 123, status: 'pending', type: 'GenericCommitStatus')
+ builds.create!(id: 3, project_id: 123, status: 'success', type: 'Ci::Bridge')
+ builds.create!(id: 4, project_id: 123, status: 'success', type: 'Ci::Build')
+ builds.create!(id: 5, project_id: 123, status: 'running', type: 'Ci::Build')
+ builds.create!(id: 6, project_id: 123, status: 'created', type: 'Ci::Build')
+
+ queue.create!(id: 1, project_id: 123, build_id: 1)
+ queue.create!(id: 2, project_id: 123, build_id: 4)
+ queue.create!(id: 3, project_id: 123, build_id: 5)
+ end
+
+ it 'removes duplicated data from pending builds table' do
+ migrate!
+
+ expect(queue.all.count).to eq 1
+ expect(queue.first.id).to eq 1
+ expect(builds.all.count).to eq 6
+ end
+
+ context 'when there are multiple batches' do
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ it 'iterates the data correctly' do
+ migrate!
+
+ expect(queue.all.count).to eq 1
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_empty_commit_user_mentions_spec.rb b/spec/migrations/cleanup_empty_commit_user_mentions_spec.rb
index da714e7da4c..d128c13e212 100644
--- a/spec/migrations/cleanup_empty_commit_user_mentions_spec.rb
+++ b/spec/migrations/cleanup_empty_commit_user_mentions_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200128133510_cleanup_empty_commit_user_mentions')
+require_migration!
RSpec.describe CleanupEmptyCommitUserMentions, :migration, :sidekiq do
let(:users) { table(:users) }
diff --git a/spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb b/spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb
index f9285c857de..acd6a19779d 100644
--- a/spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb
+++ b/spec/migrations/cleanup_group_import_states_with_null_user_id_spec.rb
@@ -12,8 +12,8 @@
# To solve this problem, use SchemaVersionFinder to set schema one version prior to AddNotNullConstraintToUserOnGroupImportStates
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200907092715_add_not_null_constraint_to_user_on_group_import_states.rb')
-require Rails.root.join('db', 'post_migrate', '20200909161624_cleanup_group_import_states_with_null_user_id.rb')
+require_migration!('add_not_null_constraint_to_user_on_group_import_states')
+require_migration!
RSpec.describe CleanupGroupImportStatesWithNullUserId, :migration,
schema: MigrationHelpers::SchemaVersionFinder.migration_prior(AddNotNullConstraintToUserOnGroupImportStates) do
diff --git a/spec/migrations/cleanup_legacy_artifact_migration_spec.rb b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb
index 83b88a0cf1a..6362965cc31 100644
--- a/spec/migrations/cleanup_legacy_artifact_migration_spec.rb
+++ b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190104182041_cleanup_legacy_artifact_migration.rb')
+require_migration!('cleanup_legacy_artifact_migration')
RSpec.describe CleanupLegacyArtifactMigration, :redis do
let(:migration) { spy('migration') }
diff --git a/spec/migrations/cleanup_move_container_registry_enabled_to_project_features_spec.rb b/spec/migrations/cleanup_move_container_registry_enabled_to_project_features_spec.rb
new file mode 100644
index 00000000000..3c39327304e
--- /dev/null
+++ b/spec/migrations/cleanup_move_container_registry_enabled_to_project_features_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('cleanup_move_container_registry_enabled_to_project_feature')
+
+RSpec.describe CleanupMoveContainerRegistryEnabledToProjectFeature, :migration do
+ let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
+ let(:non_null_project_features) { { pages_access_level: 20 } }
+ let(:bg_class_name) { 'MoveContainerRegistryEnabledToProjectFeature' }
+
+ let!(:project1) { table(:projects).create!(namespace_id: namespace.id, name: 'project 1', container_registry_enabled: true) }
+ let!(:project2) { table(:projects).create!(namespace_id: namespace.id, name: 'project 2', container_registry_enabled: false) }
+ let!(:project3) { table(:projects).create!(namespace_id: namespace.id, name: 'project 3', container_registry_enabled: nil) }
+
+ let!(:project4) { table(:projects).create!(namespace_id: namespace.id, name: 'project 4', container_registry_enabled: true) }
+ let!(:project5) { table(:projects).create!(namespace_id: namespace.id, name: 'project 5', container_registry_enabled: false) }
+ let!(:project6) { table(:projects).create!(namespace_id: namespace.id, name: 'project 6', container_registry_enabled: nil) }
+
+ let!(:project_feature1) { table(:project_features).create!(project_id: project1.id, container_registry_access_level: 20, **non_null_project_features) }
+ let!(:project_feature2) { table(:project_features).create!(project_id: project2.id, container_registry_access_level: 0, **non_null_project_features) }
+ let!(:project_feature3) { table(:project_features).create!(project_id: project3.id, container_registry_access_level: 0, **non_null_project_features) }
+
+ let!(:project_feature4) { table(:project_features).create!(project_id: project4.id, container_registry_access_level: 0, **non_null_project_features) }
+ let!(:project_feature5) { table(:project_features).create!(project_id: project5.id, container_registry_access_level: 20, **non_null_project_features) }
+ let!(:project_feature6) { table(:project_features).create!(project_id: project6.id, container_registry_access_level: 20, **non_null_project_features) }
+
+ let!(:background_migration_job1) { table(:background_migration_jobs).create!(class_name: bg_class_name, arguments: [project4.id, project5.id], status: 0) }
+ let!(:background_migration_job2) { table(:background_migration_jobs).create!(class_name: bg_class_name, arguments: [project6.id, project6.id], status: 0) }
+ let!(:background_migration_job3) { table(:background_migration_jobs).create!(class_name: bg_class_name, arguments: [project1.id, project3.id], status: 1) }
+
+ it 'steals remaining jobs, updates any remaining rows and deletes background_migration_jobs rows' do
+ expect(Gitlab::BackgroundMigration).to receive(:steal).with(bg_class_name).and_call_original
+
+ migrate!
+
+ expect(project_feature1.reload.container_registry_access_level).to eq(20)
+ expect(project_feature2.reload.container_registry_access_level).to eq(0)
+ expect(project_feature3.reload.container_registry_access_level).to eq(0)
+ expect(project_feature4.reload.container_registry_access_level).to eq(20)
+ expect(project_feature5.reload.container_registry_access_level).to eq(0)
+ expect(project_feature6.reload.container_registry_access_level).to eq(0)
+
+ expect(table(:background_migration_jobs).where(class_name: bg_class_name).count).to eq(0)
+ end
+end
diff --git a/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb b/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb
index 7c7f87d4e06..2f461ebc1d5 100644
--- a/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb
+++ b/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200427064130_cleanup_optimistic_locking_nulls_pt2_fixed.rb')
+require_migration!('cleanup_optimistic_locking_nulls_pt2_fixed')
RSpec.describe CleanupOptimisticLockingNullsPt2Fixed, :migration, schema: 20200219193117 do
test_tables = %w(ci_stages ci_builds ci_pipelines).freeze
diff --git a/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb b/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
index bcdcd3e9273..a287d950c89 100644
--- a/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
+++ b/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200128210353_cleanup_optimistic_locking_nulls')
+require_migration!('cleanup_optimistic_locking_nulls')
RSpec.describe CleanupOptimisticLockingNulls do
let(:epics) { table(:epics) }
diff --git a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
index a50e98faf48..c640bfcd174 100644
--- a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
+++ b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200511080113_add_projects_foreign_key_to_namespaces.rb')
-require Rails.root.join('db', 'post_migrate', '20200511083541_cleanup_projects_with_missing_namespace.rb')
+require_migration!('add_projects_foreign_key_to_namespaces')
+require_migration!
# In order to test the CleanupProjectsWithMissingNamespace migration, we need
# to first create an orphaned project (one with an invalid namespace_id)
diff --git a/spec/migrations/complete_namespace_settings_migration_spec.rb b/spec/migrations/complete_namespace_settings_migration_spec.rb
index 7820536f355..46c455d8b19 100644
--- a/spec/migrations/complete_namespace_settings_migration_spec.rb
+++ b/spec/migrations/complete_namespace_settings_migration_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200907124300_complete_namespace_settings_migration.rb')
+require_migration!
RSpec.describe CompleteNamespaceSettingsMigration, :redis do
let(:migration) { spy('migration') }
diff --git a/spec/migrations/confirm_project_bot_users_spec.rb b/spec/migrations/confirm_project_bot_users_spec.rb
index 11aa08d16a9..5f70181e70a 100644
--- a/spec/migrations/confirm_project_bot_users_spec.rb
+++ b/spec/migrations/confirm_project_bot_users_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200713071042_confirm_project_bot_users.rb')
+require_migration!
RSpec.describe ConfirmProjectBotUsers, :migration do
let(:users) { table(:users) }
diff --git a/spec/migrations/create_environment_for_self_monitoring_project_spec.rb b/spec/migrations/create_environment_for_self_monitoring_project_spec.rb
index 1ba464f1610..4615c231510 100644
--- a/spec/migrations/create_environment_for_self_monitoring_project_spec.rb
+++ b/spec/migrations/create_environment_for_self_monitoring_project_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200214214934_create_environment_for_self_monitoring_project')
+require_migration!
RSpec.describe CreateEnvironmentForSelfMonitoringProject do
let(:application_settings_table) { table(:application_settings) }
diff --git a/spec/migrations/deduplicate_epic_iids_spec.rb b/spec/migrations/deduplicate_epic_iids_spec.rb
index 8afb8b06948..c9dd5b3253b 100644
--- a/spec/migrations/deduplicate_epic_iids_spec.rb
+++ b/spec/migrations/deduplicate_epic_iids_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201106134950_deduplicate_epic_iids.rb')
+require_migration!
RSpec.describe DeduplicateEpicIids, :migration, schema: 20201106082723 do
let(:routes) { table(:routes) }
diff --git a/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb b/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb
index 50b5897220a..30d776c498b 100644
--- a/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb
+++ b/spec/migrations/delete_internal_ids_where_feature_flags_usage_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200117194900_delete_internal_ids_where_feature_flags_usage')
+require_migration!
RSpec.describe DeleteInternalIdsWhereFeatureFlagsUsage do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/delete_template_project_services_spec.rb b/spec/migrations/delete_template_project_services_spec.rb
index aacc4fcfd58..20532e4187a 100644
--- a/spec/migrations/delete_template_project_services_spec.rb
+++ b/spec/migrations/delete_template_project_services_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200305151736_delete_template_project_services.rb')
+require_migration!
RSpec.describe DeleteTemplateProjectServices, :migration do
let(:services) { table(:services) }
diff --git a/spec/migrations/delete_template_services_duplicated_by_type_spec.rb b/spec/migrations/delete_template_services_duplicated_by_type_spec.rb
index 5bcbfb7c733..b5a29436159 100644
--- a/spec/migrations/delete_template_services_duplicated_by_type_spec.rb
+++ b/spec/migrations/delete_template_services_duplicated_by_type_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200304160801_delete_template_services_duplicated_by_type.rb')
+require_migration!
RSpec.describe DeleteTemplateServicesDuplicatedByType do
let(:services) { table(:services) }
diff --git a/spec/migrations/delete_user_callout_alerts_moved_spec.rb b/spec/migrations/delete_user_callout_alerts_moved_spec.rb
index f6b1a8982fb..401cf77628d 100644
--- a/spec/migrations/delete_user_callout_alerts_moved_spec.rb
+++ b/spec/migrations/delete_user_callout_alerts_moved_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200710102418_delete_user_callout_alerts_moved.rb')
+require_migration!
RSpec.describe DeleteUserCalloutAlertsMoved do
let(:users) { table(:users) }
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
new file mode 100644
index 00000000000..f2be06f1ed6
--- /dev/null
+++ b/spec/migrations/disable_expiration_policies_linked_to_no_container_images_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe DisableExpirationPoliciesLinkedToNoContainerImages 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/drop_activate_prometheus_services_background_jobs_spec.rb b/spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb
index b4bbadd199e..c6115d5889c 100644
--- a/spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb
+++ b/spec/migrations/drop_activate_prometheus_services_background_jobs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200221144534_drop_activate_prometheus_services_background_jobs.rb')
+require_migration!
RSpec.describe DropActivatePrometheusServicesBackgroundJobs, :sidekiq, :redis, schema: 2020_02_21_144534 do
subject(:migration) { described_class.new }
diff --git a/spec/migrations/drop_alerts_service_data_spec.rb b/spec/migrations/drop_alerts_service_data_spec.rb
index b2128da938f..06382132952 100644
--- a/spec/migrations/drop_alerts_service_data_spec.rb
+++ b/spec/migrations/drop_alerts_service_data_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210205213933_drop_alerts_service_data.rb')
+require_migration!
RSpec.describe DropAlertsServiceData do
let_it_be(:alerts_service_data) { table(:alerts_service_data) }
diff --git a/spec/migrations/drop_background_migration_jobs_spec.rb b/spec/migrations/drop_background_migration_jobs_spec.rb
index 2896f4ca0eb..82b3f9f7187 100644
--- a/spec/migrations/drop_background_migration_jobs_spec.rb
+++ b/spec/migrations/drop_background_migration_jobs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200116051619_drop_background_migration_jobs.rb')
+require_migration!
RSpec.describe DropBackgroundMigrationJobs, :sidekiq, :redis, schema: 2020_01_16_051619 do
subject(:migration) { described_class.new }
diff --git a/spec/migrations/drop_project_ci_cd_settings_merge_trains_enabled_spec.rb b/spec/migrations/drop_project_ci_cd_settings_merge_trains_enabled_spec.rb
index 3f6aae401be..3093cd85ced 100644
--- a/spec/migrations/drop_project_ci_cd_settings_merge_trains_enabled_spec.rb
+++ b/spec/migrations/drop_project_ci_cd_settings_merge_trains_enabled_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb')
+require_migration!
RSpec.describe DropProjectCiCdSettingsMergeTrainsEnabled do
let!(:project_ci_cd_setting) { table(:project_ci_cd_settings) }
diff --git a/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb b/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb
index c705515ce98..62bd0dafb8e 100644
--- a/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb
+++ b/spec/migrations/encrypt_feature_flags_clients_tokens_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190606175050_encrypt_feature_flags_clients_tokens.rb')
+require_migration!
RSpec.describe EncryptFeatureFlagsClientsTokens do
let(:migration) { described_class.new }
diff --git a/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb b/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb
index 7cd3646d107..2e233816b8b 100644
--- a/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb
+++ b/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191120115530_encrypt_plaintext_attributes_on_application_settings.rb')
+require_migration!
RSpec.describe EncryptPlaintextAttributesOnApplicationSettings do
let(:migration) { described_class.new }
diff --git a/spec/migrations/enqueue_reset_merge_status_second_run_spec.rb b/spec/migrations/enqueue_reset_merge_status_second_run_spec.rb
index 2bbc67c4db3..49698f60964 100644
--- a/spec/migrations/enqueue_reset_merge_status_second_run_spec.rb
+++ b/spec/migrations/enqueue_reset_merge_status_second_run_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190620112608_enqueue_reset_merge_status_second_run.rb')
+require_migration!
RSpec.describe EnqueueResetMergeStatusSecondRun do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/enqueue_reset_merge_status_spec.rb b/spec/migrations/enqueue_reset_merge_status_spec.rb
index 0843bbacd5f..d62c99b80bc 100644
--- a/spec/migrations/enqueue_reset_merge_status_spec.rb
+++ b/spec/migrations/enqueue_reset_merge_status_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190528180441_enqueue_reset_merge_status.rb')
+require_migration!
RSpec.describe EnqueueResetMergeStatus do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb b/spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb
index 808580d5770..6998e7a91cf 100644
--- a/spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb
+++ b/spec/migrations/ensure_filled_external_diff_store_on_merge_request_diffs_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200910170908_ensure_filled_external_diff_store_on_merge_request_diffs.rb')
+require_migration!
RSpec.describe EnsureFilledExternalDiffStoreOnMergeRequestDiffs, schema: 20200908095446 do
let!(:merge_request_diffs) { table(:merge_request_diffs) }
diff --git a/spec/migrations/ensure_filled_file_store_on_package_files_spec.rb b/spec/migrations/ensure_filled_file_store_on_package_files_spec.rb
index 8a0f51ab27e..5cfc3a6eeb8 100644
--- a/spec/migrations/ensure_filled_file_store_on_package_files_spec.rb
+++ b/spec/migrations/ensure_filled_file_store_on_package_files_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200915185707_ensure_filled_file_store_on_package_files.rb')
+require_migration!
RSpec.describe EnsureFilledFileStoreOnPackageFiles, schema: 20200910175553 do
let!(:packages_package_files) { table(:packages_package_files) }
diff --git a/spec/migrations/ensure_namespace_settings_creation_spec.rb b/spec/migrations/ensure_namespace_settings_creation_spec.rb
index cececc1e569..b105e678d35 100644
--- a/spec/migrations/ensure_namespace_settings_creation_spec.rb
+++ b/spec/migrations/ensure_namespace_settings_creation_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201104124300_ensure_namespace_settings_creation.rb')
+require_migration!
RSpec.describe EnsureNamespaceSettingsCreation do
context 'when there are namespaces without namespace settings' do
diff --git a/spec/migrations/ensure_target_project_id_is_filled_spec.rb b/spec/migrations/ensure_target_project_id_is_filled_spec.rb
index 72d59a72814..7a9f49390fb 100644
--- a/spec/migrations/ensure_target_project_id_is_filled_spec.rb
+++ b/spec/migrations/ensure_target_project_id_is_filled_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200831065705_ensure_target_project_id_is_filled.rb')
+require_migration!
RSpec.describe EnsureTargetProjectIdIsFilled, schema: 20200827085101 do
let_it_be(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/ensure_u2f_registrations_migrated_spec.rb b/spec/migrations/ensure_u2f_registrations_migrated_spec.rb
index 77eab3b829a..01db29c0edf 100644
--- a/spec/migrations/ensure_u2f_registrations_migrated_spec.rb
+++ b/spec/migrations/ensure_u2f_registrations_migrated_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201026185514_ensure_u2f_registrations_migrated.rb')
+require_migration!
RSpec.describe EnsureU2fRegistrationsMigrated, schema: 20201022144501 do
let(:u2f_registrations) { table(:u2f_registrations) }
diff --git a/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb b/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb
index 64ab879d53c..7adcf74bdba 100644
--- a/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb
+++ b/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200513235532_fill_file_store_ci_job_artifacts.rb')
+require_migration!
RSpec.describe FillFileStoreCiJobArtifacts do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/fill_file_store_lfs_objects_spec.rb b/spec/migrations/fill_file_store_lfs_objects_spec.rb
index 23063cc9cd0..688976f79e8 100644
--- a/spec/migrations/fill_file_store_lfs_objects_spec.rb
+++ b/spec/migrations/fill_file_store_lfs_objects_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200513234502_fill_file_store_lfs_objects.rb')
+require_migration!
RSpec.describe FillFileStoreLfsObjects do
let(:lfs_objects) { table(:lfs_objects) }
diff --git a/spec/migrations/fill_productivity_analytics_start_date_spec.rb b/spec/migrations/fill_productivity_analytics_start_date_spec.rb
index 5d7f0ffba50..b348067a752 100644
--- a/spec/migrations/fill_productivity_analytics_start_date_spec.rb
+++ b/spec/migrations/fill_productivity_analytics_start_date_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191004081520_fill_productivity_analytics_start_date.rb')
+require_migration!
RSpec.describe FillProductivityAnalyticsStartDate do
let(:settings_table) { table('application_settings') }
diff --git a/spec/migrations/fill_store_uploads_spec.rb b/spec/migrations/fill_store_uploads_spec.rb
index bcb5d45e1c0..19db7c2b48d 100644
--- a/spec/migrations/fill_store_uploads_spec.rb
+++ b/spec/migrations/fill_store_uploads_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200513235347_fill_store_uploads.rb')
+require_migration!
RSpec.describe FillStoreUploads do
let(:uploads) { table(:uploads) }
diff --git a/spec/migrations/fix_max_pages_size_spec.rb b/spec/migrations/fix_max_pages_size_spec.rb
index b44ce21b858..97cf026df5c 100644
--- a/spec/migrations/fix_max_pages_size_spec.rb
+++ b/spec/migrations/fix_max_pages_size_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191213120427_fix_max_pages_size.rb')
+require_migration!
RSpec.describe FixMaxPagesSize do
let(:application_settings) { table(:application_settings) }
diff --git a/spec/migrations/fix_null_type_labels_spec.rb b/spec/migrations/fix_null_type_labels_spec.rb
index b3fc0e30687..4f902b92393 100644
--- a/spec/migrations/fix_null_type_labels_spec.rb
+++ b/spec/migrations/fix_null_type_labels_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190131122559_fix_null_type_labels')
+require_migration!
RSpec.describe FixNullTypeLabels do
let(:migration) { described_class.new }
diff --git a/spec/migrations/fix_pool_repository_source_project_id_spec.rb b/spec/migrations/fix_pool_repository_source_project_id_spec.rb
index 3413cef3c8b..2ee4c458c3c 100644
--- a/spec/migrations/fix_pool_repository_source_project_id_spec.rb
+++ b/spec/migrations/fix_pool_repository_source_project_id_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190604184643_fix_pool_repository_source_project_id.rb')
+require_migration!
RSpec.describe FixPoolRepositorySourceProjectId do
let(:projects) { table(:projects) }
diff --git a/spec/migrations/fix_projects_without_project_feature_spec.rb b/spec/migrations/fix_projects_without_project_feature_spec.rb
index 696c9e86384..d8c5e7a28c0 100644
--- a/spec/migrations/fix_projects_without_project_feature_spec.rb
+++ b/spec/migrations/fix_projects_without_project_feature_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200127111840_fix_projects_without_project_feature.rb')
+require_migration!
RSpec.describe FixProjectsWithoutProjectFeature do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
diff --git a/spec/migrations/fix_projects_without_prometheus_services_spec.rb b/spec/migrations/fix_projects_without_prometheus_services_spec.rb
index 987350847ca..dc03f381abd 100644
--- a/spec/migrations/fix_projects_without_prometheus_services_spec.rb
+++ b/spec/migrations/fix_projects_without_prometheus_services_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200220115023_fix_projects_without_prometheus_service.rb')
+require_migration!('fix_projects_without_prometheus_service')
RSpec.describe FixProjectsWithoutPrometheusService, :migration do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
diff --git a/spec/migrations/fix_wrong_pages_access_level_spec.rb b/spec/migrations/fix_wrong_pages_access_level_spec.rb
index 310076f2e0a..00a620b4426 100644
--- a/spec/migrations/fix_wrong_pages_access_level_spec.rb
+++ b/spec/migrations/fix_wrong_pages_access_level_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190703185326_fix_wrong_pages_access_level.rb')
+require_migration!
RSpec.describe FixWrongPagesAccessLevel, :sidekiq_might_not_need_inline, schema: 20190628185004 do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/migrations/generate_ci_jwt_signing_key_spec.rb b/spec/migrations/generate_ci_jwt_signing_key_spec.rb
index 249af3bcb50..7a895284aa1 100644
--- a/spec/migrations/generate_ci_jwt_signing_key_spec.rb
+++ b/spec/migrations/generate_ci_jwt_signing_key_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201008013434_generate_ci_jwt_signing_key.rb')
+require_migration!
RSpec.describe GenerateCiJwtSigningKey do
let(:application_settings) do
diff --git a/spec/migrations/generate_lets_encrypt_private_key_spec.rb b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
index cad257c18a0..8525a7bbd1c 100644
--- a/spec/migrations/generate_lets_encrypt_private_key_spec.rb
+++ b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190524062810_generate_lets_encrypt_private_key.rb')
+require_migration!
RSpec.describe GenerateLetsEncryptPrivateKey do
describe '#up' do
diff --git a/spec/migrations/generate_missing_routes_for_bots_spec.rb b/spec/migrations/generate_missing_routes_for_bots_spec.rb
index 8af22042350..594e51b4410 100644
--- a/spec/migrations/generate_missing_routes_for_bots_spec.rb
+++ b/spec/migrations/generate_missing_routes_for_bots_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200703064117_generate_missing_routes_for_bots.rb')
+require_migration!
RSpec.describe GenerateMissingRoutesForBots, :migration do
let(:users) { table(:users) }
diff --git a/spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb b/spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb
new file mode 100644
index 00000000000..4b8d3641247
--- /dev/null
+++ b/spec/migrations/insert_ci_daily_pipeline_schedule_triggers_plan_limits_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20210526190553_insert_ci_daily_pipeline_schedule_triggers_plan_limits.rb')
+
+RSpec.describe InsertCiDailyPipelineScheduleTriggersPlanLimits do
+ let_it_be(:plans) { table(:plans) }
+ let_it_be(:plan_limits) { table(:plan_limits) }
+
+ context 'when on Gitlab.com' do
+ let(:free_plan) { plans.create!(name: 'free') }
+ let(:bronze_plan) { plans.create!(name: 'bronze') }
+ let(:silver_plan) { plans.create!(name: 'silver') }
+ let(:gold_plan) { plans.create!(name: 'gold') }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ plan_limits.create!(plan_id: free_plan.id)
+ plan_limits.create!(plan_id: bronze_plan.id)
+ plan_limits.create!(plan_id: silver_plan.id)
+ plan_limits.create!(plan_id: gold_plan.id)
+ end
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(plan_limits.pluck(:plan_id, :ci_daily_pipeline_schedule_triggers)).to contain_exactly(
+ [free_plan.id, 0],
+ [bronze_plan.id, 0],
+ [silver_plan.id, 0],
+ [gold_plan.id, 0]
+ )
+ }
+
+ migration.after -> {
+ expect(plan_limits.pluck(:plan_id, :ci_daily_pipeline_schedule_triggers)).to contain_exactly(
+ [free_plan.id, 24],
+ [bronze_plan.id, 288],
+ [silver_plan.id, 288],
+ [gold_plan.id, 288]
+ )
+ }
+ end
+ end
+ end
+
+ context 'when on self hosted' do
+ let(:free_plan) { plans.create!(name: 'free') }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+
+ plan_limits.create!(plan_id: free_plan.id)
+ end
+
+ it 'does nothing' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(plan_limits.pluck(:plan_id, :ci_daily_pipeline_schedule_triggers)).to contain_exactly(
+ [free_plan.id, 0]
+ )
+ }
+
+ migration.after -> {
+ expect(plan_limits.pluck(:plan_id, :ci_daily_pipeline_schedule_triggers)).to contain_exactly(
+ [free_plan.id, 0]
+ )
+ }
+ end
+ end
+ end
+end
diff --git a/spec/migrations/insert_daily_invites_plan_limits_spec.rb b/spec/migrations/insert_daily_invites_plan_limits_spec.rb
index 3265efcb0ce..49d41a1039f 100644
--- a/spec/migrations/insert_daily_invites_plan_limits_spec.rb
+++ b/spec/migrations/insert_daily_invites_plan_limits_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201007033723_insert_daily_invites_plan_limits.rb')
+require_migration!
RSpec.describe InsertDailyInvitesPlanLimits do
let(:plans) { table(:plans) }
diff --git a/spec/migrations/insert_project_feature_flags_plan_limits_spec.rb b/spec/migrations/insert_project_feature_flags_plan_limits_spec.rb
index 1ad070de1ea..481e987c188 100644
--- a/spec/migrations/insert_project_feature_flags_plan_limits_spec.rb
+++ b/spec/migrations/insert_project_feature_flags_plan_limits_spec.rb
@@ -1,11 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join(
- 'db',
- 'migrate',
- '20200831222347_insert_project_feature_flags_plan_limits.rb'
-)
+require_migration!
RSpec.describe InsertProjectFeatureFlagsPlanLimits do
let(:migration) { described_class.new }
diff --git a/spec/migrations/insert_project_hooks_plan_limits_spec.rb b/spec/migrations/insert_project_hooks_plan_limits_spec.rb
index f29d72168bc..365dd679d76 100644
--- a/spec/migrations/insert_project_hooks_plan_limits_spec.rb
+++ b/spec/migrations/insert_project_hooks_plan_limits_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191216183532_insert_project_hooks_plan_limits.rb')
+require_migration!
RSpec.describe InsertProjectHooksPlanLimits do
let(:migration) { described_class.new }
diff --git a/spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb b/spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb
index f7789469efb..c2df04bf2d6 100644
--- a/spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb
+++ b/spec/migrations/migrate_all_merge_request_user_mentions_to_db_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200601120434_migrate_all_merge_request_user_mentions_to_db')
+require_migration!
RSpec.describe MigrateAllMergeRequestUserMentionsToDb, :migration do
let(:users) { table(:users) }
diff --git a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
index c7c846c7ef3..a836fb4bfb9 100644
--- a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
+++ b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190204115450_migrate_auto_dev_ops_domain_to_cluster_domain.rb')
+require_migration!
RSpec.describe MigrateAutoDevOpsDomainToClusterDomain do
include MigrationHelpers::ClusterHelpers
diff --git a/spec/migrations/migrate_bot_type_to_user_type_spec.rb b/spec/migrations/migrate_bot_type_to_user_type_spec.rb
index fcd7f1ebcb8..54cf3450692 100644
--- a/spec/migrations/migrate_bot_type_to_user_type_spec.rb
+++ b/spec/migrations/migrate_bot_type_to_user_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200311074438_migrate_bot_type_to_user_type.rb')
+require_migration!
RSpec.describe MigrateBotTypeToUserType, :migration do
let(:users) { table(:users) }
diff --git a/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb b/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb
index 70304a4deb2..121ff3d6622 100644
--- a/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb
+++ b/spec/migrations/migrate_code_owner_approval_status_to_protected_branches_in_batches_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190827102026_migrate_code_owner_approval_status_to_protected_branches_in_batches.rb')
+require_migration!
RSpec.describe MigrateCodeOwnerApprovalStatusToProtectedBranchesInBatches do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb b/spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb
index eb7fb7d4fc5..aa2aa6297c4 100644
--- a/spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb
+++ b/spec/migrations/migrate_commit_notes_mentions_to_db_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200128134110_migrate_commit_notes_mentions_to_db')
+require_migration!
RSpec.describe MigrateCommitNotesMentionsToDb, :migration, :sidekiq do
let(:users) { table(:users) }
diff --git a/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb b/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
index 5c3ca281d49..6a9a75a7019 100644
--- a/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
+++ b/spec/migrations/migrate_compliance_framework_enum_to_database_framework_record_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201005094331_migrate_compliance_framework_enum_to_database_framework_record.rb')
+require_migration!
RSpec.describe MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord, schema: 20201005092753 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb b/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb
index 5f7b4755980..0e631f255bf 100644
--- a/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_create_commit_signature_worker_sidekiq_queue_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200206091544_migrate_create_commit_signature_worker_sidekiq_queue.rb')
+require_migration!
RSpec.describe MigrateCreateCommitSignatureWorkerSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
diff --git a/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb b/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb
index 28a8dcf0d4c..0f45cc842ef 100644
--- a/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb
+++ b/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb')
+require_migration!
RSpec.describe MigrateDelayedProjectRemovalFromNamespacesToNamespaceSettings, :migration do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb b/spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb
index 699e9507f50..e42baab9927 100644
--- a/spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb
+++ b/spec/migrations/migrate_discussion_id_on_promoted_epics_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190715193142_migrate_discussion_id_on_promoted_epics.rb')
+require_migration!
RSpec.describe MigrateDiscussionIdOnPromotedEpics do
let(:migration_class) { described_class::MIGRATION }
diff --git a/spec/migrations/migrate_elastic_index_settings_spec.rb b/spec/migrations/migrate_elastic_index_settings_spec.rb
index 41483773903..5f39d9b9fc1 100644
--- a/spec/migrations/migrate_elastic_index_settings_spec.rb
+++ b/spec/migrations/migrate_elastic_index_settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20210324131727_migrate_elastic_index_settings.rb')
+require_migration!
RSpec.describe MigrateElasticIndexSettings do
let(:elastic_index_settings) { table(:elastic_index_settings) }
diff --git a/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb b/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb
index dc38695c7fe..acac6114c71 100644
--- a/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb
+++ b/spec/migrations/migrate_incident_issues_to_incident_type_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200723040950_migrate_incident_issues_to_incident_type.rb')
+require_migration!
RSpec.describe MigrateIncidentIssuesToIncidentType do
let(:migration) { described_class.new }
diff --git a/spec/migrations/migrate_k8s_service_integration_spec.rb b/spec/migrations/migrate_k8s_service_integration_spec.rb
index 66e30b29f61..ba6071b72e4 100644
--- a/spec/migrations/migrate_k8s_service_integration_spec.rb
+++ b/spec/migrations/migrate_k8s_service_integration_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190517153211_migrate_k8s_service_integration.rb')
+require_migration!
RSpec.describe MigrateK8sServiceIntegration do
context 'template service' do
diff --git a/spec/migrations/migrate_legacy_managed_clusters_to_unmanaged_spec.rb b/spec/migrations/migrate_legacy_managed_clusters_to_unmanaged_spec.rb
index c8bfeec8049..3d8685c7619 100644
--- a/spec/migrations/migrate_legacy_managed_clusters_to_unmanaged_spec.rb
+++ b/spec/migrations/migrate_legacy_managed_clusters_to_unmanaged_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190606163724_migrate_legacy_managed_clusters_to_unmanaged.rb')
+require_migration!
RSpec.describe MigrateLegacyManagedClustersToUnmanaged do
let(:cluster_type) { 'project_type' }
diff --git a/spec/migrations/migrate_managed_clusters_with_no_token_to_unmanaged_spec.rb b/spec/migrations/migrate_managed_clusters_with_no_token_to_unmanaged_spec.rb
index 1fc92f6ceea..b753b84ae55 100644
--- a/spec/migrations/migrate_managed_clusters_with_no_token_to_unmanaged_spec.rb
+++ b/spec/migrations/migrate_managed_clusters_with_no_token_to_unmanaged_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190613231640_migrate_managed_clusters_with_no_token_to_unmanaged.rb')
+require_migration!
RSpec.describe MigrateManagedClustersWithNoTokenToUnmanaged do
let(:cluster_type) { 'project_type' }
diff --git a/spec/migrations/migrate_merge_request_mentions_to_db_spec.rb b/spec/migrations/migrate_merge_request_mentions_to_db_spec.rb
index 83388cc6314..06493c4e5c1 100644
--- a/spec/migrations/migrate_merge_request_mentions_to_db_spec.rb
+++ b/spec/migrations/migrate_merge_request_mentions_to_db_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200211155539_migrate_merge_request_mentions_to_db')
+require_migration!
RSpec.describe MigrateMergeRequestMentionsToDb, :migration do
let(:users) { table(:users) }
diff --git a/spec/migrations/migrate_ops_feature_flags_scopes_target_user_ids_spec.rb b/spec/migrations/migrate_ops_feature_flags_scopes_target_user_ids_spec.rb
index f066b9c90cd..5caf03992dd 100644
--- a/spec/migrations/migrate_ops_feature_flags_scopes_target_user_ids_spec.rb
+++ b/spec/migrations/migrate_ops_feature_flags_scopes_target_user_ids_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191118211629_migrate_ops_feature_flags_scopes_target_user_ids.rb')
+require_migration!
RSpec.describe MigrateOpsFeatureFlagsScopesTargetUserIds do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb b/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb
new file mode 100644
index 00000000000..e838476a650
--- /dev/null
+++ b/spec/migrations/migrate_protected_attribute_to_pending_builds_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210610102413_migrate_protected_attribute_to_pending_builds.rb')
+
+RSpec.describe MigrateProtectedAttributeToPendingBuilds 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/migrate_storage_migrator_sidekiq_queue_spec.rb b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
index 1181c169f57..4db819f2fa1 100644
--- a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190124200344_migrate_storage_migrator_sidekiq_queue.rb')
+require_migration!
RSpec.describe MigrateStorageMigratorSidekiqQueue, :redis do
include Gitlab::Database::MigrationHelpers
diff --git a/spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb b/spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb
index 4face37bd66..35cb6104fe2 100644
--- a/spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_store_security_reports_sidekiq_queue_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200213220159_migrate_store_security_reports_sidekiq_queue.rb')
+require_migration!
RSpec.describe MigrateStoreSecurityReportsSidekiqQueue, :redis do
include Gitlab::Database::MigrationHelpers
diff --git a/spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb b/spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb
index 15c5761bd99..a9e386301b8 100644
--- a/spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200213220211_migrate_sync_security_reports_to_report_approval_rules_sidekiq_queue.rb')
+require_migration!
RSpec.describe MigrateSyncSecurityReportsToReportApprovalRulesSidekiqQueue, :redis do
include Gitlab::Database::MigrationHelpers
diff --git a/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb b/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb
index 4c50aa2dd10..e47cea749d6 100644
--- a/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb
+++ b/spec/migrations/move_container_registry_enabled_to_project_features3_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210415155043_move_container_registry_enabled_to_project_features3.rb')
+require_migration!
RSpec.describe MoveContainerRegistryEnabledToProjectFeatures3, :migration do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
diff --git a/spec/migrations/move_limits_from_plans_spec.rb b/spec/migrations/move_limits_from_plans_spec.rb
index c65fc439dd6..92ac804733f 100644
--- a/spec/migrations/move_limits_from_plans_spec.rb
+++ b/spec/migrations/move_limits_from_plans_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20191030152934_move_limits_from_plans.rb')
+require_migration!
RSpec.describe MoveLimitsFromPlans do
let(:plans) { table(:plans) }
diff --git a/spec/migrations/nullify_users_role_spec.rb b/spec/migrations/nullify_users_role_spec.rb
index 3cdeb81f362..11056d9cf0c 100644
--- a/spec/migrations/nullify_users_role_spec.rb
+++ b/spec/migrations/nullify_users_role_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191104142124_nullify_users_role.rb')
+require_migration!
RSpec.describe NullifyUsersRole do
let(:users) { table(:users) }
diff --git a/spec/migrations/populate_project_statistics_packages_size_spec.rb b/spec/migrations/populate_project_statistics_packages_size_spec.rb
index 6c838e83c56..af9237f4bd6 100644
--- a/spec/migrations/populate_project_statistics_packages_size_spec.rb
+++ b/spec/migrations/populate_project_statistics_packages_size_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190418132125_populate_project_statistics_packages_size.rb')
+require_migration!
RSpec.describe PopulateProjectStatisticsPackagesSize do
let(:project_statistics) { table(:project_statistics) }
diff --git a/spec/migrations/populate_rule_type_on_approval_merge_request_rules_spec.rb b/spec/migrations/populate_rule_type_on_approval_merge_request_rules_spec.rb
index 2ac912d7979..7dc3f5a1004 100644
--- a/spec/migrations/populate_rule_type_on_approval_merge_request_rules_spec.rb
+++ b/spec/migrations/populate_rule_type_on_approval_merge_request_rules_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190520201748_populate_rule_type_on_approval_merge_request_rules.rb')
+require_migration!
RSpec.describe PopulateRuleTypeOnApprovalMergeRequestRules do
let(:migration) { described_class.new }
diff --git a/spec/migrations/remove_additional_application_settings_rows_spec.rb b/spec/migrations/remove_additional_application_settings_rows_spec.rb
index a865e8e8dd7..d781195abf2 100644
--- a/spec/migrations/remove_additional_application_settings_rows_spec.rb
+++ b/spec/migrations/remove_additional_application_settings_rows_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200420162730_remove_additional_application_settings_rows.rb')
+require_migration!
RSpec.describe RemoveAdditionalApplicationSettingsRows do
let(:application_settings) { table(:application_settings) }
diff --git a/spec/migrations/remove_alerts_service_records_again_spec.rb b/spec/migrations/remove_alerts_service_records_again_spec.rb
index 963b54848f9..94d3e957b6a 100644
--- a/spec/migrations/remove_alerts_service_records_again_spec.rb
+++ b/spec/migrations/remove_alerts_service_records_again_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210205214003_remove_alerts_service_records_again.rb')
+require_migration!
RSpec.describe RemoveAlertsServiceRecordsAgain do
let(:services) { table(:services) }
diff --git a/spec/migrations/remove_alerts_service_records_spec.rb b/spec/migrations/remove_alerts_service_records_spec.rb
index eaf9f90b445..83f440f8e17 100644
--- a/spec/migrations/remove_alerts_service_records_spec.rb
+++ b/spec/migrations/remove_alerts_service_records_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210107194543_remove_alerts_service_records.rb')
+require_migration!
RSpec.describe RemoveAlertsServiceRecords do
let(:services) { table(:services) }
diff --git a/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb b/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb
index 00fde88c528..817cf183e0c 100644
--- a/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb
+++ b/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb
@@ -1,8 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200511130129_remove_deprecated_jenkins_service_records.rb')
-require Rails.root.join('db', 'post_migrate', '20200511130130_ensure_deprecated_jenkins_service_records_removal.rb')
+require_migration!
+
+require_migration!('ensure_deprecated_jenkins_service_records_removal')
RSpec.shared_examples 'remove DeprecatedJenkinsService records' do
let(:services) { table(:services) }
diff --git a/spec/migrations/remove_duplicate_labels_from_groups_spec.rb b/spec/migrations/remove_duplicate_labels_from_groups_spec.rb
index eff2c6a37e4..125314f70dd 100644
--- a/spec/migrations/remove_duplicate_labels_from_groups_spec.rb
+++ b/spec/migrations/remove_duplicate_labels_from_groups_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200716234259_remove_duplicate_labels_from_group.rb')
+require_migration!('remove_duplicate_labels_from_group')
RSpec.describe RemoveDuplicateLabelsFromGroup do
let(:labels_table) { table(:labels) }
diff --git a/spec/migrations/remove_duplicate_labels_from_project_spec.rb b/spec/migrations/remove_duplicate_labels_from_project_spec.rb
index 5eb8ba96aae..eeb9f155e01 100644
--- a/spec/migrations/remove_duplicate_labels_from_project_spec.rb
+++ b/spec/migrations/remove_duplicate_labels_from_project_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200305082754_remove_duplicate_labels_from_project.rb')
+require_migration!
RSpec.describe RemoveDuplicateLabelsFromProject do
let(:labels_table) { table(:labels) }
diff --git a/spec/migrations/remove_empty_github_service_templates_spec.rb b/spec/migrations/remove_empty_github_service_templates_spec.rb
index 7a77e342efd..ad84187c298 100644
--- a/spec/migrations/remove_empty_github_service_templates_spec.rb
+++ b/spec/migrations/remove_empty_github_service_templates_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191021101942_remove_empty_github_service_templates.rb')
+require_migration!
RSpec.describe RemoveEmptyGithubServiceTemplates do
subject(:migration) { described_class.new }
diff --git a/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb b/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb
index 81fa29f4c54..b4aa5187d4c 100644
--- a/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb
+++ b/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200623142159_remove_gitlab_issue_tracker_service_records.rb')
+require_migration!
RSpec.describe RemoveGitlabIssueTrackerServiceRecords do
let(:services) { table(:services) }
diff --git a/spec/migrations/remove_hipchat_service_records_spec.rb b/spec/migrations/remove_hipchat_service_records_spec.rb
index bc76d7933d8..d218b6b23a5 100644
--- a/spec/migrations/remove_hipchat_service_records_spec.rb
+++ b/spec/migrations/remove_hipchat_service_records_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210420103955_remove_hipchat_service_records.rb')
+require_migration!
RSpec.describe RemoveHipchatServiceRecords do
let(:services) { table(:services) }
diff --git a/spec/migrations/remove_orphan_service_hooks_spec.rb b/spec/migrations/remove_orphan_service_hooks_spec.rb
index c06a8b97738..71e70daf1e6 100644
--- a/spec/migrations/remove_orphan_service_hooks_spec.rb
+++ b/spec/migrations/remove_orphan_service_hooks_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-require Rails.root.join('db', 'migrate', '20201119125730_add_web_hooks_service_foreign_key.rb')
+require_migration!('add_web_hooks_service_foreign_key')
RSpec.describe RemoveOrphanServiceHooks, schema: 20201203123201 do
let(:web_hooks) { table(:web_hooks) }
diff --git a/spec/migrations/remove_orphaned_invited_members_spec.rb b/spec/migrations/remove_orphaned_invited_members_spec.rb
index 10da63518c2..67e98b69ccc 100644
--- a/spec/migrations/remove_orphaned_invited_members_spec.rb
+++ b/spec/migrations/remove_orphaned_invited_members_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200424050250_remove_orphaned_invited_members.rb')
+require_migration!
RSpec.describe RemoveOrphanedInvitedMembers do
let(:members_table) { table(:members) }
diff --git a/spec/migrations/remove_packages_deprecated_dependencies_spec.rb b/spec/migrations/remove_packages_deprecated_dependencies_spec.rb
index 84c23240af9..f76a26bcdc1 100644
--- a/spec/migrations/remove_packages_deprecated_dependencies_spec.rb
+++ b/spec/migrations/remove_packages_deprecated_dependencies_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200210135504_remove_packages_deprecated_dependencies.rb')
+require_migration!
RSpec.describe RemovePackagesDeprecatedDependencies do
let(:projects) { table(:projects) }
diff --git a/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb b/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
index a28ca12a10d..c267e419b42 100644
--- a/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
+++ b/spec/migrations/remove_records_without_group_from_webhooks_table_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
-require Rails.root.join('db', 'migrate', '20210325092215_add_not_valid_foreign_key_to_group_hooks.rb')
+require_migration!('add_not_valid_foreign_key_to_group_hooks')
RSpec.describe RemoveRecordsWithoutGroupFromWebhooksTable, schema: 20210330091751 do
let(:web_hooks) { table(:web_hooks) }
diff --git a/spec/migrations/remove_security_dashboard_feature_flag_spec.rb b/spec/migrations/remove_security_dashboard_feature_flag_spec.rb
index 77363aebfe6..fea7fe01cc7 100644
--- a/spec/migrations/remove_security_dashboard_feature_flag_spec.rb
+++ b/spec/migrations/remove_security_dashboard_feature_flag_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200214034836_remove_security_dashboard_feature_flag.rb')
+require_migration!
RSpec.describe RemoveSecurityDashboardFeatureFlag do
let(:feature_gates) { table(:feature_gates) }
diff --git a/spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb b/spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb
index 83a79ac9795..fcbf94812fb 100644
--- a/spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb
+++ b/spec/migrations/rename_security_dashboard_feature_flag_to_instance_security_dashboard_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200212014653_rename_security_dashboard_feature_flag_to_instance_security_dashboard.rb')
+require_migration!
RSpec.describe RenameSecurityDashboardFeatureFlagToInstanceSecurityDashboard do
let(:feature_gates) { table(:feature_gates) }
diff --git a/spec/migrations/rename_sitemap_namespace_spec.rb b/spec/migrations/rename_sitemap_namespace_spec.rb
index 83f0721c600..21b74587d50 100644
--- a/spec/migrations/rename_sitemap_namespace_spec.rb
+++ b/spec/migrations/rename_sitemap_namespace_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201102112206_rename_sitemap_namespace.rb')
+require_migration!
RSpec.describe RenameSitemapNamespace do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/rename_sitemap_root_namespaces_spec.rb b/spec/migrations/rename_sitemap_root_namespaces_spec.rb
index 7cbf27849b0..12a687194e0 100644
--- a/spec/migrations/rename_sitemap_root_namespaces_spec.rb
+++ b/spec/migrations/rename_sitemap_root_namespaces_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201019094741_rename_sitemap_root_namespaces.rb')
+require_migration!
RSpec.describe RenameSitemapRootNamespaces do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/reschedule_artifact_expiry_backfill_spec.rb b/spec/migrations/reschedule_artifact_expiry_backfill_spec.rb
index 532035849cb..c06ce3d5bea 100644
--- a/spec/migrations/reschedule_artifact_expiry_backfill_spec.rb
+++ b/spec/migrations/reschedule_artifact_expiry_backfill_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210224150506_reschedule_artifact_expiry_backfill.rb')
+require_migration!
RSpec.describe RescheduleArtifactExpiryBackfill, :migration do
let(:migration_class) { Gitlab::BackgroundMigration::BackfillArtifactExpiryDate }
diff --git a/spec/migrations/reseed_merge_trains_enabled_spec.rb b/spec/migrations/reseed_merge_trains_enabled_spec.rb
index 71ef0b47da9..14ed44151d3 100644
--- a/spec/migrations/reseed_merge_trains_enabled_spec.rb
+++ b/spec/migrations/reseed_merge_trains_enabled_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201112195322_reseed_merge_trains_enabled.rb')
+require_migration!
RSpec.describe ReseedMergeTrainsEnabled do
describe 'migrate' do
diff --git a/spec/migrations/reseed_repository_storages_weighted_spec.rb b/spec/migrations/reseed_repository_storages_weighted_spec.rb
index 8abad3c0d93..d7efff3dfba 100644
--- a/spec/migrations/reseed_repository_storages_weighted_spec.rb
+++ b/spec/migrations/reseed_repository_storages_weighted_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200509203901_reseed_repository_storages_weighted.rb')
+require_migration!
RSpec.describe ReseedRepositoryStoragesWeighted do
let(:storages) { { "foo" => {}, "baz" => {} } }
diff --git a/spec/migrations/retry_backfill_traversal_ids_spec.rb b/spec/migrations/retry_backfill_traversal_ids_spec.rb
new file mode 100644
index 00000000000..e5ebd4228ca
--- /dev/null
+++ b/spec/migrations/retry_backfill_traversal_ids_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210604070207_retry_backfill_traversal_ids.rb')
+
+RSpec.describe RetryBackfillTraversalIds, :migration do
+ include ReloadHelpers
+
+ let_it_be(: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/save_instance_administrators_group_id_spec.rb b/spec/migrations/save_instance_administrators_group_id_spec.rb
index cb11cd1653a..0846df18b5e 100644
--- a/spec/migrations/save_instance_administrators_group_id_spec.rb
+++ b/spec/migrations/save_instance_administrators_group_id_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200210092405_save_instance_administrators_group_id')
+require_migration!
RSpec.describe SaveInstanceAdministratorsGroupId do
let(:application_settings_table) { table(:application_settings) }
diff --git a/spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb b/spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb
index 37a769bbc52..7b71110e62d 100644
--- a/spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb
+++ b/spec/migrations/schedule_backfill_push_rules_id_in_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200325162730_schedule_backfill_push_rules_id_in_projects.rb')
+require_migration!
RSpec.describe ScheduleBackfillPushRulesIdInProjects do
let(:push_rules) { table(:push_rules) }
diff --git a/spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb b/spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb
index 9cd1b98fbd7..f2a0bdba32a 100644
--- a/spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb
+++ b/spec/migrations/schedule_blocked_by_links_replacement_second_try_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201102073808_schedule_blocked_by_links_replacement_second_try')
+require_migration!
RSpec.describe ScheduleBlockedByLinksReplacementSecondTry do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
diff --git a/spec/migrations/schedule_calculate_wiki_sizes_spec.rb b/spec/migrations/schedule_calculate_wiki_sizes_spec.rb
deleted file mode 100644
index 0af491d863b..00000000000
--- a/spec/migrations/schedule_calculate_wiki_sizes_spec.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190527194900_schedule_calculate_wiki_sizes.rb')
-
-RSpec.describe ScheduleCalculateWikiSizes do
- let(:migration_class) { Gitlab::BackgroundMigration::CalculateWikiSizes }
- let(:migration_name) { migration_class.to_s.demodulize }
-
- let(:namespaces) { table(:namespaces) }
- let(:projects) { table(:projects) }
- let(:project_statistics) { table(:project_statistics) }
- let(:namespace) { namespaces.create!(name: 'wiki-migration', path: 'wiki-migration') }
- let(:project1) { projects.create!(name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: namespace.id) }
- let(:project2) { projects.create!(name: 'wiki-project-2', path: 'wiki-project-2', namespace_id: namespace.id) }
- let(:project3) { projects.create!(name: 'wiki-project-3', path: 'wiki-project-3', namespace_id: namespace.id) }
-
- context 'when missing wiki sizes exist' do
- let!(:project_statistic1) { project_statistics.create!(project_id: project1.id, namespace_id: namespace.id, wiki_size: 1000) }
- let!(:project_statistic2) { project_statistics.create!(project_id: project2.id, namespace_id: namespace.id, wiki_size: nil) }
- let!(:project_statistic3) { project_statistics.create!(project_id: project3.id, namespace_id: namespace.id, wiki_size: nil) }
-
- it 'schedules a background migration' do
- freeze_time do
- migrate!
-
- expect(migration_name).to be_scheduled_delayed_migration(5.minutes, project_statistic2.id, project_statistic3.id)
- expect(BackgroundMigrationWorker.jobs.size).to eq 1
- end
- end
-
- it 'calculates missing wiki sizes', :sidekiq_inline do
- expect(project_statistic2.wiki_size).to be_nil
- expect(project_statistic3.wiki_size).to be_nil
-
- migrate!
-
- expect(project_statistic2.reload.wiki_size).not_to be_nil
- expect(project_statistic3.reload.wiki_size).not_to be_nil
- end
- end
-
- context 'when missing wiki sizes do not exist' do
- before do
- namespace = namespaces.create!(name: 'wiki-migration', path: 'wiki-migration')
- project = projects.create!(name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: namespace.id)
- project_statistics.create!(project_id: project.id, namespace_id: namespace.id, wiki_size: 1000)
- end
-
- it 'does not schedule a background migration' do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(BackgroundMigrationWorker.jobs.size).to eq 0
- end
- end
- end
- end
-end
diff --git a/spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb b/spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb
new file mode 100644
index 00000000000..888d306f893
--- /dev/null
+++ b/spec/migrations/schedule_disable_expiration_policies_linked_to_no_container_images_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe ScheduleDisableExpirationPoliciesLinkedToNoContainerImages do
+ let_it_be(:projects) { table(:projects) }
+ let_it_be(:container_expiration_policies) { table(:container_expiration_policies) }
+ let_it_be(:container_repositories) { table(:container_repositories) }
+ let_it_be(:namespaces) { table(:namespaces) }
+ let_it_be(:namespace) { namespaces.create!(name: 'test', path: 'test') }
+
+ let_it_be(:policy1) { create_expiration_policy(id: 1, enabled: true) }
+ let_it_be(:policy2) { create_expiration_policy(id: 2, enabled: false) }
+ let_it_be(:policy3) { create_expiration_policy(id: 3, enabled: false) }
+ let_it_be(:policy4) { create_expiration_policy(id: 4, enabled: true) }
+ let_it_be(:policy5) { create_expiration_policy(id: 5, enabled: false) }
+ let_it_be(:policy6) { create_expiration_policy(id: 6, enabled: false) }
+ let_it_be(:policy7) { create_expiration_policy(id: 7, enabled: true) }
+ let_it_be(:policy8) { create_expiration_policy(id: 8, enabled: true) }
+ let_it_be(:policy9) { create_expiration_policy(id: 9, enabled: true) }
+
+ it 'schedules background migrations', :aggregate_failures do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 4)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 7, 8)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(6.minutes, 9, 9)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(3)
+ end
+ end
+ end
+
+ def create_expiration_policy(id:, enabled:)
+ project = projects.create!(id: id, namespace_id: namespace.id, name: "gitlab-#{id}")
+ container_expiration_policies.create!(
+ enabled: enabled,
+ project_id: project.id
+ )
+ end
+end
diff --git a/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
index 539b9ee96a8..45bd5073d55 100644
--- a/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
+++ b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb')
+require_migration!
RSpec.describe ScheduleFillValidTimeForPagesDomainCertificates do
let(:migration_class) { described_class::MIGRATION }
diff --git a/spec/migrations/schedule_link_lfs_objects_projects_spec.rb b/spec/migrations/schedule_link_lfs_objects_projects_spec.rb
index 2384c5d5ce7..29c203c2c31 100644
--- a/spec/migrations/schedule_link_lfs_objects_projects_spec.rb
+++ b/spec/migrations/schedule_link_lfs_objects_projects_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200310075115_schedule_link_lfs_objects_projects.rb')
+require_migration!
RSpec.describe ScheduleLinkLfsObjectsProjects, :migration, :sidekiq do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb b/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb
index fa8dd38619a..319c0802f2c 100644
--- a/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb
+++ b/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe ScheduleMergeRequestCleanupSchedulesBackfill, :sidekiq, schema: 2
stub_const("#{described_class}::BATCH_SIZE", 2)
end
- it 'schdules BackfillMergeRequestCleanupSchedules background jobs' do
+ it 'schedules BackfillMergeRequestCleanupSchedules background jobs' do
Sidekiq::Testing.fake! do
migrate!
diff --git a/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
index 1d35da528e4..29e4cf05c2b 100644
--- a/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
+++ b/spec/migrations/schedule_migrate_pages_to_zip_storage_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210302150310_schedule_migrate_pages_to_zip_storage.rb')
+require_migration!
RSpec.describe ScheduleMigratePagesToZipStorage, :sidekiq_might_not_need_inline, schema: 20201231133921 do
let(:migration_class) { described_class::MIGRATION }
diff --git a/spec/migrations/schedule_migrate_security_scans_spec.rb b/spec/migrations/schedule_migrate_security_scans_spec.rb
index eb86a910611..ce926241ba6 100644
--- a/spec/migrations/schedule_migrate_security_scans_spec.rb
+++ b/spec/migrations/schedule_migrate_security_scans_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200217225719_schedule_migrate_security_scans.rb')
+require_migration!
RSpec.describe ScheduleMigrateSecurityScans, :sidekiq do
let(:migration) { described_class.new }
diff --git a/spec/migrations/schedule_migrate_u2f_webauthn_spec.rb b/spec/migrations/schedule_migrate_u2f_webauthn_spec.rb
index 5dc4d676063..48f098e34fc 100644
--- a/spec/migrations/schedule_migrate_u2f_webauthn_spec.rb
+++ b/spec/migrations/schedule_migrate_u2f_webauthn_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200929114107_schedule_migrate_u2f_webauthn.rb')
+require_migration!
RSpec.describe ScheduleMigrateU2fWebauthn do
let(:migration_name) { described_class::MIGRATION }
diff --git a/spec/migrations/schedule_pages_metadata_migration_spec.rb b/spec/migrations/schedule_pages_metadata_migration_spec.rb
index 94311237cf4..96fbc1f9f51 100644
--- a/spec/migrations/schedule_pages_metadata_migration_spec.rb
+++ b/spec/migrations/schedule_pages_metadata_migration_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191002031332_schedule_pages_metadata_migration')
+require_migration!
RSpec.describe SchedulePagesMetadataMigration do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/schedule_populate_issue_email_participants_spec.rb b/spec/migrations/schedule_populate_issue_email_participants_spec.rb
index a3f18617b70..3a7a4e4df1e 100644
--- a/spec/migrations/schedule_populate_issue_email_participants_spec.rb
+++ b/spec/migrations/schedule_populate_issue_email_participants_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201128210234_schedule_populate_issue_email_participants.rb')
+require_migration!
RSpec.describe SchedulePopulateIssueEmailParticipants do
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
diff --git a/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb b/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb
index 1f44df82ad0..3caab64a72d 100644
--- a/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb
+++ b/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190322132835_schedule_populate_merge_request_assignees_table.rb')
+require_migration!
RSpec.describe SchedulePopulateMergeRequestAssigneesTable do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb b/spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb
index faa993dfbc7..5f764a1ee8f 100644
--- a/spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb
+++ b/spec/migrations/schedule_populate_personal_snippet_statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200714075739_schedule_populate_personal_snippet_statistics.rb')
+require_migration!
RSpec.describe SchedulePopulatePersonalSnippetStatistics do
let(:users) { table(:users) }
diff --git a/spec/migrations/schedule_populate_project_snippet_statistics_spec.rb b/spec/migrations/schedule_populate_project_snippet_statistics_spec.rb
index d5f048ed5cb..4ac107c5202 100644
--- a/spec/migrations/schedule_populate_project_snippet_statistics_spec.rb
+++ b/spec/migrations/schedule_populate_project_snippet_statistics_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200709101408_schedule_populate_project_snippet_statistics.rb')
+require_migration!
RSpec.describe SchedulePopulateProjectSnippetStatistics do
let(:users) { table(:users) }
diff --git a/spec/migrations/schedule_populate_user_highest_roles_table_spec.rb b/spec/migrations/schedule_populate_user_highest_roles_table_spec.rb
index 32def8ab47d..0a2ee82b349 100644
--- a/spec/migrations/schedule_populate_user_highest_roles_table_spec.rb
+++ b/spec/migrations/schedule_populate_user_highest_roles_table_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200311130802_schedule_populate_user_highest_roles_table.rb')
+require_migration!
RSpec.describe SchedulePopulateUserHighestRolesTable do
let(:users) { table(:users) }
diff --git a/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb
index a02e00de1e3..380d107250b 100644
--- a/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb
+++ b/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200204113224_schedule_recalculate_project_authorizations_second_run.rb')
+require_migration!
RSpec.describe ScheduleRecalculateProjectAuthorizationsSecondRun do
let(:users_table) { table(:users) }
diff --git a/spec/migrations/schedule_recalculate_project_authorizations_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_spec.rb
index 378e6aa133d..a4400c2ac83 100644
--- a/spec/migrations/schedule_recalculate_project_authorizations_spec.rb
+++ b/spec/migrations/schedule_recalculate_project_authorizations_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200204113223_schedule_recalculate_project_authorizations.rb')
+require_migration!
RSpec.describe ScheduleRecalculateProjectAuthorizations do
let(:users_table) { table(:users) }
diff --git a/spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb
index 5328abf7ea7..302ae1d5ebe 100644
--- a/spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb
+++ b/spec/migrations/schedule_recalculate_project_authorizations_third_run_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200204113225_schedule_recalculate_project_authorizations_third_run.rb')
+require_migration!
RSpec.describe ScheduleRecalculateProjectAuthorizationsThirdRun do
let(:users_table) { table(:users) }
diff --git a/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb b/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb
new file mode 100644
index 00000000000..e7d1813e428
--- /dev/null
+++ b/spec/migrations/schedule_recalculate_uuid_on_vulnerabilities_occurrences2_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences2 do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:users) { table(:users) }
+ let(:user) { create_user! }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let(:vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ let(:different_vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: 'uuid-v4',
+ external_id: 'uuid-v4',
+ fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89',
+ name: 'Identifier for UUIDv4')
+ end
+
+ let(:vulnerability_for_uuidv4) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:vulnerability_for_uuidv5) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding1) do
+ create_finding!(
+ vulnerability_id: vulnerability_for_uuidv4.id,
+ project_id: project.id,
+ scanner_id: different_scanner.id,
+ primary_identifier_id: different_vulnerability_identifier.id,
+ location_fingerprint: 'fa18f432f1d56675f4098d318739c3cd5b14eb3e',
+ uuid: 'b3cc2518-5446-4dea-871c-89d5e999c1ac'
+ )
+ end
+
+ let!(:finding2) do
+ create_finding!(
+ vulnerability_id: vulnerability_for_uuidv5.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ location_fingerprint: '838574be0210968bf6b9f569df9c2576242cbf0a',
+ uuid: '77211ed6-7dff-5f6b-8c9a-da89ad0a9b60'
+ )
+ end
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migrations', :aggregate_failures do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, finding1.id, finding1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, finding2.id, finding2.id)
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ def create_finding!(
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, location_fingerprint:, uuid:)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: 'test',
+ severity: 7,
+ confidence: 7,
+ report_type: 0,
+ project_fingerprint: '123qweasdzxc',
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: 'test',
+ raw_metadata: 'test',
+ uuid: uuid
+ )
+ end
+
+ def create_user!(name: "Example User", email: "user@example.com", user_type: nil)
+ users.create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0
+ )
+ end
+end
diff --git a/spec/migrations/schedule_sync_issuables_state_id_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
index d22d636e084..5a7105a0c84 100644
--- a/spec/migrations/schedule_sync_issuables_state_id_spec.rb
+++ b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190214112022_schedule_sync_issuables_state_id.rb')
+require_migration!
RSpec.describe ScheduleSyncIssuablesStateId do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb
index 3d450f0e6bc..d8eaaa1df04 100644
--- a/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb
+++ b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190506135400_schedule_sync_issuables_state_id_where_nil')
+require_migration!
RSpec.describe ScheduleSyncIssuablesStateIdWhereNil do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb b/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
index deb7aae737a..8f265acccae 100644
--- a/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
+++ b/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200110121314_schedule_update_existing_subgroup_to_match_visibility_level_of_parent.rb')
+require_migration!
RSpec.describe ScheduleUpdateExistingSubgroupToMatchVisibilityLevelOfParent do
include MigrationHelpers::NamespacesHelpers
diff --git a/spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb b/spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb
index 74f24906e41..a839229ec22 100644
--- a/spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb
+++ b/spec/migrations/schedule_update_existing_users_that_require_two_factor_auth_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20201030121314_schedule_update_existing_users_that_require_two_factor_auth.rb')
+require_migration!
RSpec.describe ScheduleUpdateExistingUsersThatRequireTwoFactorAuth do
let(:users) { table(:users) }
diff --git a/spec/migrations/schedule_update_timelogs_project_id_spec.rb b/spec/migrations/schedule_update_timelogs_project_id_spec.rb
index e2972d2fd08..b9130fd86be 100644
--- a/spec/migrations/schedule_update_timelogs_project_id_spec.rb
+++ b/spec/migrations/schedule_update_timelogs_project_id_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20210427212034_schedule_update_timelogs_project_id.rb')
+require_migration!
RSpec.describe ScheduleUpdateTimelogsProjectId do
let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
diff --git a/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb b/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb
index cec141cacc9..2fe739659f0 100644
--- a/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb
+++ b/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20210519154058_schedule_update_users_where_two_factor_auth_required_from_group.rb')
+require_migration!
RSpec.describe ScheduleUpdateUsersWhereTwoFactorAuthRequiredFromGroup do
let(:users) { table(:users) }
diff --git a/spec/migrations/seed_merge_trains_enabled_spec.rb b/spec/migrations/seed_merge_trains_enabled_spec.rb
index 2abb064a111..1cb0e3cf8a6 100644
--- a/spec/migrations/seed_merge_trains_enabled_spec.rb
+++ b/spec/migrations/seed_merge_trains_enabled_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201026200736_seed_merge_trains_enabled.rb')
+require_migration!
RSpec.describe SeedMergeTrainsEnabled do
describe 'migrate' do
diff --git a/spec/migrations/seed_repository_storages_weighted_spec.rb b/spec/migrations/seed_repository_storages_weighted_spec.rb
index d2fb1f7a014..102107bcc9f 100644
--- a/spec/migrations/seed_repository_storages_weighted_spec.rb
+++ b/spec/migrations/seed_repository_storages_weighted_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200526000407_seed_repository_storages_weighted.rb')
+require_migration!
RSpec.describe SeedRepositoryStoragesWeighted do
let(:storages) { { "foo" => {}, "baz" => {} } }
diff --git a/spec/migrations/services_remove_temporary_index_on_project_id_spec.rb b/spec/migrations/services_remove_temporary_index_on_project_id_spec.rb
index 6cab4c16cfb..d47f6deb2d5 100644
--- a/spec/migrations/services_remove_temporary_index_on_project_id_spec.rb
+++ b/spec/migrations/services_remove_temporary_index_on_project_id_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200203104214_services_remove_temporary_index_on_project_id.rb')
+require_migration!
RSpec.describe ServicesRemoveTemporaryIndexOnProjectId do
let(:migration_instance) { described_class.new }
diff --git a/spec/migrations/set_issue_id_for_all_versions_spec.rb b/spec/migrations/set_issue_id_for_all_versions_spec.rb
index 0908d054e70..78bc4bbce1c 100644
--- a/spec/migrations/set_issue_id_for_all_versions_spec.rb
+++ b/spec/migrations/set_issue_id_for_all_versions_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190715043954_set_issue_id_for_all_versions.rb')
+require_migration!
RSpec.describe SetIssueIdForAllVersions do
let(:projects) { table(:projects) }
diff --git a/spec/migrations/set_job_waiter_ttl_spec.rb b/spec/migrations/set_job_waiter_ttl_spec.rb
index b9cf7c55798..a051f8a535c 100644
--- a/spec/migrations/set_job_waiter_ttl_spec.rb
+++ b/spec/migrations/set_job_waiter_ttl_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200930144340_set_job_waiter_ttl.rb')
+require_migration!
RSpec.describe SetJobWaiterTtl, :redis do
it 'sets TTLs where necessary' do
diff --git a/spec/migrations/sync_issuables_state_id_spec.rb b/spec/migrations/sync_issuables_state_id_spec.rb
index dcddbca6a36..67403893f74 100644
--- a/spec/migrations/sync_issuables_state_id_spec.rb
+++ b/spec/migrations/sync_issuables_state_id_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20190911251732_sync_issuables_state_id')
+require_migration!
RSpec.describe SyncIssuablesStateId do
let(:migration) { described_class.new }
diff --git a/spec/migrations/truncate_user_fullname_spec.rb b/spec/migrations/truncate_user_fullname_spec.rb
index cb95c222790..dc5bef06cdc 100644
--- a/spec/migrations/truncate_user_fullname_spec.rb
+++ b/spec/migrations/truncate_user_fullname_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20190325080727_truncate_user_fullname.rb')
+require_migration!
RSpec.describe TruncateUserFullname do
let(:users) { table(:users) }
diff --git a/spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb b/spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb
index cae2f331f6f..5adc866d0a5 100644
--- a/spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb
+++ b/spec/migrations/unconfirm_wrongfully_verified_emails_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200615111857_unconfirm_wrongfully_verified_emails.rb')
+require_migration!
RSpec.describe UnconfirmWrongfullyVerifiedEmails do
before do
diff --git a/spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb b/spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb
index 8b241d1b28e..be209536208 100644
--- a/spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb
+++ b/spec/migrations/update_application_setting_npm_package_requests_forwarding_default_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200221105436_update_application_setting_npm_package_requests_forwarding_default.rb')
+require_migration!
RSpec.describe UpdateApplicationSettingNpmPackageRequestsForwardingDefault do
# Create test data - pipeline and CI/CD jobs.
diff --git a/spec/migrations/update_fingerprint_sha256_within_keys_spec.rb b/spec/migrations/update_fingerprint_sha256_within_keys_spec.rb
index 7f5ae892391..22ec3135703 100644
--- a/spec/migrations/update_fingerprint_sha256_within_keys_spec.rb
+++ b/spec/migrations/update_fingerprint_sha256_within_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200106071113_update_fingerprint_sha256_within_keys.rb')
+require_migration!
RSpec.describe UpdateFingerprintSha256WithinKeys do
let(:key_table) { table(:keys) }
diff --git a/spec/migrations/update_historical_data_recorded_at_spec.rb b/spec/migrations/update_historical_data_recorded_at_spec.rb
index bccc711f339..95d2bb989fd 100644
--- a/spec/migrations/update_historical_data_recorded_at_spec.rb
+++ b/spec/migrations/update_historical_data_recorded_at_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201022094846_update_historical_data_recorded_at.rb')
+require_migration!
RSpec.describe UpdateHistoricalDataRecordedAt do
let(:historical_data_table) { table(:historical_data) }
diff --git a/spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb b/spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb
index 71dd3a53bcd..d7d1781aaa2 100644
--- a/spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb
+++ b/spec/migrations/update_internal_ids_last_value_for_epics_renamed_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20201208081429_update_internal_ids_last_value_for_epics_renamed.rb')
+require_migration!
RSpec.describe UpdateInternalIdsLastValueForEpicsRenamed, :migration, schema: 20201124185639 do
let(:namespaces) { table(:namespaces) }
diff --git a/spec/migrations/update_minimum_password_length_spec.rb b/spec/migrations/update_minimum_password_length_spec.rb
index a4485dfbcea..02254ba1343 100644
--- a/spec/migrations/update_minimum_password_length_spec.rb
+++ b/spec/migrations/update_minimum_password_length_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20191205084057_update_minimum_password_length')
+require_migration!
RSpec.describe UpdateMinimumPasswordLength do
let(:application_settings) { table(:application_settings) }
diff --git a/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb b/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb
index ffab5c40182..74e97b82363 100644
--- a/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb
+++ b/spec/migrations/update_routes_for_lost_and_found_group_and_orphaned_projects_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'post_migrate', '20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb')
+require_migration!
RSpec.describe UpdateRoutesForLostAndFoundGroupAndOrphanedProjects, :migration do
let(:users) { table(:users) }
diff --git a/spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb b/spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb
index f55d55f94c7..0210f23f5c5 100644
--- a/spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb
+++ b/spec/migrations/update_timestamp_softwarelicensespolicy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20200116175538_update_timestamp_softwarelicensespolicy.rb')
+require_migration!
RSpec.describe UpdateTimestampSoftwarelicensespolicy do
let(:software_licenses_policy) { table(:software_license_policies) }
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index 24de46cb536..85a6717d259 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -132,5 +132,47 @@ RSpec.describe ApplicationRecord do
end.to raise_error(ActiveRecord::QueryCanceled)
end
end
+
+ context 'with database load balancing' do
+ let(:session) { double(:session) }
+
+ before do
+ allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(session)
+ allow(session).to receive(:fallback_to_replicas_for_ambiguous_queries).and_yield
+ end
+
+ it 'yields control' do
+ expect do |blk|
+ described_class.with_fast_read_statement_timeout(&blk)
+ end.to yield_control.once
+ end
+
+ context 'when the query runs faster than configured timeout' do
+ it 'executes the query without error' do
+ result = nil
+
+ expect do
+ described_class.with_fast_read_statement_timeout(100) do
+ result = described_class.connection.exec_query('SELECT 1')
+ end
+ end.not_to raise_error
+
+ expect(result).not_to be_nil
+ end
+ end
+
+ # This query hangs for 10ms and then gets cancelled. As there is no
+ # other way to test the timeout for sure, 10ms of waiting seems to be
+ # reasonable!
+ context 'when the query runs longer than configured timeout' do
+ it 'cancels the query and raiss an exception' do
+ expect do
+ described_class.with_fast_read_statement_timeout(10) do
+ described_class.connection.exec_query('SELECT pg_sleep(0.1)')
+ end
+ end.to raise_error(ActiveRecord::QueryCanceled)
+ end
+ end
+ end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c13d83d1685..4e72d558b52 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -990,6 +990,34 @@ RSpec.describe ApplicationSetting do
end
end
end
+
+ describe '#diff_max_files' do
+ context 'validations' do
+ it { is_expected.to validate_presence_of(:diff_max_files) }
+
+ specify do
+ is_expected
+ .to validate_numericality_of(:diff_max_files)
+ .only_integer
+ .is_greater_than_or_equal_to(Commit::DEFAULT_MAX_DIFF_FILES_SETTING)
+ .is_less_than_or_equal_to(Commit::MAX_DIFF_FILES_SETTING_UPPER_BOUND)
+ end
+ end
+ end
+
+ describe '#diff_max_lines' do
+ context 'validations' do
+ it { is_expected.to validate_presence_of(:diff_max_lines) }
+
+ specify do
+ is_expected
+ .to validate_numericality_of(:diff_max_lines)
+ .only_integer
+ .is_greater_than_or_equal_to(Commit::DEFAULT_MAX_DIFF_LINES_SETTING)
+ .is_less_than_or_equal_to(Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND)
+ end
+ end
+ end
end
describe '#sourcegraph_url_is_com?' do
diff --git a/spec/models/bulk_imports/export_status_spec.rb b/spec/models/bulk_imports/export_status_spec.rb
new file mode 100644
index 00000000000..48f32a2092a
--- /dev/null
+++ b/spec/models/bulk_imports/export_status_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::ExportStatus do
+ let_it_be(:relation) { 'labels' }
+ let_it_be(:import) { create(:bulk_import) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: import) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: import, source_full_path: 'foo') }
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+
+ let(:response_double) do
+ double(parsed_response: [{ 'relation' => 'labels', 'status' => status, 'error' => 'error!' }])
+ end
+
+ subject { described_class.new(tracker, relation) }
+
+ before do
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ allow(client).to receive(:get).and_return(response_double)
+ end
+ end
+
+ describe '#started?' do
+ context 'when export status is started' do
+ let(:status) { BulkImports::Export::STARTED }
+
+ it 'returns true' do
+ expect(subject.started?).to eq(true)
+ end
+ end
+
+ context 'when export status is not started' do
+ let(:status) { BulkImports::Export::FAILED }
+
+ it 'returns false' do
+ expect(subject.started?).to eq(false)
+ end
+ end
+ end
+
+ describe '#failed' do
+ context 'when export status is failed' do
+ let(:status) { BulkImports::Export::FAILED }
+
+ it 'returns true' do
+ expect(subject.failed?).to eq(true)
+ end
+ end
+
+ context 'when export status is not failed' do
+ let(:status) { BulkImports::Export::STARTED }
+
+ it 'returns false' do
+ expect(subject.failed?).to eq(false)
+ end
+ end
+ end
+
+ describe '#error' do
+ let(:status) { BulkImports::Export::FAILED }
+
+ it 'returns error message' do
+ expect(subject.error).to eq('error!')
+ end
+
+ context 'when something goes wrong during export status fetch' do
+ it 'returns exception class as error' do
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ allow(client).to receive(:get).and_raise(StandardError, 'Error!')
+ end
+
+ expect(subject.error).to eq('Error!')
+ end
+ end
+ end
+end
diff --git a/spec/models/bulk_imports/export_upload_spec.rb b/spec/models/bulk_imports/export_upload_spec.rb
index 641fa4a1b6c..d9ae41af0db 100644
--- a/spec/models/bulk_imports/export_upload_spec.rb
+++ b/spec/models/bulk_imports/export_upload_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe BulkImports::ExportUpload do
method = 'export_file'
filename = 'labels.ndjson.gz'
- subject.public_send("#{method}=", fixture_file_upload("spec/fixtures/bulk_imports/#{filename}"))
+ subject.public_send("#{method}=", fixture_file_upload("spec/fixtures/bulk_imports/gz/#{filename}"))
subject.save!
url = "/uploads/-/system/bulk_imports/export_upload/export_file/#{subject.id}/#{filename}"
diff --git a/spec/models/bulk_imports/file_transfer/group_config_spec.rb b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
index 21da71de3c7..4611a00b0cc 100644
--- a/spec/models/bulk_imports/file_transfer/group_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/group_config_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe BulkImports::FileTransfer::GroupConfig do
subject { described_class.new(exportable) }
- describe '#exportable_tree' do
- it 'returns exportable tree' do
+ describe '#portable_tree' do
+ it 'returns portable tree' do
expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder|
expect(finder).to receive(:find_root).with(:group).and_call_original
end
@@ -30,9 +30,21 @@ RSpec.describe BulkImports::FileTransfer::GroupConfig do
end
end
- describe '#exportable_relations' do
+ describe '#portable_relations' do
it 'returns a list of top level exportable relations' do
expect(subject.portable_relations).to include('milestones', 'badges', 'boards', 'labels')
end
end
+
+ describe '#top_relation_tree' do
+ it 'returns relation tree of a top level relation' do
+ expect(subject.top_relation_tree('labels')).to eq('priorities' => {})
+ end
+ end
+
+ describe '#relation_excluded_keys' do
+ it 'returns excluded keys for relation' do
+ expect(subject.relation_excluded_keys('group')).to include('owner_id')
+ end
+ end
end
diff --git a/spec/models/bulk_imports/file_transfer/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
index 021f96ac2a3..2995556a58d 100644
--- a/spec/models/bulk_imports/file_transfer/project_config_spec.rb
+++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb
@@ -12,8 +12,8 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
subject { described_class.new(exportable) }
- describe '#exportable_tree' do
- it 'returns exportable tree' do
+ describe 'portable_tree' do
+ it 'returns portable tree' do
expect_next_instance_of(::Gitlab::ImportExport::AttributesFinder) do |finder|
expect(finder).to receive(:find_root).with(:project).and_call_original
end
@@ -30,9 +30,21 @@ RSpec.describe BulkImports::FileTransfer::ProjectConfig do
end
end
- describe '#exportable_relations' do
+ describe '#portable_relations' do
it 'returns a list of top level exportable relations' do
expect(subject.portable_relations).to include('issues', 'labels', 'milestones', 'merge_requests')
end
end
+
+ describe '#top_relation_tree' do
+ it 'returns relation tree of a top level relation' do
+ expect(subject.top_relation_tree('labels')).to eq('priorities' => {})
+ end
+ end
+
+ describe '#relation_excluded_keys' do
+ it 'returns excluded keys for relation' do
+ expect(subject.relation_excluded_keys('project')).to include('creator_id')
+ end
+ end
end
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index d00d88ae397..331ba9953ca 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -187,15 +187,6 @@ RSpec.describe Ci::BuildDependencies do
it { expect(cross_pipeline_deps).to contain_exactly(upstream_job) }
it { is_expected.to be_valid }
end
-
- context 'when feature flag `ci_cross_pipeline_artifacts_download` is disabled' do
- before do
- stub_feature_flags(ci_cross_pipeline_artifacts_download: false)
- end
-
- it { expect(cross_pipeline_deps).to be_empty }
- it { is_expected.to be_valid }
- end
end
context 'when same job names exist in other pipelines in the hierarchy' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 66d2f5f4ee9..62dec522161 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -111,10 +111,6 @@ RSpec.describe Ci::Build do
describe '.with_downloadable_artifacts' do
subject { described_class.with_downloadable_artifacts }
- before do
- stub_feature_flags(drop_license_management_artifact: false)
- end
-
context 'when job does not have a downloadable artifact' do
let!(:job) { create(:ci_build) }
@@ -320,11 +316,23 @@ RSpec.describe Ci::Build do
end
end
+ describe '#stick_build_if_status_changed' do
+ it 'sticks the build if the status changed' do
+ job = create(:ci_build, :pending)
+
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?)
+ .and_return(true)
+
+ expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:stick)
+ .with(:build, job.id)
+
+ job.update!(status: :running)
+ end
+ end
+
describe '#enqueue' do
let(:build) { create(:ci_build, :created) }
- subject { build.enqueue }
-
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_prerequisites)
allow(Ci::PrepareBuildService).to receive(:perform_async)
@@ -334,28 +342,74 @@ RSpec.describe Ci::Build do
let(:has_prerequisites) { true }
it 'transitions to preparing' do
- subject
+ build.enqueue
expect(build).to be_preparing
end
+
+ it 'does not push build to the queue' do
+ build.enqueue
+
+ expect(build.queuing_entry).not_to be_present
+ end
end
context 'build has no prerequisites' do
let(:has_prerequisites) { false }
it 'transitions to pending' do
- subject
+ build.enqueue
expect(build).to be_pending
end
+
+ it 'pushes build to a queue' do
+ build.enqueue
+
+ expect(build.queuing_entry).to be_present
+ end
+
+ context 'when build status transition fails' do
+ before do
+ ::Ci::Build.find(build.id).update_column(:lock_version, 100)
+ end
+
+ it 'does not push build to a queue' do
+ expect { build.enqueue! }
+ .to raise_error(ActiveRecord::StaleObjectError)
+
+ expect(build.queuing_entry).not_to be_present
+ end
+ end
+
+ context 'when there is a queuing entry already present' do
+ before do
+ ::Ci::PendingBuild.create!(build: build, project: build.project)
+ end
+
+ it 'does not raise an error' do
+ expect { build.enqueue! }.not_to raise_error
+ expect(build.reload.queuing_entry).to be_present
+ end
+ end
+
+ context 'when both failure scenario happen at the same time' do
+ before do
+ ::Ci::Build.find(build.id).update_column(:lock_version, 100)
+ ::Ci::PendingBuild.create!(build: build, project: build.project)
+ end
+
+ it 'raises stale object error exception' do
+ expect { build.enqueue! }
+ .to raise_error(ActiveRecord::StaleObjectError)
+ end
+ end
end
end
describe '#enqueue_preparing' do
let(:build) { create(:ci_build, :preparing) }
- subject { build.enqueue_preparing }
-
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_unmet_prerequisites)
end
@@ -364,9 +418,10 @@ RSpec.describe Ci::Build do
let(:has_unmet_prerequisites) { false }
it 'transitions to pending' do
- subject
+ build.enqueue_preparing
expect(build).to be_pending
+ expect(build.queuing_entry).to be_present
end
end
@@ -374,9 +429,10 @@ RSpec.describe Ci::Build do
let(:has_unmet_prerequisites) { true }
it 'remains in preparing' do
- subject
+ build.enqueue_preparing
expect(build).to be_preparing
+ expect(build.queuing_entry).not_to be_present
end
end
end
@@ -405,6 +461,64 @@ RSpec.describe Ci::Build do
end
end
+ describe '#run' do
+ context 'when build has been just created' do
+ let(:build) { create(:ci_build, :created) }
+
+ it 'creates queuing entry and then removes it' do
+ build.enqueue!
+ expect(build.queuing_entry).to be_present
+
+ build.run!
+ expect(build.reload.queuing_entry).not_to be_present
+ end
+ end
+
+ context 'when build status transition fails' do
+ let(:build) { create(:ci_build, :pending) }
+
+ before do
+ ::Ci::PendingBuild.create!(build: build, project: build.project)
+ ::Ci::Build.find(build.id).update_column(:lock_version, 100)
+ end
+
+ it 'does not remove build from a queue' do
+ expect { build.run! }
+ .to raise_error(ActiveRecord::StaleObjectError)
+
+ expect(build.queuing_entry).to be_present
+ end
+ end
+
+ context 'when build has been picked by a shared runner' do
+ let(:build) { create(:ci_build, :pending) }
+
+ it 'creates runtime metadata entry' do
+ build.runner = create(:ci_runner, :instance_type)
+
+ build.run!
+
+ expect(build.reload.runtime_metadata).to be_present
+ end
+ end
+ end
+
+ describe '#drop' do
+ context 'when has a runtime tracking entry' do
+ let(:build) { create(:ci_build, :pending) }
+
+ it 'removes runtime tracking entry' do
+ build.runner = create(:ci_runner, :instance_type)
+
+ build.run!
+ expect(build.reload.runtime_metadata).to be_present
+
+ build.drop!
+ expect(build.reload.runtime_metadata).not_to be_present
+ end
+ end
+ end
+
describe '#schedulable?' do
subject { build.schedulable? }
@@ -586,28 +700,10 @@ RSpec.describe Ci::Build do
end
end
- context 'with runners_cached_states feature flag enabled' do
- before do
- stub_feature_flags(runners_cached_states: true)
- end
-
- it 'caches the result in Redis' do
- expect(Rails.cache).to receive(:fetch).with(['has-online-runners', build.id], expires_in: 1.minute)
-
- build.any_runners_online?
- end
- end
-
- context 'with runners_cached_states feature flag disabled' do
- before do
- stub_feature_flags(runners_cached_states: false)
- end
-
- it 'does not cache' do
- expect(Rails.cache).not_to receive(:fetch).with(['has-online-runners', build.id], expires_in: 1.minute)
+ it 'caches the result in Redis' do
+ expect(Rails.cache).to receive(:fetch).with(['has-online-runners', build.id], expires_in: 1.minute)
- build.any_runners_online?
- end
+ build.any_runners_online?
end
end
@@ -624,28 +720,10 @@ RSpec.describe Ci::Build do
it { is_expected.to be_truthy }
end
- context 'with runners_cached_states feature flag enabled' do
- before do
- stub_feature_flags(runners_cached_states: true)
- end
-
- it 'caches the result in Redis' do
- expect(Rails.cache).to receive(:fetch).with(['has-available-runners', build.project.id], expires_in: 1.minute)
-
- build.any_runners_available?
- end
- end
-
- context 'with runners_cached_states feature flag disabled' do
- before do
- stub_feature_flags(runners_cached_states: false)
- end
-
- it 'does not cache' do
- expect(Rails.cache).not_to receive(:fetch).with(['has-available-runners', build.project.id], expires_in: 1.minute)
+ it 'caches the result in Redis' do
+ expect(Rails.cache).to receive(:fetch).with(['has-available-runners', build.project.id], expires_in: 1.minute)
- build.any_runners_available?
- end
+ build.any_runners_available?
end
end
@@ -1650,8 +1728,6 @@ RSpec.describe Ci::Build do
subject { build.erase_erasable_artifacts! }
before do
- stub_feature_flags(drop_license_management_artifact: false)
-
Ci::JobArtifact.file_types.keys.each do |file_type|
create(:ci_job_artifact, job: build, file_type: file_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[file_type.to_sym])
end
@@ -1840,6 +1916,26 @@ RSpec.describe Ci::Build do
it { is_expected.not_to be_retryable }
end
+
+ context 'when a canceled build has been retried already' do
+ before do
+ project.add_developer(user)
+ build.cancel!
+ described_class.retry(build, user)
+ end
+
+ context 'when prevent_retry_of_retried_jobs feature flag is enabled' do
+ it { is_expected.not_to be_retryable }
+ end
+
+ context 'when prevent_retry_of_retried_jobs feature flag is disabled' do
+ before do
+ stub_feature_flags(prevent_retry_of_retried_jobs: false)
+ end
+
+ it { is_expected.to be_retryable }
+ end
+ end
end
end
@@ -2525,7 +2621,6 @@ RSpec.describe Ci::Build do
{ key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false },
{ key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: project.repository_languages.map(&:name).join(',').downcase, public: true, masked: false },
{ key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false },
- { key: 'CI_PROJECT_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false },
{ key: 'CI_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false },
{ key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false },
{ key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false },
@@ -2566,6 +2661,17 @@ RSpec.describe Ci::Build do
it { is_expected.to be_instance_of(Gitlab::Ci::Variables::Collection) }
it { expect(subject.to_runner_variables).to eq(predefined_variables) }
+ it 'excludes variables that require an environment or user' do
+ environment_based_variables_collection = subject.filter do |variable|
+ %w[
+ YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG
+ CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_URL
+ ].include?(variable[:key])
+ end
+
+ expect(environment_based_variables_collection).to be_empty
+ end
+
context 'when ci_job_jwt feature flag is disabled' do
before do
stub_feature_flags(ci_job_jwt: false)
@@ -2635,7 +2741,7 @@ RSpec.describe Ci::Build do
let(:expected_variables) do
predefined_variables.map { |variable| variable.fetch(:key) } +
%w[YAML_VARIABLE CI_ENVIRONMENT_NAME CI_ENVIRONMENT_SLUG
- CI_ENVIRONMENT_URL]
+ CI_ENVIRONMENT_TIER CI_ENVIRONMENT_ACTION CI_ENVIRONMENT_URL]
end
before do
@@ -2653,6 +2759,50 @@ RSpec.describe Ci::Build do
expect(received_variables).to eq expected_variables
end
+
+ describe 'CI_ENVIRONMENT_ACTION' do
+ let(:enviroment_action_variable) { subject.find { |variable| variable[:key] == 'CI_ENVIRONMENT_ACTION' } }
+
+ shared_examples 'defaults value' do
+ it 'value matches start' do
+ expect(enviroment_action_variable[:value]).to eq('start')
+ end
+ end
+
+ it_behaves_like 'defaults value'
+
+ context 'when options is set' do
+ before do
+ build.update!(options: options)
+ end
+
+ context 'when options is empty' do
+ let(:options) { {} }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options is nil' do
+ let(:options) { nil }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options environment is specified' do
+ let(:options) { { environment: {} } }
+
+ it_behaves_like 'defaults value'
+ end
+
+ context 'when options environment action specified' do
+ let(:options) { { environment: { action: 'stop' } } }
+
+ it 'matches the specified action' do
+ expect(enviroment_action_variable[:value]).to eq('stop')
+ end
+ end
+ end
+ end
end
end
end
@@ -2691,7 +2841,8 @@ RSpec.describe Ci::Build do
let(:environment_variables) do
[
{ key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true, masked: false },
- { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true, masked: false }
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true, masked: false },
+ { key: 'CI_ENVIRONMENT_TIER', value: 'production', public: true, masked: false }
]
end
@@ -2700,6 +2851,7 @@ RSpec.describe Ci::Build do
project: build.project,
name: 'production',
slug: 'prod-slug',
+ tier: 'production',
external_url: '')
end
@@ -4693,7 +4845,7 @@ RSpec.describe Ci::Build do
context 'with project services' do
before do
- create(:service, active: true, job_events: true, project: project)
+ create(:integration, active: true, job_events: true, project: project)
end
it 'executes services' do
@@ -4707,7 +4859,7 @@ RSpec.describe Ci::Build do
context 'without relevant project services' do
before do
- create(:service, active: true, job_events: false, project: project)
+ create(:integration, active: true, job_events: false, project: project)
end
it 'does not execute services' do
@@ -4987,4 +5139,113 @@ RSpec.describe Ci::Build do
it { is_expected.to be_truthy }
end
end
+
+ describe '.build_matchers' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :protected) }
+
+ subject(:matchers) { pipeline.builds.build_matchers(pipeline.project) }
+
+ context 'when the pipeline is empty' do
+ it 'does not throw errors' do
+ is_expected.to eq([])
+ end
+ end
+
+ context 'when the pipeline has builds' do
+ let_it_be(:build_without_tags) do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ let_it_be(:build_with_tags) do
+ create(:ci_build, pipeline: pipeline, tag_list: %w[tag1 tag2])
+ end
+
+ let_it_be(:other_build_with_tags) do
+ create(:ci_build, pipeline: pipeline, tag_list: %w[tag2 tag1])
+ end
+
+ it { expect(matchers.size).to eq(2) }
+
+ it 'groups build ids' do
+ expect(matchers.map(&:build_ids)).to match_array([
+ [build_without_tags.id],
+ match_array([build_with_tags.id, other_build_with_tags.id])
+ ])
+ end
+
+ it { expect(matchers.map(&:tag_list)).to match_array([[], %w[tag1 tag2]]) }
+
+ it { expect(matchers.map(&:protected?)).to all be_falsey }
+
+ context 'when the builds are protected' do
+ before do
+ pipeline.builds.update_all(protected: true)
+ end
+
+ it { expect(matchers).to all be_protected }
+ end
+ end
+ end
+
+ describe '#build_matcher' do
+ let_it_be(:build) do
+ build_stubbed(:ci_build, tag_list: %w[tag1 tag2])
+ end
+
+ subject(:matcher) { build.build_matcher }
+
+ it { expect(matcher.build_ids).to eq([build.id]) }
+
+ it { expect(matcher.tag_list).to match_array(%w[tag1 tag2]) }
+
+ it { expect(matcher.protected?).to eq(build.protected?) }
+
+ it { expect(matcher.project).to eq(build.project) }
+ end
+
+ describe '#shared_runner_build?' do
+ context 'when build does not have a runner assigned' do
+ it 'is not a shared runner build' do
+ expect(build.runner).to be_nil
+
+ expect(build).not_to be_shared_runner_build
+ end
+ end
+
+ context 'when build has a project runner assigned' do
+ before do
+ build.runner = create(:ci_runner, :project)
+ end
+
+ it 'is not a shared runner build' do
+ expect(build).not_to be_shared_runner_build
+ end
+ end
+
+ context 'when build has an instance runner assigned' do
+ before do
+ build.runner = create(:ci_runner, :instance_type)
+ end
+
+ it 'is a shared runner build' do
+ expect(build).to be_shared_runner_build
+ end
+ end
+ end
+
+ describe '.without_coverage' do
+ let!(:build_with_coverage) { create(:ci_build, pipeline: pipeline, coverage: 100.0) }
+
+ it 'returns builds without coverage values' do
+ expect(described_class.without_coverage).to eq([build])
+ end
+ end
+
+ describe '.with_coverage_regex' do
+ let!(:build_with_coverage_regex) { create(:ci_build, pipeline: pipeline, coverage_regex: '\d') }
+
+ it 'returns builds with coverage regex values' do
+ expect(described_class.with_coverage_regex).to eq([build_with_coverage_regex])
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 12bc5d9aa3c..a16453f3d01 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -2,13 +2,13 @@
require 'spec_helper'
-RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
+RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_gitlab_redis_trace_chunks do
include ExclusiveLeaseHelpers
let_it_be(:build) { create(:ci_build, :running) }
let(:chunk_index) { 0 }
- let(:data_store) { :redis }
+ let(:data_store) { :redis_trace_chunks }
let(:raw_data) { nil }
let(:build_trace_chunk) do
@@ -18,10 +18,17 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
it_behaves_like 'having unique enum values'
before do
- stub_feature_flags(ci_enable_live_trace: true, gitlab_ci_trace_read_consistency: true)
+ stub_feature_flags(ci_enable_live_trace: true)
stub_artifacts_object_storage
end
+ def redis_instance
+ {
+ redis: Gitlab::Redis::SharedState,
+ redis_trace_chunks: Gitlab::Redis::TraceChunks
+ }[data_store]
+ end
+
describe 'chunk creation' do
let(:metrics) { spy('metrics') }
@@ -85,7 +92,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
def external_data_counter
- Gitlab::Redis::SharedState.with do |redis|
+ redis_instance.with do |redis|
redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size
end
end
@@ -101,24 +108,16 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
subject { described_class.all_stores }
it 'returns a correctly ordered array' do
- is_expected.to eq(%i[redis database fog])
- end
-
- it 'returns redis store as the lowest precedence' do
- expect(subject.first).to eq(:redis)
- end
-
- it 'returns fog store as the highest precedence' do
- expect(subject.last).to eq(:fog)
+ is_expected.to eq(%i[redis database fog redis_trace_chunks])
end
end
describe '#data' do
subject { build_trace_chunk.data }
- context 'when data_store is redis' do
- let(:data_store) { :redis }
+ where(:data_store) { %i[redis redis_trace_chunks] }
+ with_them do
before do
build_trace_chunk.send(:unsafe_set_data!, +'Sample data in redis')
end
@@ -148,6 +147,22 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
+ describe '#data_store' do
+ subject { described_class.new.data_store }
+
+ context 'default value' do
+ it { expect(subject).to eq('redis_trace_chunks') }
+
+ context 'when dedicated_redis_trace_chunks is disabled' do
+ before do
+ stub_feature_flags(dedicated_redis_trace_chunks: false)
+ end
+
+ it { expect(subject).to eq('redis') }
+ end
+ end
+ end
+
describe '#get_store_class' do
using RSpec::Parameterized::TableSyntax
@@ -155,6 +170,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
:redis | Ci::BuildTraceChunks::Redis
:database | Ci::BuildTraceChunks::Database
:fog | Ci::BuildTraceChunks::Fog
+ :redis_trace_chunks | Ci::BuildTraceChunks::RedisTraceChunks
end
with_them do
@@ -302,9 +318,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
- context 'when data_store is redis' do
- let(:data_store) { :redis }
+ where(:data_store) { %i[redis redis_trace_chunks] }
+ with_them do
context 'when there are no data' do
let(:data) { +'' }
@@ -441,8 +457,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
- context 'when data_store is redis' do
- let(:data_store) { :redis }
+ where(:data_store) { %i[redis redis_trace_chunks] }
+
+ with_them do
let(:data) { +'Sample data in redis' }
before do
@@ -475,9 +492,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
describe '#size' do
subject { build_trace_chunk.size }
- context 'when data_store is redis' do
- let(:data_store) { :redis }
+ where(:data_store) { %i[redis redis_trace_chunks] }
+ with_them do
context 'when data exists' do
let(:data) { +'Sample data in redis' }
@@ -537,9 +554,14 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
subject { build_trace_chunk.persist_data! }
- context 'when data_store is redis' do
- let(:data_store) { :redis }
+ where(:data_store, :redis_class) do
+ [
+ [:redis, Ci::BuildTraceChunks::Redis],
+ [:redis_trace_chunks, Ci::BuildTraceChunks::RedisTraceChunks]
+ ]
+ end
+ with_them do
context 'when data exists' do
before do
build_trace_chunk.send(:unsafe_set_data!, data)
@@ -549,15 +571,15 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { +'a' * described_class::CHUNK_SIZE }
it 'persists the data' do
- expect(build_trace_chunk.redis?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
+ expect(build_trace_chunk.data_store).to eq(data_store.to_s)
+ expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
subject
expect(build_trace_chunk.fog?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(redis_class.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
end
@@ -575,8 +597,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
it 'does not persist the data and the orignal data is intact' do
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
- expect(build_trace_chunk.redis?).to be_truthy
- expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
+ expect(build_trace_chunk.data_store).to eq(data_store.to_s)
+ expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
end
@@ -810,7 +832,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
shared_examples_for 'deletes all build_trace_chunk and data in redis' do
it 'deletes all build_trace_chunk and data in redis', :sidekiq_might_not_need_inline do
- Gitlab::Redis::SharedState.with do |redis|
+ redis_instance.with do |redis|
expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(3)
end
@@ -820,7 +842,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(described_class.count).to eq(0)
- Gitlab::Redis::SharedState.with do |redis|
+ redis_instance.with do |redis|
expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(0)
end
end
@@ -902,4 +924,38 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
end
+
+ describe '#live?' do
+ subject { build_trace_chunk.live? }
+
+ where(:data_store, :value) do
+ [
+ [:redis, true],
+ [:redis_trace_chunks, true],
+ [:database, false],
+ [:fog, false]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to eq(value) }
+ end
+ end
+
+ describe '#flushed?' do
+ subject { build_trace_chunk.flushed? }
+
+ where(:data_store, :value) do
+ [
+ [:redis, false],
+ [:redis_trace_chunks, false],
+ [:database, true],
+ [:fog, true]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to eq(value) }
+ end
+ end
end
diff --git a/spec/models/ci/build_trace_chunks/database_spec.rb b/spec/models/ci/build_trace_chunks/database_spec.rb
index 313328ac037..d99aede853c 100644
--- a/spec/models/ci/build_trace_chunks/database_spec.rb
+++ b/spec/models/ci/build_trace_chunks/database_spec.rb
@@ -5,12 +5,6 @@ require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::Database do
let(:data_store) { described_class.new }
- describe '#available?' do
- subject { data_store.available? }
-
- it { is_expected.to be_truthy }
- end
-
describe '#data' do
subject { data_store.data(model) }
diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb
index cb0b6baadeb..c004887d609 100644
--- a/spec/models/ci/build_trace_chunks/redis_spec.rb
+++ b/spec/models/ci/build_trace_chunks/redis_spec.rb
@@ -5,12 +5,6 @@ require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
let(:data_store) { described_class.new }
- describe '#available?' do
- subject { data_store.available? }
-
- it { is_expected.to be_truthy }
- end
-
describe '#data' do
subject { data_store.data(model) }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 3c4769764d5..582639b105e 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -328,35 +328,9 @@ RSpec.describe Ci::JobArtifact do
end
end
- describe 'validates if file format is supported' do
- subject { artifact }
-
- let(:artifact) { build(:ci_job_artifact, file_type: :license_management, file_format: :raw) }
-
- context 'when license_management is supported' do
- before do
- stub_feature_flags(drop_license_management_artifact: false)
- end
-
- it { is_expected.to be_valid }
- end
-
- context 'when license_management is not supported' do
- before do
- stub_feature_flags(drop_license_management_artifact: true)
- end
-
- it { is_expected.not_to be_valid }
- end
- end
-
describe 'validates file format' do
subject { artifact }
- before do
- stub_feature_flags(drop_license_management_artifact: false)
- end
-
described_class::TYPE_AND_FORMAT_PAIRS.except(:trace).each do |file_type, file_format|
context "when #{file_type} type with #{file_format} format" do
let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: file_format) }
diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb
new file mode 100644
index 00000000000..d18495b9312
--- /dev/null
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobToken::ProjectScopeLink do
+ it { is_expected.to belong_to(:source_project) }
+ it { is_expected.to belong_to(:target_project) }
+ it { is_expected.to belong_to(:added_by) }
+
+ let_it_be(:project) { create(:project) }
+
+ describe 'unique index' do
+ let!(:link) { create(:ci_job_token_project_scope_link) }
+
+ it 'raises an error' do
+ expect do
+ create(:ci_job_token_project_scope_link,
+ source_project: link.source_project,
+ target_project: link.target_project)
+ end.to raise_error(ActiveRecord::RecordNotUnique)
+ 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)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:source_project]).to contain_exactly("can't be blank")
+ end
+
+ it 'must have a target project', :aggregate_failures do
+ link = build(:ci_job_token_project_scope_link, target_project: nil)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:target_project]).to contain_exactly("can't be blank")
+ end
+
+ it 'must have a target project different than source project', :aggregate_failures do
+ link = build(:ci_job_token_project_scope_link, target_project: project, source_project: project)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:target_project]).to contain_exactly("can't be the same as the source project")
+ end
+ end
+
+ describe '.from_project' do
+ subject { described_class.from_project(project) }
+
+ let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
+ let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
+
+ it 'returns only the links having the given source project' do
+ expect(subject).to contain_exactly(source_link)
+ end
+ end
+
+ describe '.to_project' do
+ subject { described_class.to_project(project) }
+
+ let!(:source_link) { create(:ci_job_token_project_scope_link, source_project: project) }
+ let!(:target_link) { create(:ci_job_token_project_scope_link, target_project: project) }
+
+ it 'returns only the links having the given target project' do
+ expect(subject).to contain_exactly(target_link)
+ end
+ end
+end
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
new file mode 100644
index 00000000000..c731a2634f5
--- /dev/null
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::JobToken::Scope do
+ let_it_be(:project) { create(:project) }
+
+ let(:scope) { described_class.new(project) }
+
+ describe '#all_projects' do
+ subject(:all_projects) { scope.all_projects }
+
+ context 'when no projects are added to the scope' do
+ it 'returns the project defining the scope' do
+ expect(all_projects).to contain_exactly(project)
+ end
+ end
+
+ context 'when other projects are added to the scope' do
+ let_it_be(:scoped_project) { create(:project) }
+ let_it_be(:unscoped_project) { create(:project) }
+
+ let!(:link_in_scope) { create(:ci_job_token_project_scope_link, source_project: project, target_project: scoped_project) }
+ let!(:link_out_of_scope) { create(:ci_job_token_project_scope_link, target_project: unscoped_project) }
+
+ it 'returns all projects that can be accessed from a given scope' do
+ expect(subject).to contain_exactly(project, scoped_project)
+ end
+ end
+ end
+
+ describe 'includes?' do
+ subject { scope.includes?(target_project) }
+
+ context 'when param is the project defining the scope' do
+ let(:target_project) { project }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when param is a project in scope' do
+ let(:target_link) { create(:ci_job_token_project_scope_link, source_project: project) }
+ let(:target_project) { target_link.target_project }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when param is a project in another scope' do
+ let(:scope_link) { create(:ci_job_token_project_scope_link) }
+ let(:target_project) { scope_link.target_project }
+
+ it { is_expected.to be_falsey }
+
+ context 'when project scope setting is disabled' do
+ before do
+ project.ci_job_token_scope_enabled = false
+ end
+
+ it 'considers any project to be part of the scope' do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pending_build_spec.rb b/spec/models/ci/pending_build_spec.rb
new file mode 100644
index 00000000000..c1d4f4b0a5e
--- /dev/null
+++ b/spec/models/ci/pending_build_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PendingBuild do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
+
+ describe '.upsert_from_build!' do
+ context 'another pending entry does not exist' do
+ it 'creates a new pending entry' do
+ result = described_class.upsert_from_build!(build)
+
+ expect(result.rows.dig(0, 0)).to eq build.id
+ expect(build.reload.queuing_entry).to be_present
+ end
+ end
+
+ context 'when another queuing entry exists for given build' do
+ before do
+ described_class.create!(build: build, project: project, protected: false)
+ end
+
+ it 'returns a build id as a result' do
+ result = described_class.upsert_from_build!(build)
+
+ expect(result.rows.dig(0, 0)).to eq build.id
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index d5560edbbfd..cf73460bf1e 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Ci::PipelineSchedule do
+ let_it_be(:project) { create_default(:project) }
+
subject { build(:ci_pipeline_schedule) }
it { is_expected.to belong_to(:project) }
@@ -18,7 +20,7 @@ RSpec.describe Ci::PipelineSchedule do
it { is_expected.to respond_to(:next_run_at) }
it_behaves_like 'includes Limitable concern' do
- subject { build(:ci_pipeline_schedule) }
+ subject { build(:ci_pipeline_schedule, project: project) }
end
describe 'validations' do
@@ -103,26 +105,49 @@ RSpec.describe Ci::PipelineSchedule do
end
describe '#set_next_run_at' do
- let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
- let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_from, Time.zone.now) }
- let(:cron_worker_next_run_at) { pipeline_schedule.send(:cron_worker_next_run_from, Time.zone.now) }
+ using RSpec::Parameterized::TableSyntax
+
+ where(:worker_cron, :schedule_cron, :plan_limit, :ff_enabled, :now, :result) do
+ '0 1 2 3 *' | '0 1 * * *' | nil | true | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
+ '0 1 2 3 *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
+ '0 1 2 3 *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | false | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
+ '*/5 * * * *' | '*/1 * * * *' | nil | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5)
+ '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
+ '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | false | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5)
+ '*/5 * * * *' | '*/1 * * * *' | (1.day.in_minutes / 10).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
+ '*/5 * * * *' | '*/1 * * * *' | 200 | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
+ '*/5 * * * *' | '*/1 * * * *' | 200 | false | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5)
+ '*/5 * * * *' | '0 * * * *' | nil | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
+ '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 10).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
+ '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 10).to_i | false | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
+ '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
+ '*/5 * * * *' | '0 * * * *' | (1.day.in_minutes / 2.hours.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
+ '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
+ '*/5 * * * *' | '0 1 * * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 27, 1, 0) | Time.zone.local(2021, 5, 28, 1, 0)
+ '*/5 * * * *' | '0 1 1 * *' | (1.day.in_minutes / 1.hour.in_minutes).to_i | true | Time.zone.local(2021, 5, 1, 1, 0) | Time.zone.local(2021, 6, 1, 1, 0)
+ end
+
+ with_them do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: schedule_cron) }
- context 'when PipelineScheduleWorker runs at a specific interval' do
before do
allow(Settings).to receive(:cron_jobs) do
- {
- 'pipeline_schedule_worker' => {
- 'cron' => '0 1 2 3 *'
- }
- }
+ { 'pipeline_schedule_worker' => { 'cron' => worker_cron } }
end
+
+ create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: plan_limit) if plan_limit
+ stub_feature_flags(ci_daily_limit_for_pipeline_schedules: false) unless ff_enabled
+
+ # Setting this here to override initial save with the current time
+ pipeline_schedule.next_run_at = now
end
- it "updates next_run_at to the sidekiq worker's execution time" do
- expect(pipeline_schedule.next_run_at.min).to eq(0)
- expect(pipeline_schedule.next_run_at.hour).to eq(1)
- expect(pipeline_schedule.next_run_at.day).to eq(2)
- expect(pipeline_schedule.next_run_at.month).to eq(3)
+ it 'updates next_run_at' do
+ travel_to(now) do
+ pipeline_schedule.set_next_run_at
+
+ expect(pipeline_schedule.next_run_at).to eq(result)
+ end
end
end
@@ -176,4 +201,26 @@ RSpec.describe Ci::PipelineSchedule do
it { is_expected.to contain_exactly(*pipeline_schedule_variables.map(&:to_runner_variable)) }
end
+
+ describe '#daily_limit' do
+ let(:pipeline_schedule) { build(:ci_pipeline_schedule) }
+
+ subject(:daily_limit) { pipeline_schedule.daily_limit }
+
+ context 'when there is no limit' do
+ before do
+ create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: 0)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a limit' do
+ before do
+ create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: 144)
+ end
+
+ it { is_expected.to eq(144) }
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b9457055a18..72af40e31e0 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -744,6 +744,42 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ describe '#update_builds_coverage' do
+ let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+
+ context 'builds with coverage_regex defined' do
+ let!(:build_1) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, pipeline: pipeline) }
+ let!(:build_2) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 80.0, pipeline: pipeline) }
+
+ it 'updates the coverage value of each build from the trace' do
+ pipeline.update_builds_coverage
+
+ expect(build_1.reload.coverage).to eq(60.0)
+ expect(build_2.reload.coverage).to eq(80.0)
+ end
+ end
+
+ context 'builds without coverage_regex defined' do
+ let!(:build) { create(:ci_build, :success, :trace_with_coverage, coverage_regex: nil, trace_coverage: 60.0, pipeline: pipeline) }
+
+ it 'does not update the coverage value of each build from the trace' do
+ pipeline.update_builds_coverage
+
+ expect(build.reload.coverage).to eq(nil)
+ end
+ end
+
+ context 'builds with coverage values already present' do
+ let!(:build) { create(:ci_build, :success, :trace_with_coverage, trace_coverage: 60.0, coverage: 10.0, pipeline: pipeline) }
+
+ it 'does not update the coverage value of each build from the trace' do
+ pipeline.update_builds_coverage
+
+ expect(build.reload.coverage).to eq(10.0)
+ end
+ end
+ end
+
describe '#retryable?' do
subject { pipeline.retryable? }
@@ -2726,7 +2762,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
pipeline2.cancel_running
end
- extra_update_queries = 3 # transition ... => :canceled
+ extra_update_queries = 4 # transition ... => :canceled, queue pop
extra_generic_commit_status_validation_queries = 2 # name_uniqueness_across_types
expect(control2.count).to eq(control1.count + extra_update_queries + extra_generic_commit_status_validation_queries)
@@ -3162,6 +3198,81 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
end
+ describe '#environments_in_self_and_descendants' do
+ subject { pipeline.environments_in_self_and_descendants }
+
+ context 'when pipeline is not child nor parent' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :created) }
+ let_it_be(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
+
+ it 'returns just the pipeline environment' do
+ expect(subject).to contain_exactly(build.deployment.environment)
+ end
+ end
+
+ context 'when pipeline is in extended family' do
+ let_it_be(:parent) { create(:ci_pipeline) }
+ let_it_be(:parent_build) { create(:ci_build, :with_deployment, environment: 'staging', pipeline: parent) }
+
+ let_it_be(:pipeline) { create(:ci_pipeline, child_of: parent) }
+ let_it_be(:build) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
+
+ let_it_be(:child) { create(:ci_pipeline, child_of: pipeline) }
+ let_it_be(:child_build) { create(:ci_build, :with_deployment, environment: 'canary', pipeline: child) }
+
+ let_it_be(:grandchild) { create(:ci_pipeline, child_of: child) }
+ let_it_be(:grandchild_build) { create(:ci_build, :with_deployment, environment: 'test', pipeline: grandchild) }
+
+ let_it_be(:sibling) { create(:ci_pipeline, child_of: parent) }
+ let_it_be(:sibling_build) { create(:ci_build, :with_deployment, environment: 'review', pipeline: sibling) }
+
+ it 'returns its own environment and from all descendants' do
+ expected_environments = [
+ build.deployment.environment,
+ child_build.deployment.environment,
+ grandchild_build.deployment.environment
+ ]
+ expect(subject).to match_array(expected_environments)
+ end
+
+ it 'does not return parent environment' do
+ expect(subject).not_to include(parent_build.deployment.environment)
+ end
+
+ it 'does not return sibling environment' do
+ expect(subject).not_to include(sibling_build.deployment.environment)
+ end
+ end
+
+ context 'when each pipeline has multiple environments' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :created) }
+ let_it_be(:build1) { create(:ci_build, :with_deployment, :deploy_to_production, pipeline: pipeline) }
+ let_it_be(:build2) { create(:ci_build, :with_deployment, environment: 'staging', pipeline: pipeline) }
+
+ let_it_be(:child) { create(:ci_pipeline, child_of: pipeline) }
+ let_it_be(:child_build1) { create(:ci_build, :with_deployment, environment: 'canary', pipeline: child) }
+ let_it_be(:child_build2) { create(:ci_build, :with_deployment, environment: 'test', pipeline: child) }
+
+ it 'returns all related environments' do
+ expected_environments = [
+ build1.deployment.environment,
+ build2.deployment.environment,
+ child_build1.deployment.environment,
+ child_build2.deployment.environment
+ ]
+ expect(subject).to match_array(expected_environments)
+ end
+ end
+
+ context 'when pipeline has no environment' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :created) }
+
+ it 'returns empty' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
describe '#root_ancestor' do
subject { pipeline.root_ancestor }
@@ -4512,4 +4623,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
.not_to exceed_query_limit(control_count)
end
end
+
+ describe '#build_matchers' do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:builds) { create_list(:ci_build, 2, pipeline: pipeline, project: pipeline.project) }
+
+ subject(:matchers) { pipeline.build_matchers }
+
+ it 'returns build matchers' do
+ expect(matchers.size).to eq(1)
+ expect(matchers).to all be_a(Gitlab::Ci::Matching::BuildMatcher)
+ expect(matchers.first.build_ids).to match_array(builds.map(&:id))
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index ffe0b0d0b19..61f80bd43b1 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -75,6 +75,22 @@ RSpec.describe Ci::Runner do
expect { create(:group, runners: [project_runner]) }
.to raise_error(ActiveRecord::RecordInvalid)
end
+
+ context 'when runner has config' do
+ it 'is valid' do
+ runner = build(:ci_runner, config: { gpus: "all" })
+
+ expect(runner).to be_valid
+ end
+ end
+
+ context 'when runner has an invalid config' do
+ it 'is invalid' do
+ runner = build(:ci_runner, config: { test: 1 })
+
+ expect(runner).not_to be_valid
+ end
+ end
end
context 'cost factors validations' do
@@ -257,6 +273,20 @@ RSpec.describe Ci::Runner do
end
end
+ describe '.recent' do
+ subject { described_class.recent }
+
+ before do
+ @runner1 = create(:ci_runner, :instance, contacted_at: nil, created_at: 2.months.ago)
+ @runner2 = create(:ci_runner, :instance, contacted_at: nil, created_at: 3.months.ago)
+ @runner3 = create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 2.months.ago)
+ @runner4 = create(:ci_runner, :instance, contacted_at: 1.month.ago, created_at: 3.months.ago)
+ @runner5 = create(:ci_runner, :instance, contacted_at: 3.months.ago, created_at: 5.months.ago)
+ end
+
+ it { is_expected.to eq([@runner1, @runner3, @runner4])}
+ end
+
describe '.online' do
subject { described_class.online }
@@ -349,6 +379,22 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq([@runner1])}
end
+ describe '#tick_runner_queue' do
+ it 'sticks the runner to the primary and calls the original method' do
+ runner = create(:ci_runner)
+
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?)
+ .and_return(true)
+
+ expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:stick)
+ .with(:runner, runner.id)
+
+ expect(Gitlab::Workhorse).to receive(:set_key_and_notify)
+
+ runner.tick_runner_queue
+ end
+ end
+
describe '#can_pick?' do
using RSpec::Parameterized::TableSyntax
@@ -653,7 +699,7 @@ RSpec.describe Ci::Runner do
describe '#heartbeat' do
let(:runner) { create(:ci_runner, :project) }
- subject { runner.heartbeat(architecture: '18-bit') }
+ subject { runner.heartbeat(architecture: '18-bit', config: { gpus: "all" }) }
context 'when database was updated recently' do
before do
@@ -701,6 +747,7 @@ RSpec.describe Ci::Runner do
def does_db_update
expect { subject }.to change { runner.reload.read_attribute(:contacted_at) }
.and change { runner.reload.read_attribute(:architecture) }
+ .and change { runner.reload.read_attribute(:config) }
end
end
@@ -826,12 +873,12 @@ RSpec.describe Ci::Runner do
expect(described_class.search(runner.token)).to eq([runner])
end
- it 'returns runners with a partially matching token' do
- expect(described_class.search(runner.token[0..2])).to eq([runner])
+ it 'does not return runners with a partially matching token' do
+ expect(described_class.search(runner.token[0..2])).to be_empty
end
- it 'returns runners with a matching token regardless of the casing' do
- expect(described_class.search(runner.token.upcase)).to eq([runner])
+ it 'does not return runners with a matching token with different casing' do
+ expect(described_class.search(runner.token.upcase)).to be_empty
end
it 'returns runners with a matching description' do
@@ -919,29 +966,13 @@ RSpec.describe Ci::Runner do
end
end
- context 'build picking improvement enabled' do
- before do
- stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: true)
- end
-
+ context 'build picking improvement' do
it 'does not check if the build is assignable to a runner' do
expect(runner).not_to receive(:can_pick?)
runner.pick_build!(build)
end
end
-
- context 'build picking improvement disabled' do
- before do
- stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: false)
- end
-
- it 'checks if the build is assignable to a runner' do
- expect(runner).to receive(:can_pick?).and_call_original
-
- runner.pick_build!(build)
- end
- end
end
describe 'project runner without projects is destroyable' do
@@ -975,6 +1006,108 @@ RSpec.describe Ci::Runner do
end
end
+ describe '.runner_matchers' do
+ subject(:matchers) { described_class.all.runner_matchers }
+
+ context 'deduplicates on runner_type' do
+ before do
+ create_list(:ci_runner, 2, :instance)
+ create_list(:ci_runner, 2, :project)
+ end
+
+ it 'creates two matchers' do
+ expect(matchers.size).to eq(2)
+
+ expect(matchers.map(&:runner_type)).to match_array(%w[instance_type project_type])
+ end
+ end
+
+ context 'deduplicates on public_projects_minutes_cost_factor' do
+ before do
+ create_list(:ci_runner, 2, public_projects_minutes_cost_factor: 5)
+ create_list(:ci_runner, 2, public_projects_minutes_cost_factor: 10)
+ end
+
+ it 'creates two matchers' do
+ expect(matchers.size).to eq(2)
+
+ expect(matchers.map(&:public_projects_minutes_cost_factor)).to match_array([5, 10])
+ end
+ end
+
+ context 'deduplicates on private_projects_minutes_cost_factor' do
+ before do
+ create_list(:ci_runner, 2, private_projects_minutes_cost_factor: 5)
+ create_list(:ci_runner, 2, private_projects_minutes_cost_factor: 10)
+ end
+
+ it 'creates two matchers' do
+ expect(matchers.size).to eq(2)
+
+ expect(matchers.map(&:private_projects_minutes_cost_factor)).to match_array([5, 10])
+ end
+ end
+
+ context 'deduplicates on run_untagged' do
+ before do
+ create_list(:ci_runner, 2, run_untagged: true, tag_list: ['a'])
+ create_list(:ci_runner, 2, run_untagged: false, tag_list: ['a'])
+ end
+
+ it 'creates two matchers' do
+ expect(matchers.size).to eq(2)
+
+ expect(matchers.map(&:run_untagged)).to match_array([true, false])
+ end
+ end
+
+ context 'deduplicates on access_level' do
+ before do
+ create_list(:ci_runner, 2, access_level: :ref_protected)
+ create_list(:ci_runner, 2, access_level: :not_protected)
+ end
+
+ it 'creates two matchers' do
+ expect(matchers.size).to eq(2)
+
+ expect(matchers.map(&:access_level)).to match_array(%w[ref_protected not_protected])
+ end
+ end
+
+ context 'deduplicates on tag_list' do
+ before do
+ create_list(:ci_runner, 2, tag_list: %w[tag1 tag2])
+ create_list(:ci_runner, 2, tag_list: %w[tag3 tag4])
+ end
+
+ it 'creates two matchers' do
+ expect(matchers.size).to eq(2)
+
+ expect(matchers.map(&:tag_list)).to match_array([%w[tag1 tag2], %w[tag3 tag4]])
+ end
+ end
+ end
+
+ describe '#runner_matcher' do
+ let(:runner) do
+ build_stubbed(:ci_runner, :instance_type, tag_list: %w[tag1 tag2])
+ end
+
+ subject(:matcher) { runner.runner_matcher }
+
+ it { expect(matcher.runner_type).to eq(runner.runner_type) }
+
+ it { expect(matcher.public_projects_minutes_cost_factor).to eq(runner.public_projects_minutes_cost_factor) }
+
+ it { expect(matcher.private_projects_minutes_cost_factor).to eq(runner.private_projects_minutes_cost_factor) }
+
+ it { expect(matcher.run_untagged).to eq(runner.run_untagged) }
+
+ it { expect(matcher.access_level).to eq(runner.access_level) }
+
+ it { expect(matcher.tag_list).to match_array(runner.tag_list) }
+ end
+
describe '#uncached_contacted_at' do
let(:contacted_at_stored) { 1.hour.ago.change(usec: 0) }
let(:runner) { create(:ci_runner, contacted_at: contacted_at_stored) }
diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb
new file mode 100644
index 00000000000..589e5a86f4d
--- /dev/null
+++ b/spec/models/ci/running_build_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::RunningBuild do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:runner) { create(:ci_runner, :instance_type) }
+ let(:build) { create(:ci_build, :running, runner: runner, pipeline: pipeline) }
+
+ describe '.upsert_shared_runner_build!' do
+ context 'another pending entry does not exist' do
+ it 'creates a new pending entry' do
+ result = described_class.upsert_shared_runner_build!(build)
+
+ expect(result.rows.dig(0, 0)).to eq build.id
+ expect(build.reload.runtime_metadata).to be_present
+ end
+ end
+
+ context 'when another queuing entry exists for given build' do
+ before do
+ described_class.create!(build: build,
+ project: project,
+ runner: runner,
+ runner_type: runner.runner_type)
+ end
+
+ it 'returns a build id as a result' do
+ result = described_class.upsert_shared_runner_build!(build)
+
+ expect(result.rows.dig(0, 0)).to eq build.id
+ end
+ end
+
+ context 'when build has been picked by a specific runner' do
+ let(:runner) { create(:ci_runner, :project) }
+
+ it 'raises an error' do
+ expect { described_class.upsert_shared_runner_build!(build) }
+ .to raise_error(ArgumentError, 'build has not been picked by a shared runner')
+ end
+ end
+
+ context 'when build has not been picked by a runner yet' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'raises an error' do
+ expect { described_class.upsert_shared_runner_build!(build) }
+ .to raise_error(ArgumentError, 'build has not been picked by a shared runner')
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/fluentd_spec.rb b/spec/models/clusters/applications/fluentd_spec.rb
deleted file mode 100644
index ccdf6b0e40d..00000000000
--- a/spec/models/clusters/applications/fluentd_spec.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Fluentd do
- let(:waf_log_enabled) { true }
- let(:cilium_log_enabled) { true }
- let(:fluentd) { create(:clusters_applications_fluentd, waf_log_enabled: waf_log_enabled, cilium_log_enabled: cilium_log_enabled) }
-
- include_examples 'cluster application core specs', :clusters_applications_fluentd
- include_examples 'cluster application status specs', :clusters_applications_fluentd
- include_examples 'cluster application version specs', :clusters_applications_fluentd
- include_examples 'cluster application initial status specs'
-
- describe '#can_uninstall?' do
- subject { fluentd.can_uninstall? }
-
- it { is_expected.to be true }
- end
-
- describe '#install_command' do
- subject { fluentd.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with fluentd arguments' do
- expect(subject.name).to eq('fluentd')
- expect(subject.chart).to eq('fluentd/fluentd')
- expect(subject.version).to eq('2.4.0')
- expect(subject).to be_rbac
- end
-
- context 'application failed to install previously' do
- let(:fluentd) { create(:clusters_applications_fluentd, :errored, version: '0.0.1') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('2.4.0')
- end
- end
- end
-
- describe '#files' do
- let(:application) { fluentd }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes fluentd specific keys in the values.yaml file' do
- expect(values).to include('output.conf', 'general.conf')
- end
- end
-
- describe '#values' do
- let(:modsecurity_log_path) { "/var/log/containers/*#{Clusters::Applications::Ingress::MODSECURITY_LOG_CONTAINER_NAME}*.log" }
- let(:cilium_log_path) { "/var/log/containers/*#{described_class::CILIUM_CONTAINER_NAME}*.log" }
-
- subject { fluentd.values }
-
- context 'with both logs variables set to false' do
- let(:waf_log_enabled) { false }
- let(:cilium_log_enabled) { false }
-
- it "raises ActiveRecord::RecordInvalid" do
- expect {subject}.to raise_error(ActiveRecord::RecordInvalid)
- end
- end
-
- context 'with both logs variables set to true' do
- it { is_expected.to include("#{modsecurity_log_path},#{cilium_log_path}") }
- end
-
- context 'with waf_log_enabled set to true' do
- let(:cilium_log_enabled) { false }
-
- it { is_expected.to include(modsecurity_log_path) }
- end
-
- context 'with cilium_log_enabled set to true' do
- let(:waf_log_enabled) { false }
-
- it { is_expected.to include(cilium_log_path) }
- end
- end
-end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 1bc1a4343aa..e16d97c42d9 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -172,94 +172,4 @@ RSpec.describe Clusters::Applications::Ingress do
expect(values).to include('clusterIP')
end
end
-
- describe '#values' do
- subject { ingress }
-
- context 'when modsecurity_enabled is enabled' do
- before do
- allow(subject).to receive(:modsecurity_enabled).and_return(true)
- end
-
- it 'includes modsecurity module enablement' do
- expect(subject.values).to include("enable-modsecurity: 'true'")
- end
-
- it 'includes modsecurity core ruleset enablement set to false' do
- expect(subject.values).to include("enable-owasp-modsecurity-crs: 'false'")
- end
-
- it 'includes modsecurity snippet with information related to security rules' do
- expect(subject.values).to include("SecRuleEngine DetectionOnly")
- expect(subject.values).to include("Include #{described_class::MODSECURITY_OWASP_RULES_FILE}")
- end
-
- context 'when modsecurity_mode is set to :blocking' do
- before do
- subject.blocking!
- end
-
- it 'includes modsecurity snippet with information related to security rules' do
- expect(subject.values).to include("SecRuleEngine On")
- expect(subject.values).to include("Include #{described_class::MODSECURITY_OWASP_RULES_FILE}")
- end
- end
-
- it 'includes modsecurity.conf content' do
- expect(subject.values).to include('modsecurity.conf')
- # Includes file content from Ingress#modsecurity_config_content
- expect(subject.values).to include('SecAuditLog')
-
- expect(subject.values).to include('extraVolumes')
- expect(subject.values).to include('extraVolumeMounts')
- end
-
- it 'includes modsecurity sidecar container' do
- expect(subject.values).to include('modsecurity-log-volume')
-
- expect(subject.values).to include('extraContainers')
- end
-
- it 'executes command to tail modsecurity logs with -F option' do
- args = YAML.safe_load(subject.values).dig('controller', 'extraContainers', 0, 'args')
-
- expect(args).to eq(['/bin/sh', '-c', 'tail -F /var/log/modsec/audit.log'])
- end
-
- it 'includes livenessProbe for modsecurity sidecar container' do
- probe_config = YAML.safe_load(subject.values).dig('controller', 'extraContainers', 0, 'livenessProbe')
-
- expect(probe_config).to eq('exec' => { 'command' => ['ls', '/var/log/modsec/audit.log'] })
- end
- end
-
- context 'when modsecurity_enabled is disabled' do
- before do
- allow(subject).to receive(:modsecurity_enabled).and_return(false)
- end
-
- it 'excludes modsecurity module enablement' do
- expect(subject.values).not_to include('enable-modsecurity')
- end
-
- it 'excludes modsecurity core ruleset enablement' do
- expect(subject.values).not_to include('enable-owasp-modsecurity-crs')
- end
-
- it 'excludes modsecurity.conf content' do
- expect(subject.values).not_to include('modsecurity.conf')
- # Excludes file content from Ingress#modsecurity_config_content
- expect(subject.values).not_to include('SecAuditLog')
-
- expect(subject.values).not_to include('extraVolumes')
- expect(subject.values).not_to include('extraVolumeMounts')
- end
-
- it 'excludes modsecurity sidecar container' do
- expect(subject.values).not_to include('modsecurity-log-volume')
-
- expect(subject.values).not_to include('extraContainers')
- end
- end
- end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index b2ed64fd9b0..278e200b05c 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -42,7 +42,8 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
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(:application_elastic_stack).with_prefix }
+ it { is_expected.to delegate_method(:available?).to(:integration_elastic_stack).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 }
@@ -195,28 +196,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
- describe '.with_enabled_modsecurity' do
- subject { described_class.with_enabled_modsecurity }
-
- let_it_be(:cluster) { create(:cluster) }
-
- context 'cluster has ingress application with enabled modsecurity' do
- let!(:application) { create(:clusters_applications_ingress, :installed, :modsecurity_logging, cluster: cluster) }
-
- it { is_expected.to include(cluster) }
- end
-
- context 'cluster has ingress application with disabled modsecurity' do
- let!(:application) { create(:clusters_applications_ingress, :installed, :modsecurity_disabled, cluster: cluster) }
-
- it { is_expected.not_to include(cluster) }
- end
-
- context 'cluster does not have ingress application' do
- it { is_expected.not_to include(cluster) }
- end
- end
-
describe '.with_available_elasticstack' do
subject { described_class.with_available_elasticstack }
@@ -1042,7 +1021,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
where(:status_name, :cleanup_status) do
provider_status | :cleanup_not_started
- :cleanup_ongoing | :cleanup_uninstalling_applications
:cleanup_ongoing | :cleanup_removing_project_namespaces
:cleanup_ongoing | :cleanup_removing_service_account
:cleanup_errored | :cleanup_errored
@@ -1098,8 +1076,8 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
describe '#start_cleanup!' do
- let(:expected_worker_class) { Clusters::Cleanup::AppWorker }
- let(:to_state) { :cleanup_uninstalling_applications }
+ let(:expected_worker_class) { Clusters::Cleanup::ProjectNamespaceWorker }
+ let(:to_state) { :cleanup_removing_project_namespaces }
subject { cluster.start_cleanup! }
@@ -1137,25 +1115,13 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
describe '#continue_cleanup!' do
- context 'when cleanup_status is cleanup_uninstalling_applications' do
- let(:expected_worker_class) { Clusters::Cleanup::ProjectNamespaceWorker }
- let(:from_state) { :cleanup_uninstalling_applications }
- let(:to_state) { :cleanup_removing_project_namespaces }
-
- subject { cluster.continue_cleanup! }
+ let(:expected_worker_class) { Clusters::Cleanup::ServiceAccountWorker }
+ let(:from_state) { :cleanup_removing_project_namespaces }
+ let(:to_state) { :cleanup_removing_service_account }
- it_behaves_like 'cleanup_status transition'
- end
-
- context 'when cleanup_status is cleanup_removing_project_namespaces' do
- let(:expected_worker_class) { Clusters::Cleanup::ServiceAccountWorker }
- let(:from_state) { :cleanup_removing_project_namespaces }
- let(:to_state) { :cleanup_removing_service_account }
+ subject { cluster.continue_cleanup! }
- subject { cluster.continue_cleanup! }
-
- it_behaves_like 'cleanup_status transition'
- end
+ it_behaves_like 'cleanup_status transition'
end
end
@@ -1349,45 +1315,23 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
- describe '#application_prometheus_available?' do
+ describe '#integration_prometheus_available?' do
let_it_be_with_reload(:cluster) { create(:cluster, :project) }
- subject { cluster.application_prometheus_available? }
+ subject { cluster.integration_prometheus_available? }
it { is_expected.to be_falsey }
- context 'has a integration_prometheus' do
- let_it_be(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
+ context 'when integration is enabled' do
+ let!(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
it { is_expected.to be_truthy }
-
- context 'disabled' do
- before do
- cluster.integration_prometheus.enabled = false
- end
-
- it { is_expected.to be_falsey }
- end
end
- context 'has a application_prometheus' do
- let_it_be(:application) { create(:clusters_applications_prometheus, :installed, :no_helm_installed, cluster: cluster) }
+ context 'when integration is disabled' do
+ let!(:integration) { create(:clusters_integrations_prometheus, enabled: false, cluster: cluster) }
- it { is_expected.to be_truthy }
-
- context 'errored' do
- before do
- cluster.application_prometheus.status = Clusters::Applications::Prometheus.state_machines[:status].states[:errored]
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'also has a integration_prometheus' do
- let_it_be(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
-
- it { is_expected.to be_truthy }
- end
+ it { is_expected.to be_falsey }
end
end
@@ -1398,7 +1342,7 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
expect(cluster.prometheus_adapter).to be_nil
end
- context 'has a integration_prometheus' do
+ context 'has integration_prometheus' do
let_it_be(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
it 'returns the integration' do
@@ -1406,11 +1350,11 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
- context 'has a application_prometheus' do
+ context 'has application_prometheus' do
let_it_be(:application) { create(:clusters_applications_prometheus, :no_helm_installed, cluster: cluster) }
- it 'returns the application' do
- expect(cluster.prometheus_adapter).to eq(application)
+ it 'returns nil' do
+ expect(cluster.prometheus_adapter).to be_nil
end
context 'also has a integration_prometheus' do
diff --git a/spec/models/clusters/clusters_hierarchy_spec.rb b/spec/models/clusters/clusters_hierarchy_spec.rb
index 5ac561eb2d0..5dd2fe98352 100644
--- a/spec/models/clusters/clusters_hierarchy_spec.rb
+++ b/spec/models/clusters/clusters_hierarchy_spec.rb
@@ -4,8 +4,8 @@ require 'spec_helper'
RSpec.describe Clusters::ClustersHierarchy do
describe '#base_and_ancestors' do
- def base_and_ancestors(clusterable, include_management_project: true)
- described_class.new(clusterable, include_management_project: include_management_project).base_and_ancestors
+ def base_and_ancestors(clusterable)
+ described_class.new(clusterable).base_and_ancestors
end
context 'project in nested group with clusters at every level' do
@@ -101,10 +101,6 @@ RSpec.describe Clusters::ClustersHierarchy do
expect(base_and_ancestors(management_project)).to eq([ancestor, child])
end
- it 'returns clusters for management_project' do
- expect(base_and_ancestors(management_project, include_management_project: false)).to eq([child, ancestor])
- end
-
it 'returns clusters for project' do
expect(base_and_ancestors(project)).to eq([child, ancestor])
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 7c00f367844..8ffc198fc4d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -471,16 +471,25 @@ eos
end
it_behaves_like 'a mentionable' do
- subject { create(:project, :repository).commit }
+ subject(:commit) { create(:project, :repository).commit }
let(:author) { create(:user, email: subject.author_email) }
let(:backref_text) { "commit #{subject.id}" }
let(:set_mentionable_text) do
- ->(txt) { allow(subject).to receive(:safe_message).and_return(txt) }
+ ->(txt) { allow(commit).to receive(:safe_message).and_return(txt) }
end
# Include the subject in the repository stub.
- let(:extra_commits) { [subject] }
+ let(:extra_commits) { [commit] }
+
+ it 'uses the CachedMarkdownField cache instead of the Mentionable cache', :use_clean_rails_redis_caching do
+ expect(commit.title_html).not_to be_present
+
+ commit.all_references(project.owner).all
+
+ expect(commit.title_html).to be_present
+ expect(Rails.cache.read("banzai/commit:#{commit.id}/safe_message/single_line")).to be_nil
+ end
end
describe '#hook_attrs' do
@@ -663,6 +672,92 @@ eos
it_behaves_like '#uri_type'
end
+ describe '.diff_max_files' do
+ subject(:diff_max_files) { described_class.diff_max_files }
+
+ let(:increased_diff_limits) { false }
+ let(:configurable_diff_limits) { false }
+
+ before do
+ stub_feature_flags(increased_diff_limits: increased_diff_limits, configurable_diff_limits: configurable_diff_limits)
+ end
+
+ context 'when increased_diff_limits is enabled' do
+ let(:increased_diff_limits) { true }
+
+ it 'returns 3000' do
+ expect(diff_max_files).to eq(3000)
+ end
+ end
+
+ context 'when configurable_diff_limits is enabled' do
+ let(:configurable_diff_limits) { true }
+
+ it 'returns the current settings' do
+ Gitlab::CurrentSettings.update!(diff_max_files: 1234)
+ expect(diff_max_files).to eq(1234)
+ end
+ end
+
+ context 'when neither feature flag is enabled' do
+ it 'returns 1000' do
+ expect(diff_max_files).to eq(1000)
+ end
+ end
+ end
+
+ describe '.diff_max_lines' do
+ subject(:diff_max_lines) { described_class.diff_max_lines }
+
+ let(:increased_diff_limits) { false }
+ let(:configurable_diff_limits) { false }
+
+ before do
+ stub_feature_flags(increased_diff_limits: increased_diff_limits, configurable_diff_limits: configurable_diff_limits)
+ end
+
+ context 'when increased_diff_limits is enabled' do
+ let(:increased_diff_limits) { true }
+
+ it 'returns 100000' do
+ expect(diff_max_lines).to eq(100000)
+ end
+ end
+
+ context 'when configurable_diff_limits is enabled' do
+ let(:configurable_diff_limits) { true }
+
+ it 'returns the current settings' do
+ Gitlab::CurrentSettings.update!(diff_max_lines: 65321)
+ expect(diff_max_lines).to eq(65321)
+ end
+ end
+
+ context 'when neither feature flag is enabled' do
+ it 'returns 50000' do
+ expect(diff_max_lines).to eq(50000)
+ end
+ end
+ end
+
+ describe '.diff_safe_max_files' do
+ subject(:diff_safe_max_files) { described_class.diff_safe_max_files }
+
+ it 'returns the commit diff max divided by the limit factor of 10' do
+ expect(::Commit).to receive(:diff_max_files).and_return(10)
+ expect(diff_safe_max_files).to eq(1)
+ end
+ end
+
+ describe '.diff_safe_max_lines' do
+ subject(:diff_safe_max_lines) { described_class.diff_safe_max_lines }
+
+ it 'returns the commit diff max divided by the limit factor of 10' do
+ expect(::Commit).to receive(:diff_max_lines).and_return(100)
+ expect(diff_safe_max_lines).to eq(10)
+ end
+ end
+
describe '.from_hash' do
subject { described_class.from_hash(commit.to_hash, container) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index feb2f3630c1..69b4d752f4c 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -79,6 +79,32 @@ RSpec.describe CommitStatus do
end
end
+ describe '.updated_before' do
+ let!(:lookback) { 5.days.ago }
+ let!(:timeout) { 1.day.ago }
+ let!(:before_lookback) { lookback - 1.hour }
+ let!(:after_lookback) { lookback + 1.hour }
+ let!(:before_timeout) { timeout - 1.hour }
+ let!(:after_timeout) { timeout + 1.hour }
+
+ subject { described_class.updated_before(lookback: lookback, timeout: timeout) }
+
+ def create_build_with_set_timestamps(created_at:, updated_at:)
+ travel_to(created_at) { create(:ci_build, created_at: Time.current) }.tap do |build|
+ travel_to(updated_at) { build.update!(status: :failed) }
+ end
+ end
+
+ it 'finds builds updated and created in the window between lookback and timeout' do
+ build_in_lookback_timeout_window = create_build_with_set_timestamps(created_at: after_lookback, updated_at: before_timeout)
+ build_outside_lookback_window = create_build_with_set_timestamps(created_at: before_lookback, updated_at: before_timeout)
+ build_outside_timeout_window = create_build_with_set_timestamps(created_at: after_lookback, updated_at: after_timeout)
+
+ expect(subject).to contain_exactly(build_in_lookback_timeout_window)
+ expect(subject).not_to include(build_outside_lookback_window, build_outside_timeout_window)
+ end
+ end
+
describe '#processed' do
subject { commit_status.processed }
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index b5b3772ecb6..b80b6ec95e2 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe Awardable do
it "doesn't include unused thumbs buttons when disabled in project" do
issue_without_downvote.project.show_default_award_emojis = false
- expect(issue_without_downvote.grouped_awards.keys.sort).to eq []
+ expect(issue_without_downvote.grouped_awards.keys.sort).to be_empty
end
it "includes unused thumbs buttons when enabled in project" do
@@ -118,7 +118,7 @@ RSpec.describe Awardable do
end
it "doesn't include unused thumbs buttons in summary" do
- expect(issue_without_downvote.grouped_awards(with_thumbs: false).keys).to eq []
+ expect(issue_without_downvote.grouped_awards(with_thumbs: false).keys).to be_empty
end
it "includes used thumbs buttons when disabled in project" do
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index ca6df506ee8..209ee1264d5 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -20,6 +20,13 @@ RSpec.describe BulkInsertSafe do
t.index :name, unique: true
end
+
+ create_table :bulk_insert_items_with_composite_pk, id: false, force: true do |t|
+ t.integer :id, null: true
+ t.string :name, null: true
+ end
+
+ execute("ALTER TABLE bulk_insert_items_with_composite_pk ADD PRIMARY KEY (id,name);")
end
end
@@ -27,6 +34,7 @@ RSpec.describe BulkInsertSafe do
ActiveRecord::Schema.define do
drop_table :bulk_insert_items, force: true
drop_table :bulk_insert_parent_items, force: true
+ drop_table :bulk_insert_items_with_composite_pk, force: true
end
end
@@ -227,5 +235,28 @@ RSpec.describe BulkInsertSafe do
end
end
end
+
+ context 'when a model with composite primary key is inserted' do
+ let_it_be(:bulk_insert_items_with_composite_pk_class) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'bulk_insert_items_with_composite_pk'
+
+ include BulkInsertSafe
+ end
+ end
+
+ let(:new_object) { bulk_insert_items_with_composite_pk_class.new(id: 1, name: 'composite') }
+
+ it 'successfully inserts an item' do
+ expect(ActiveRecord::InsertAll).to receive(:new)
+ .with(
+ bulk_insert_items_with_composite_pk_class, [new_object.as_json], on_duplicate: :raise, returning: false, unique_by: %w[id name]
+ ).and_call_original
+
+ expect { bulk_insert_items_with_composite_pk_class.bulk_insert!([new_object]) }.to(
+ change(bulk_insert_items_with_composite_pk_class, :count).from(0).to(1)
+ )
+ end
+ end
end
end
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index 2bb6aa27e21..7fa55184cf1 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -254,20 +254,8 @@ RSpec.describe DeploymentPlatform do
create(:cluster, :provided_by_user, projects: [another_project], management_project: project)
end
- context 'cluster_management_project feature is enabled' do
- it 'returns the cluster with management project' do
- is_expected.to eq(cluster_with_management_project.platform_kubernetes)
- end
- end
-
- context 'cluster_management_project feature is disabled' do
- before do
- stub_feature_flags(cluster_management_project: false)
- end
-
- it 'returns nothing' do
- is_expected.to be_nil
- end
+ it 'returns the cluster with management project' do
+ is_expected.to eq(cluster_with_management_project.platform_kubernetes)
end
end
@@ -311,20 +299,8 @@ RSpec.describe DeploymentPlatform do
create(:cluster, :provided_by_user, projects: [another_project], management_project: project)
end
- context 'cluster_management_project feature is enabled' do
- it 'returns the cluster with management project' do
- is_expected.to eq(cluster_with_management_project.platform_kubernetes)
- end
- end
-
- context 'cluster_management_project feature is disabled' do
- before do
- stub_feature_flags(cluster_management_project: false)
- end
-
- it 'returns the group cluster' do
- is_expected.to eq(group_cluster.platform_kubernetes)
- end
+ it 'returns the cluster with management project' do
+ is_expected.to eq(cluster_with_management_project.platform_kubernetes)
end
end
diff --git a/spec/models/concerns/has_timelogs_report_spec.rb b/spec/models/concerns/has_timelogs_report_spec.rb
deleted file mode 100644
index f0dca47fae1..00000000000
--- a/spec/models/concerns/has_timelogs_report_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe HasTimelogsReport do
- let_it_be(:user) { create(:user) }
-
- let(:group) { create(:group) }
- let(:project) { create(:project, :public, group: group) }
- let(:issue1) { create(:issue, project: project) }
- let(:merge_request1) { create(:merge_request, source_project: project) }
-
- describe '#timelogs' do
- let_it_be(:start_time) { 20.days.ago }
- let_it_be(:end_time) { 8.days.ago }
-
- let!(:timelog1) { create_timelog(15.days.ago, issue: issue1) }
- let!(:timelog2) { create_timelog(10.days.ago, merge_request: merge_request1) }
- let!(:timelog3) { create_timelog(5.days.ago, issue: issue1) }
-
- before do
- group.add_developer(user)
- end
-
- it 'returns collection of timelogs between given times' do
- expect(group.timelogs(start_time, end_time).to_a).to match_array([timelog1, timelog2])
- end
-
- it 'returns empty collection if times are not present' do
- expect(group.timelogs(nil, nil)).to be_empty
- end
-
- it 'returns empty collection if time range is invalid' do
- expect(group.timelogs(end_time, start_time)).to be_empty
- end
- end
-
- describe '#user_can_access_group_timelogs?' do
- it 'returns true if user can access group timelogs' do
- group.add_developer(user)
-
- expect(group).to be_user_can_access_group_timelogs(user)
- end
-
- it 'returns false if user has insufficient permissions' do
- group.add_guest(user)
-
- expect(group).not_to be_user_can_access_group_timelogs(user)
- end
- end
-
- def create_timelog(time, issue: nil, merge_request: nil)
- create(:timelog, issue: issue, merge_request: merge_request, user: user, spent_at: time)
- end
-end
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index c87bbf24c30..a6a0e074589 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe User do
specify 'types consistency checks', :aggregate_failures do
expect(described_class::USER_TYPES.keys)
- .to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot migration_bot])
+ .to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot migration_bot automation_bot])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
diff --git a/spec/models/project_services/data_fields_spec.rb b/spec/models/concerns/integrations/has_data_fields_spec.rb
index d3e6afe4978..54e0ac9c5a5 100644
--- a/spec/models/project_services/data_fields_spec.rb
+++ b/spec/models/concerns/integrations/has_data_fields_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DataFields do
+RSpec.describe Integrations::HasDataFields do
let(:url) { 'http://url.com' }
let(:username) { 'username_one' }
let(:properties) do
@@ -100,7 +100,7 @@ RSpec.describe DataFields do
context 'when service and data_fields are not persisted' do
let(:service) do
- JiraService.new
+ Integrations::Jira.new
end
describe 'data_fields_present?' do
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 14db9b530db..7b100b7a6f3 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -715,6 +715,12 @@ RSpec.describe Issuable do
expect(issue.total_time_spent).to eq(1800)
end
+ it 'stores the time change' do
+ spend_time(1800)
+
+ expect(issue.time_change).to eq(1800)
+ end
+
it 'updates issues updated_at' do
issue
@@ -735,6 +741,12 @@ RSpec.describe Issuable do
expect(issue.total_time_spent).to eq(900)
end
+ it 'stores negative time change' do
+ spend_time(-900)
+
+ expect(issue.time_change).to eq(-900)
+ end
+
context 'when time to subtract exceeds the total time spent' do
it 'raise a validation error' do
Timecop.travel(1.minute.from_now) do
diff --git a/spec/models/concerns/limitable_spec.rb b/spec/models/concerns/limitable_spec.rb
index 753e2a8ee5e..6b25ed39efb 100644
--- a/spec/models/concerns/limitable_spec.rb
+++ b/spec/models/concerns/limitable_spec.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'active_model'
RSpec.describe Limitable do
let(:minimal_test_class) do
@@ -35,6 +36,28 @@ RSpec.describe Limitable do
instance.valid?(:create)
end
+
+ context 'with custom relation' do
+ before do
+ MinimalTestClass.limit_relation = :custom_relation
+ end
+
+ it 'triggers custom limit_relation' do
+ instance = MinimalTestClass.new
+
+ def instance.project
+ @project ||= Object.new
+ end
+
+ limits = Object.new
+ custom_relation = Object.new
+ expect(instance).to receive(:custom_relation).and_return(custom_relation)
+ expect(instance.project).to receive(:actual_limits).and_return(limits)
+ expect(limits).to receive(:exceeded?).with(instance.class.name.demodulize.tableize, custom_relation).and_return(false)
+
+ instance.valid?(:create)
+ end
+ end
end
context 'with global limit' do
diff --git a/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
index 6f322a32a3b..671e51e3913 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb
@@ -3,25 +3,99 @@
require 'spec_helper'
RSpec.describe TokenAuthenticatableStrategies::EncryptionHelper do
- let(:encrypted_token) { described_class.encrypt_token('my-value') }
+ let(:encrypted_token) { described_class.encrypt_token('my-value-my-value-my-value') }
describe '.encrypt_token' do
- it 'encrypts token' do
- expect(encrypted_token).not_to eq('my-value')
+ context 'when dynamic_nonce feature flag is switched on' do
+ it 'adds nonce identifier on the beginning' do
+ expect(encrypted_token.first).to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
+ end
+
+ it 'adds nonce at the end' do
+ nonce = encrypted_token.last(described_class::NONCE_SIZE)
+
+ expect(nonce).to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
+ end
+
+ it 'encrypts token' do
+ expect(encrypted_token[1...-described_class::NONCE_SIZE]).not_to eq('my-value-my-value-my-value')
+ end
+ end
+
+ context 'when dynamic_nonce feature flag is switched off' do
+ before do
+ stub_feature_flags(dynamic_nonce: false)
+ end
+
+ it 'does not add nonce identifier on the beginning' do
+ expect(encrypted_token.first).not_to eq(described_class::DYNAMIC_NONCE_IDENTIFIER)
+ end
+
+ it 'does not add nonce in the end' do
+ nonce = encrypted_token.last(described_class::NONCE_SIZE)
+
+ expect(nonce).not_to eq(::Digest::SHA256.hexdigest('my-value-my-value-my-value').bytes.take(described_class::NONCE_SIZE).pack('c*'))
+ end
+
+ it 'encrypts token with static iv' do
+ token = Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value-my-value-my-value')
+
+ expect(encrypted_token).to eq(token)
+ end
end
end
describe '.decrypt_token' do
- it 'decrypts token with static iv' do
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
+ context 'with feature flag switched off' do
+ before do
+ stub_feature_flags(dynamic_nonce: false)
+ end
+
+ it 'decrypts token with static iv' do
+ encrypted_token = described_class.encrypt_token('my-value')
+
+ expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
+ end
+
+ it 'decrypts token if feature flag changed after encryption' do
+ encrypted_token = described_class.encrypt_token('my-value')
+
+ expect(encrypted_token).not_to eq('my-value')
+
+ stub_feature_flags(dynamic_nonce: true)
+
+ expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
+ end
+
+ it 'decrypts token with dynamic iv' do
+ iv = ::Digest::SHA256.hexdigest('my-value').bytes.take(described_class::NONCE_SIZE).pack('c*')
+ token = Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value', nonce: iv)
+ encrypted_token = "#{described_class::DYNAMIC_NONCE_IDENTIFIER}#{token}#{iv}"
+
+ expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
+ end
end
- it 'decrypts token with dynamic iv' do
- iv = ::Digest::SHA256.hexdigest('my-value').bytes.take(described_class::NONCE_SIZE).pack('c*')
- token = Gitlab::CryptoHelper.aes256_gcm_encrypt('my-value', nonce: iv)
- encrypted_token = "#{described_class::DYNAMIC_NONCE_IDENTIFIER}#{token}#{iv}"
+ context 'with feature flag switched on' do
+ before do
+ stub_feature_flags(dynamic_nonce: true)
+ end
+
+ it 'decrypts token with dynamic iv' do
+ encrypted_token = described_class.encrypt_token('my-value')
+
+ expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
+ end
+
+ it 'decrypts token if feature flag changed after encryption' do
+ encrypted_token = described_class.encrypt_token('my-value')
+
+ expect(encrypted_token).not_to eq('my-value')
+
+ stub_feature_flags(dynamic_nonce: false)
- expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
+ expect(described_class.decrypt_token(encrypted_token)).to eq('my-value')
+ end
end
end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index abaae5b059a..3232a559d0b 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -320,7 +320,7 @@ RSpec.describe ContainerRepository do
before do
group.parent = test_group
- group.save
+ group.save!
end
it { is_expected.to contain_exactly(repository, another_repository) }
@@ -331,6 +331,40 @@ RSpec.describe ContainerRepository do
it { is_expected.to eq([]) }
end
+
+ context 'with read_container_registry_access_level disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ context 'in a group' do
+ let(:test_group) { group }
+
+ it { is_expected.to contain_exactly(repository) }
+ end
+
+ context 'with a subgroup' do
+ let(:test_group) { create(:group) }
+ let(:another_project) { create(:project, path: 'test', group: test_group) }
+
+ let(:another_repository) do
+ create(:container_repository, name: 'my_image', project: another_project)
+ end
+
+ before do
+ group.parent = test_group
+ group.save!
+ end
+
+ it { is_expected.to contain_exactly(repository, another_repository) }
+ end
+
+ context 'group without container_repositories' do
+ let(:test_group) { create(:group) }
+
+ it { is_expected.to eq([]) }
+ end
+ end
end
describe '.search_by_name' do
@@ -360,6 +394,17 @@ RSpec.describe ContainerRepository do
it { is_expected.to contain_exactly(repository1, repository2, repository4) }
end
+ describe '.with_stale_ongoing_cleanup' do
+ let_it_be(:repository1) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.day.ago) }
+ let_it_be(:repository2) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 25.minutes.ago) }
+ let_it_be(:repository3) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.week.ago) }
+ let_it_be(:repository4) { create(:container_repository, :cleanup_unscheduled, expiration_policy_started_at: 25.minutes.ago) }
+
+ subject { described_class.with_stale_ongoing_cleanup(27.minutes.ago) }
+
+ it { is_expected.to contain_exactly(repository1, repository3) }
+ end
+
describe '.waiting_for_cleanup' do
let_it_be(:repository_cleanup_scheduled) { create(:container_repository, :cleanup_scheduled) }
let_it_be(:repository_cleanup_unfinished) { create(:container_repository, :cleanup_unfinished) }
@@ -423,6 +468,14 @@ RSpec.describe ContainerRepository do
it { is_expected.to eq([repository]) }
end
+
+ context 'with repository cleanup started at after policy next run at' do
+ before do
+ repository.update!(expiration_policy_started_at: policy.next_run_at + 5.minutes)
+ end
+
+ it { is_expected.to eq([]) }
+ end
end
describe '.with_unfinished_cleanup' do
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 9bdee292938..ee13aae50dc 100644
--- a/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
+++ b/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
@@ -33,6 +33,6 @@ RSpec.describe CycleAnalytics::ProjectLevelStageAdapter, type: :model do
end
it 'presents the data as json' do
- expect(subject.as_json).to include({ title: 'Review', value: 'about 1 hour' })
+ expect(subject.as_json).to include({ title: 'Review', value: 1.hour })
end
end
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index c8917a7dd65..dfc37f9e661 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -248,68 +248,54 @@ RSpec.describe DeployToken do
deploy_token.groups << group
end
- context 'and the allow_group_deploy_token feature flag is turned off' do
- it 'is false' do
- stub_feature_flags(allow_group_deploy_token: false)
-
- is_expected.to be_falsy
- end
+ context 'and the passed-in project does not belong to any group' do
+ it { is_expected.to be_falsy }
end
- context 'and the allow_group_deploy_token feature flag is turned on' do
- before do
- stub_feature_flags(allow_group_deploy_token: true)
- end
+ context 'and the passed-in project belongs to the token group' do
+ it 'is true' do
+ group.projects << project
- context 'and the passed-in project does not belong to any group' do
- it { is_expected.to be_falsy }
+ is_expected.to be_truthy
end
+ end
- context 'and the passed-in project belongs to the token group' do
- it 'is true' do
- group.projects << project
+ context 'and the passed-in project belongs to a subgroup' do
+ let(:child_group) { create(:group, parent_id: group.id) }
+ let(:grandchild_group) { create(:group, parent_id: child_group.id) }
- is_expected.to be_truthy
- end
+ before do
+ grandchild_group.projects << project
end
- context 'and the passed-in project belongs to a subgroup' do
- let(:child_group) { create(:group, parent_id: group.id) }
- let(:grandchild_group) { create(:group, parent_id: child_group.id) }
-
- before do
- grandchild_group.projects << project
- end
-
- context 'and the token group is an ancestor (grand-parent) of this group' do
- it { is_expected.to be_truthy }
- end
+ context 'and the token group is an ancestor (grand-parent) of this group' do
+ it { is_expected.to be_truthy }
+ end
- context 'and the token group is not ancestor of this group' do
- let(:child2_group) { create(:group, parent_id: group.id) }
+ context 'and the token group is not ancestor of this group' do
+ let(:child2_group) { create(:group, parent_id: group.id) }
- it 'is false' do
- deploy_token.groups = [child2_group]
+ it 'is false' do
+ deploy_token.groups = [child2_group]
- is_expected.to be_falsey
- end
+ is_expected.to be_falsey
end
end
+ end
- context 'and the passed-in project does not belong to the token group' do
- it { is_expected.to be_falsy }
- end
+ context 'and the passed-in project does not belong to the token group' do
+ it { is_expected.to be_falsy }
+ end
- context 'and the project belongs to a group that is parent of the token group' do
- let(:super_group) { create(:group) }
- let(:deploy_token) { create(:deploy_token, :group) }
- let(:group) { create(:group, parent_id: super_group.id) }
+ context 'and the project belongs to a group that is parent of the token group' do
+ let(:super_group) { create(:group) }
+ let(:deploy_token) { create(:deploy_token, :group) }
+ let(:group) { create(:group, parent_id: super_group.id) }
- it 'is false' do
- super_group.projects << project
+ it 'is false' do
+ super_group.projects << project
- is_expected.to be_falsey
- end
+ is_expected.to be_falsey
end
end
end
diff --git a/spec/models/deployment_metrics_spec.rb b/spec/models/deployment_metrics_spec.rb
index d0474777eb7..fadfc1b63ac 100644
--- a/spec/models/deployment_metrics_spec.rb
+++ b/spec/models/deployment_metrics_spec.rb
@@ -51,10 +51,10 @@ RSpec.describe DeploymentMetrics do
context 'with a cluster Prometheus' do
let(:deployment) { create(:deployment, :success, :on_cluster) }
- let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: deployment.cluster) }
+ let!(:prometheus) { create(:clusters_integrations_prometheus, cluster: deployment.cluster) }
before do
- expect(deployment.cluster.application_prometheus).to receive(:configured?).and_return(true)
+ expect(deployment.cluster.integration_prometheus).to receive(:configured?).and_return(true)
end
it { is_expected.to be_truthy }
@@ -118,7 +118,7 @@ RSpec.describe DeploymentMetrics do
expect(prometheus_adapter).to receive(:query).with(:additional_metrics_deployment, deployment).and_return(simple_metrics)
end
- it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) }
+ it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.finished_at.to_i })) }
end
end
end
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
index 26b311fe629..2a2663149d0 100644
--- a/spec/models/diff_discussion_spec.rb
+++ b/spec/models/diff_discussion_spec.rb
@@ -21,9 +21,9 @@ RSpec.describe DiffDiscussion do
describe '#merge_request_version_params' do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
- let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
- let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ let!(:merge_request_diff1) { merge_request.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { merge_request.merge_request_diffs.create!(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { merge_request.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
context 'when the discussion is active' do
it 'returns an empty hash, which will end up showing the latest version' do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 215c733f26b..2731eadecc0 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -167,7 +167,7 @@ RSpec.describe DiffNote do
end
it 'creates a diff note file' do
- subject.save
+ subject.save!
expect(subject.note_diff_file).to be_present
end
end
@@ -188,7 +188,7 @@ RSpec.describe DiffNote do
end
it 'raises an error' do
- expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError,
+ expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError,
"Failed to find diff line for: #{diff_file.file_path}, "\
"old_line: #{position.old_line}"\
", new_line: #{position.new_line}")
@@ -201,7 +201,7 @@ RSpec.describe DiffNote do
end
it 'creates a diff note file' do
- subject.save
+ subject.save!
expect(subject.reload.note_diff_file).to be_present
end
end
@@ -544,7 +544,7 @@ RSpec.describe DiffNote do
it "does not update the position" do
expect(subject).not_to receive(:update_position)
- subject.save
+ subject.save!
end
end
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index cd0938682db..2b09ee5c190 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Email do
let(:user) { create(:user) }
it 'synchronizes the gpg keys when the email is updated' do
- email = user.emails.create(email: 'new@email.com')
+ email = user.emails.create!(email: 'new@email.com')
expect(user).to receive(:update_invalid_gpg_signatures)
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 759bb080172..ff4c8ae950d 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1157,51 +1157,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
- describe '#prometheus_status' do
- context 'when a cluster is present' do
- context 'when a deployment platform is present' do
- let(:cluster) { create(:cluster, :provided_by_user, :project) }
- let(:environment) { create(:environment, project: cluster.project) }
-
- subject { environment.prometheus_status }
-
- context 'when the prometheus application status is :updating' do
- let!(:prometheus) { create(:clusters_applications_prometheus, :updating, cluster: cluster) }
-
- it { is_expected.to eq(:updating) }
- end
-
- context 'when the prometheus application state is :updated' do
- let!(:prometheus) { create(:clusters_applications_prometheus, :updated, cluster: cluster) }
-
- it { is_expected.to eq(:updated) }
- end
-
- context 'when the prometheus application is not installed' do
- it { is_expected.to be_nil }
- end
- end
-
- context 'when a deployment platform is not present' do
- let(:cluster) { create(:cluster, :project) }
- let(:environment) { create(:environment, project: cluster.project) }
-
- subject { environment.prometheus_status }
-
- it { is_expected.to be_nil }
- end
- end
-
- context 'when a cluster is not present' do
- let(:project) { create(:project, :stubbed_repository) }
- let(:environment) { create(:environment, project: project) }
-
- subject { environment.prometheus_status }
-
- it { is_expected.to be_nil }
- end
- end
-
describe '#additional_metrics' do
let(:project) { create(:prometheus_project) }
let(:metric_params) { [] }
@@ -1434,30 +1389,14 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
let!(:cluster) { create(:cluster, :project, :provided_by_user, projects: [project]) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
- context 'when app does not exist' do
+ context 'when integration does not exist' do
it 'returns false' do
expect(environment.elastic_stack_available?).to be(false)
end
end
- context 'when app exists' do
- let!(:application) { create(:clusters_applications_elastic_stack, cluster: cluster) }
-
- it 'returns false' do
- expect(environment.elastic_stack_available?).to be(false)
- end
- end
-
- context 'when app is installed' do
- let!(:application) { create(:clusters_applications_elastic_stack, :installed, cluster: cluster) }
-
- it 'returns true' do
- expect(environment.elastic_stack_available?).to be(true)
- end
- end
-
- context 'when app is updated' do
- let!(:application) { create(:clusters_applications_elastic_stack, :updated, cluster: cluster) }
+ context 'when integration is enabled' do
+ let!(:integration) { create(:clusters_integrations_elastic_stack, cluster: cluster) }
it 'returns true' do
expect(environment.elastic_stack_available?).to be(true)
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 09a73a4cdcb..1b9b38a0932 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -245,6 +245,17 @@ RSpec.describe EnvironmentStatus do
end
end
+ context 'when there is a deployment in a child pipeline' do
+ let!(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) }
+ let!(:child_build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: child_pipeline) }
+ let(:child_environment) { child_build.deployment.environment }
+
+ it 'returns both parent and child entries' do
+ expect(subject.count).to eq(2)
+ expect(subject.map(&:id)).to contain_exactly(environment.id, child_environment.id)
+ end
+ end
+
context 'when environment is stopped' do
before do
environment.stop!
diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb
index 1517f426fa3..7f0d1e69924 100644
--- a/spec/models/experiment_spec.rb
+++ b/spec/models/experiment_spec.rb
@@ -79,7 +79,7 @@ RSpec.describe Experiment do
context 'when an experiment with the provided name does not exist' do
it 'creates a new experiment record' do
allow_next(described_class, name: :experiment_key)
- .to receive(:record_group_and_variant!).with(group, variant)
+ .to receive(:record_subject_and_variant!).with(group, variant)
expect { add_group }.to change(described_class, :count).by(1)
end
@@ -235,23 +235,23 @@ RSpec.describe Experiment do
end
end
- describe '#record_group_and_variant!' do
- let_it_be(:group) { create(:group) }
+ describe '#record_subject_and_variant!' do
+ let_it_be(:subject_to_record) { create(:group) }
let_it_be(:variant) { :control }
let_it_be(:experiment) { create(:experiment) }
- subject(:record_group_and_variant!) { experiment.record_group_and_variant!(group, variant) }
+ subject(:record_subject_and_variant!) { experiment.record_subject_and_variant!(subject_to_record, variant) }
- context 'when no existing experiment_subject record exists for the given group' do
+ context 'when no existing experiment_subject record exists for the given subject' do
it 'creates an experiment_subject record' do
- expect { record_group_and_variant! }.to change(ExperimentSubject, :count).by(1)
+ expect { record_subject_and_variant! }.to change(ExperimentSubject, :count).by(1)
expect(ExperimentSubject.last.variant).to eq(variant.to_s)
end
end
- context 'when an existing experiment_subject exists for the given group' do
+ context 'when an existing experiment_subject exists for the given subject' do
let_it_be(:experiment_subject) do
- create(:experiment_subject, experiment: experiment, group: group, user: nil, variant: :experimental)
+ create(:experiment_subject, experiment: experiment, namespace: subject_to_record, user: nil, variant: :experimental)
end
context 'when it belongs to the same variant' do
@@ -266,7 +266,55 @@ RSpec.describe Experiment do
context 'but it belonged to a different variant' do
it 'updates the variant value' do
- expect { record_group_and_variant! }.to change { experiment_subject.reload.variant }.to('control')
+ expect { record_subject_and_variant! }.to change { experiment_subject.reload.variant }.to('control')
+ end
+ end
+ end
+
+ describe 'providing a subject to record' do
+ context 'when given a group as subject' do
+ it 'saves the namespace as the experiment subject' do
+ expect(record_subject_and_variant!.namespace).to eq(subject_to_record)
+ end
+ end
+
+ context 'when given a users namespace as subject' do
+ let_it_be(:subject_to_record) { build(:namespace) }
+
+ it 'saves the namespace as the experiment_subject' do
+ expect(record_subject_and_variant!.namespace).to eq(subject_to_record)
+ end
+ end
+
+ context 'when given a user as subject' do
+ let_it_be(:subject_to_record) { build(:user) }
+
+ it 'saves the user as experiment_subject user' do
+ expect(record_subject_and_variant!.user).to eq(subject_to_record)
+ end
+ end
+
+ context 'when given a project as subject' do
+ let_it_be(:subject_to_record) { build(:project) }
+
+ it 'saves the project as experiment_subject user' do
+ expect(record_subject_and_variant!.project).to eq(subject_to_record)
+ end
+ end
+
+ context 'when given no subject' do
+ let_it_be(:subject_to_record) { nil }
+
+ it 'raises an error' do
+ expect { record_subject_and_variant! }.to raise_error('Incompatible subject provided!')
+ end
+ end
+
+ context 'when given an incompatible subject' do
+ let_it_be(:subject_to_record) { build(:ci_build) }
+
+ it 'raises an error' do
+ expect { record_subject_and_variant! }.to raise_error('Incompatible subject provided!')
end
end
end
diff --git a/spec/models/experiment_subject_spec.rb b/spec/models/experiment_subject_spec.rb
index 4850814c5f5..d86dc3cbf65 100644
--- a/spec/models/experiment_subject_spec.rb
+++ b/spec/models/experiment_subject_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ExperimentSubject, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:experiment) }
it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:namespace) }
it { is_expected.to belong_to(:project) }
end
@@ -14,8 +14,8 @@ RSpec.describe ExperimentSubject, type: :model do
it { is_expected.to validate_presence_of(:experiment) }
describe 'must_have_one_subject_present' do
- let(:experiment_subject) { build(:experiment_subject, user: nil, group: nil, project: nil) }
- let(:error_message) { 'Must have exactly one of User, Group, or Project.' }
+ let(:experiment_subject) { build(:experiment_subject, user: nil, namespace: nil, project: nil) }
+ let(:error_message) { 'Must have exactly one of User, Namespace, or Project.' }
it 'fails when no subject is present' do
expect(experiment_subject).not_to be_valid
@@ -27,8 +27,8 @@ RSpec.describe ExperimentSubject, type: :model do
expect(experiment_subject).to be_valid
end
- it 'passes when group subject is present' do
- experiment_subject.group = build(:group)
+ it 'passes when namespace subject is present' do
+ experiment_subject.namespace = build(:group)
expect(experiment_subject).to be_valid
end
@@ -40,7 +40,7 @@ RSpec.describe ExperimentSubject, type: :model do
it 'fails when more than one subject is present', :aggregate_failures do
# two subjects
experiment_subject.user = build(:user)
- experiment_subject.group = build(:group)
+ experiment_subject.namespace = build(:group)
expect(experiment_subject).not_to be_valid
expect(experiment_subject.errors[:base]).to include(error_message)
@@ -51,4 +51,22 @@ RSpec.describe ExperimentSubject, type: :model do
end
end
end
+
+ describe '.valid_subject?' do
+ subject(:valid_subject?) { described_class.valid_subject?(subject_class.new) }
+
+ context 'when passing a Group, Namespace, User or Project' do
+ [Group, Namespace, User, Project].each do |subject_class|
+ let(:subject_class) { subject_class }
+
+ it { is_expected.to be(true) }
+ end
+ end
+
+ context 'when passing another object' do
+ let(:subject_class) { Issue }
+
+ it { is_expected.to be(false) }
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 5cc5c4d86d6..8f4bc43c38a 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -517,6 +517,10 @@ RSpec.describe Group do
it { expect(group.self_and_descendants.to_sql).not_to include 'traversal_ids @>' }
end
+ describe '#self_and_descendant_ids' do
+ it { expect(group.self_and_descendant_ids.to_sql).not_to include 'traversal_ids @>' }
+ end
+
describe '#descendants' do
it { expect(group.descendants.to_sql).not_to include 'traversal_ids @>' }
end
@@ -533,6 +537,10 @@ RSpec.describe Group do
it { expect(group.self_and_descendants.to_sql).to include 'traversal_ids @>' }
end
+ describe '#self_and_descendant_ids' do
+ it { expect(group.self_and_descendant_ids.to_sql).to include 'traversal_ids @>' }
+ end
+
describe '#descendants' do
it { expect(group.descendants.to_sql).to include 'traversal_ids @>' }
end
@@ -1093,167 +1101,151 @@ RSpec.describe Group do
it { expect(subject.parent).to be_kind_of(described_class) }
end
- context "with member access" do
+ describe '#max_member_access_for_user' do
let_it_be(:group_user) { create(:user) }
- describe '#max_member_access_for_user' do
- context 'with user in the group' do
- before do
- group.add_owner(group_user)
- end
-
- it 'returns correct access level' do
- expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::OWNER)
- end
+ context 'with user in the group' do
+ before do
+ group.add_owner(group_user)
end
- context 'when user is nil' do
- it 'returns NO_ACCESS' do
- expect(group.max_member_access_for_user(nil)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::OWNER)
end
+ end
- context 'evaluating admin access level' do
- let_it_be(:admin) { create(:admin) }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'returns OWNER by default' do
- expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
- end
- end
+ context 'when user is nil' do
+ it 'returns NO_ACCESS' do
+ expect(group.max_member_access_for_user(nil)).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
- context 'when admin mode is disabled' do
- it 'returns NO_ACCESS' do
- expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS)
- end
- end
+ context 'evaluating admin access level' do
+ let_it_be(:admin) { create(:admin) }
- it 'returns NO_ACCESS when only concrete membership should be considered' do
- expect(group.max_member_access_for_user(admin, only_concrete_membership: true))
- .to eq(Gitlab::Access::NO_ACCESS)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'returns OWNER by default' do
+ expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER)
end
end
- context 'when max_access_for_group is set' do
- let(:max_member_access) { 111 }
-
- before do
- group_user.max_access_for_group[group.id] = max_member_access
+ context 'when admin mode is disabled' do
+ it 'returns NO_ACCESS' do
+ expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- it 'uses the cached value' do
- expect(group.max_member_access_for_user(group_user)).to eq(max_member_access)
- end
+ it 'returns NO_ACCESS when only concrete membership should be considered' do
+ expect(group.max_member_access_for_user(admin, only_concrete_membership: true))
+ .to eq(Gitlab::Access::NO_ACCESS)
end
end
- describe '#max_member_access' do
- context 'group shared with another group' do
- let_it_be(:parent_group_user) { create(:user) }
- let_it_be(:child_group_user) { create(:user) }
+ context 'group shared with another group' do
+ let_it_be(:parent_group_user) { create(:user) }
+ let_it_be(:child_group_user) { create(:user) }
- let_it_be(:group_parent) { create(:group, :private) }
- let_it_be(:group) { create(:group, :private, parent: group_parent) }
- let_it_be(:group_child) { create(:group, :private, parent: group) }
+ let_it_be(:group_parent) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: group_parent) }
+ let_it_be(:group_child) { create(:group, :private, parent: group) }
- let_it_be(:shared_group_parent) { create(:group, :private) }
- let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
- let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
+ let_it_be(:shared_group_parent) { create(:group, :private) }
+ let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
- before do
- group_parent.add_owner(parent_group_user)
- group.add_owner(group_user)
- group_child.add_owner(child_group_user)
+ before do
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
- create(:group_group_link, { shared_with_group: group,
- shared_group: shared_group,
- group_access: GroupMember::DEVELOPER })
- end
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group,
+ group_access: GroupMember::DEVELOPER })
+ end
- context 'with user in the group' do
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access(group_user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER)
- expect(shared_group_child.max_member_access(group_user)).to eq(Gitlab::Access::DEVELOPER)
- end
+ context 'with user in the group' do
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(group_user)).to eq(Gitlab::Access::DEVELOPER)
+ expect(shared_group_child.max_member_access_for_user(group_user)).to eq(Gitlab::Access::DEVELOPER)
+ end
- context 'with lower group access level than max access level for share' do
- let(:user) { create(:user) }
+ context 'with lower group access level than max access level for share' do
+ let(:user) { create(:user) }
- it 'returns correct access level' do
- group.add_reporter(user)
+ it 'returns correct access level' do
+ group.add_reporter(user)
- expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::REPORTER)
- expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::REPORTER)
- end
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end
+ end
- context 'with user in the parent group' do
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ context 'with user in the parent group' do
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(parent_group_user)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- context 'with user in the child group' do
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ context 'with user in the child group' do
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(child_group_user)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- context 'unrelated project owner' do
- let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
- let!(:group) { create(:group, id: common_id) }
- let!(:unrelated_project) { create(:project, id: common_id) }
- let(:user) { unrelated_project.owner }
+ context 'unrelated project owner' do
+ let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
+ let!(:group) { create(:group, id: common_id) }
+ let!(:unrelated_project) { create(:project, id: common_id) }
+ let(:user) { unrelated_project.owner }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- context 'user without accepted access request' do
- let!(:user) { create(:user) }
+ context 'user without accepted access request' do
+ let!(:user) { create(:user) }
- before do
- create(:group_member, :developer, :access_request, user: user, group: group)
- end
+ before do
+ create(:group_member, :developer, :access_request, user: user, group: group)
+ end
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
+ end
- context 'multiple groups shared with group' do
- let(:user) { create(:user) }
- let(:group) { create(:group, :private) }
- let(:shared_group_parent) { create(:group, :private) }
- let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ context 'multiple groups shared with group' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :private) }
+ let(:shared_group_parent) { create(:group, :private) }
+ let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
- before do
- group.add_owner(user)
+ before do
+ group.add_owner(user)
- create(:group_group_link, { shared_with_group: group,
- shared_group: shared_group,
- group_access: GroupMember::DEVELOPER })
- create(:group_group_link, { shared_with_group: group,
- shared_group: shared_group_parent,
- group_access: GroupMember::MAINTAINER })
- end
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group,
+ group_access: GroupMember::DEVELOPER })
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group_parent,
+ group_access: GroupMember::MAINTAINER })
+ end
- it 'returns correct access level' do
- expect(shared_group.max_member_access(user)).to eq(Gitlab::Access::MAINTAINER)
- end
+ it 'returns correct access level' do
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER)
end
end
end
@@ -2248,14 +2240,16 @@ RSpec.describe Group do
let_it_be(:group) { create(:group, :public) }
it 'returns a maximum of ten owners of the group in recent_sign_in descending order' do
- users = create_list(:user, 12, :with_sign_ins)
+ limit = 2
+ stub_const("Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT", limit)
+ users = create_list(:user, limit + 1, :with_sign_ins)
active_owners = users.map do |user|
create(:group_member, :owner, group: group, user: user)
end
active_owners_in_recent_sign_in_desc_order = group.members_and_requesters
.id_in(active_owners)
- .order_recent_sign_in.limit(10)
+ .order_recent_sign_in.limit(limit)
expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order)
end
@@ -2619,4 +2613,66 @@ RSpec.describe Group do
expect(group.activity_path).to eq(expected_path)
end
end
+
+ context 'with export' do
+ let(:group) { create(:group, :with_export) }
+
+ it '#export_file_exists? returns true' do
+ expect(group.export_file_exists?).to be true
+ end
+
+ it '#export_archive_exists? returns true' do
+ expect(group.export_archive_exists?).to be true
+ end
+ end
+
+ describe '#open_issues_count', :aggregate_failures do
+ let(:group) { build(:group) }
+
+ it 'provides the issue count' do
+ expect(group.open_issues_count).to eq 0
+ end
+
+ it 'invokes the count service with current_user' do
+ user = build(:user)
+ count_service = instance_double(Groups::OpenIssuesCountService)
+ expect(Groups::OpenIssuesCountService).to receive(:new).with(group, user).and_return(count_service)
+ expect(count_service).to receive(:count)
+
+ group.open_issues_count(user)
+ end
+
+ it 'invokes the count service with no current_user' do
+ count_service = instance_double(Groups::OpenIssuesCountService)
+ expect(Groups::OpenIssuesCountService).to receive(:new).with(group, nil).and_return(count_service)
+ expect(count_service).to receive(:count)
+
+ group.open_issues_count
+ end
+ end
+
+ describe '#open_merge_requests_count', :aggregate_failures do
+ let(:group) { build(:group) }
+
+ it 'provides the merge request count' do
+ expect(group.open_merge_requests_count).to eq 0
+ end
+
+ it 'invokes the count service with current_user' do
+ user = build(:user)
+ count_service = instance_double(Groups::MergeRequestsCountService)
+ expect(Groups::MergeRequestsCountService).to receive(:new).with(group, user).and_return(count_service)
+ expect(count_service).to receive(:count)
+
+ group.open_merge_requests_count(user)
+ end
+
+ it 'invokes the count service with no current_user' do
+ count_service = instance_double(Groups::MergeRequestsCountService)
+ expect(Groups::MergeRequestsCountService).to receive(:new).with(group, nil).and_return(count_service)
+ expect(count_service).to receive(:count)
+
+ group.open_merge_requests_count
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 88149465232..d811f67d16b 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -39,4 +39,15 @@ RSpec.describe ProjectHook do
expect(hook.rate_limit).to be(100)
end
end
+
+ describe '#application_context' do
+ let_it_be(:hook) { build(:project_hook) }
+
+ it 'includes the type and project' do
+ expect(hook.application_context).to include(
+ related_class: 'ProjectHook',
+ project: hook.project
+ )
+ end
+ end
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 651716c3280..4ce2e729d89 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -30,4 +30,14 @@ RSpec.describe ServiceHook do
expect(hook.rate_limit).to be_nil
end
end
+
+ describe '#application_context' do
+ let(:hook) { build(:service_hook) }
+
+ it 'includes the type' do
+ expect(hook.application_context).to eq(
+ related_class: 'ServiceHook'
+ )
+ end
+ end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index a72034f1ac5..a99263078b3 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -177,4 +177,14 @@ RSpec.describe SystemHook do
expect(hook.rate_limit).to be_nil
end
end
+
+ describe '#application_context' do
+ let(:hook) { build(:system_hook) }
+
+ it 'includes the type' do
+ expect(hook.application_context).to eq(
+ related_class: 'SystemHook'
+ )
+ end
+ end
end
diff --git a/spec/models/hooks/web_hook_log_archived_spec.rb b/spec/models/hooks/web_hook_log_archived_spec.rb
deleted file mode 100644
index ac726dbaf4f..00000000000
--- a/spec/models/hooks/web_hook_log_archived_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe WebHookLogArchived do
- let(:source_table) { WebHookLog }
- let(:destination_table) { described_class }
-
- it 'has the same columns as the source table' do
- column_names_from_source_table = column_names(source_table)
- column_names_from_destination_table = column_names(destination_table)
-
- expect(column_names_from_destination_table).to match_array(column_names_from_source_table)
- end
-
- it 'has the same null constraints as the source table' do
- constraints_from_source_table = null_constraints(source_table)
- constraints_from_destination_table = null_constraints(destination_table)
-
- expect(constraints_from_destination_table.to_a).to match_array(constraints_from_source_table.to_a)
- end
-
- it 'inserts the same record as the one in the source table', :aggregate_failures do
- expect { create(:web_hook_log) }.to change { destination_table.count }.by(1)
-
- event_from_source_table = source_table.connection.select_one(
- "SELECT * FROM #{source_table.table_name} ORDER BY created_at desc LIMIT 1"
- )
- event_from_destination_table = destination_table.connection.select_one(
- "SELECT * FROM #{destination_table.table_name} ORDER BY created_at desc LIMIT 1"
- )
-
- expect(event_from_destination_table).to eq(event_from_source_table)
- end
-
- def column_names(table)
- table.connection.select_all(<<~SQL)
- SELECT c.column_name
- FROM information_schema.columns c
- WHERE c.table_name = '#{table.table_name}'
- SQL
- end
-
- def null_constraints(table)
- table.connection.select_all(<<~SQL)
- SELECT c.column_name, c.is_nullable
- FROM information_schema.columns c
- WHERE c.table_name = '#{table.table_name}'
- AND c.column_name != 'created_at'
- SQL
- end
-end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index b528dbedd2c..1761b537dc0 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -268,11 +268,58 @@ RSpec.describe WebHook do
end
describe '#enable!' do
- it 'makes a hook executable' 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.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
+ expect(hook).not_to receive(:update!)
+
+ hook.enable!
+ end
+
+ it 'is idempotent on executable hooks' do
+ expect(hook).not_to receive(:update!)
+
+ expect { hook.enable! }.not_to change(hook, :executable?)
+ end
+ end
+
+ describe 'backoff!' do
+ it 'sets disabled_until to the next backoff' do
+ expect { hook.backoff! }.to change(hook, :disabled_until).to(hook.next_backoff.from_now)
+ end
+
+ it 'increments the backoff count' do
+ expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
+ end
+
+ it 'does not let the backoff count exceed the maximum failure count' do
+ hook.backoff_count = described_class::MAX_FAILURES
+
+ expect { hook.backoff! }.not_to change(hook, :backoff_count)
+ end
+ end
+
+ describe 'failed!' do
+ it 'increments the failure count' do
+ expect { hook.failed! }.to change(hook, :recent_failures).by(1)
+ end
+
+ it 'does not allow the failure count to exceed the maximum value' do
+ hook.recent_failures = described_class::MAX_FAILURES
+ expect(hook).not_to receive(:update!)
+
+ expect { hook.failed! }.not_to change(hook, :recent_failures)
+ end
end
describe '#disable!' do
diff --git a/spec/models/import_export_upload_spec.rb b/spec/models/import_export_upload_spec.rb
index 46a611852ab..e13f504b82a 100644
--- a/spec/models/import_export_upload_spec.rb
+++ b/spec/models/import_export_upload_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe ImportExportUpload do
- subject { described_class.new(project: create(:project)) }
+ let(:project) { create(:project) }
+
+ subject { described_class.new(project: project) }
shared_examples 'stores the Import/Export file' do |method|
it 'stores the import file' do
@@ -24,4 +26,99 @@ RSpec.describe ImportExportUpload do
context 'export' do
it_behaves_like 'stores the Import/Export file', :export_file
end
+
+ describe 'scopes' do
+ let_it_be(:upload1) { create(:import_export_upload, export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz')) }
+ let_it_be(:upload2) { create(:import_export_upload) }
+ let_it_be(:upload3) { create(:import_export_upload, export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'), updated_at: 25.hours.ago) }
+ let_it_be(:upload4) { create(:import_export_upload, updated_at: 2.days.ago) }
+
+ describe '.with_export_file' do
+ it 'returns uploads with export file' do
+ expect(described_class.with_export_file).to contain_exactly(upload1, upload3)
+ end
+ end
+
+ describe '.updated_before' do
+ it 'returns uploads for a specified date' do
+ expect(described_class.updated_before(24.hours.ago)).to contain_exactly(upload3, upload4)
+ end
+ end
+ end
+
+ context 'ActiveRecord callbacks' do
+ let(:after_save_callbacks) { described_class._save_callbacks.select { |cb| cb.kind == :after } }
+ let(:after_commit_callbacks) { described_class._commit_callbacks.select { |cb| cb.kind == :after } }
+
+ def find_callback(callbacks, key)
+ callbacks.find { |cb| cb.instance_variable_get(:@key) == key }
+ end
+
+ it 'export file is stored in after_commit callback' do
+ expect(find_callback(after_commit_callbacks, :store_export_file!)).to be_present
+ expect(find_callback(after_save_callbacks, :store_export_file!)).to be_nil
+ end
+
+ it 'import file is stored in after_save callback' do
+ expect(find_callback(after_save_callbacks, :store_import_file!)).to be_present
+ expect(find_callback(after_commit_callbacks, :store_import_file!)).to be_nil
+ end
+ end
+
+ describe 'export file' do
+ it '#export_file_exists? returns false' do
+ expect(subject.export_file_exists?).to be false
+ end
+
+ it '#export_archive_exists? returns false' do
+ expect(subject.export_archive_exists?).to be false
+ end
+
+ context 'with export' do
+ let(:project_with_export) { create(:project, :with_export) }
+
+ subject { described_class.with_export_file.find_by(project: project_with_export) }
+
+ it '#export_file_exists? returns true' do
+ expect(subject.export_file_exists?).to be true
+ end
+
+ it '#export_archive_exists? returns false' do
+ expect(subject.export_archive_exists?).to be true
+ end
+
+ context 'when object file does not exist' do
+ before do
+ subject.export_file.file.delete
+ end
+
+ it '#export_file_exists? returns true' do
+ expect(subject.export_file_exists?).to be true
+ end
+
+ it '#export_archive_exists? returns false' do
+ expect(subject.export_archive_exists?).to be false
+ end
+ end
+
+ context 'when checking object existence raises a error' do
+ let(:exception) { Excon::Error::Forbidden.new('not allowed') }
+
+ before do
+ file = double
+ allow(file).to receive(:exists?).and_raise(exception)
+ allow(subject).to receive(:carrierwave_export_file).and_return(file)
+ end
+
+ it '#export_file_exists? returns true' do
+ expect(subject.export_file_exists?).to be true
+ end
+
+ it '#export_archive_exists? returns false' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception)
+ expect(subject.export_archive_exists?).to be false
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 77b3778122a..d4ea3e5d08a 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -18,6 +18,7 @@ RSpec.describe Integration do
describe 'validations' do
it { is_expected.to validate_presence_of(:type) }
+ it { is_expected.to validate_exclusion_of(:type).in_array(described_class::BASE_CLASSES) }
where(:project_id, :group_id, :template, :instance, :valid) do
1 | nil | false | false | true
@@ -159,7 +160,7 @@ RSpec.describe Integration do
context 'when instance-level service' do
Integration.available_services_types.each do |service_type|
let(:service) do
- service_type.constantize.new(instance: true)
+ described_class.send(:integration_type_to_model, service_type).new(instance: true)
end
it { is_expected.to be_falsey }
@@ -169,7 +170,7 @@ RSpec.describe Integration do
context 'when group-level service' do
Integration.available_services_types.each do |service_type|
let(:service) do
- service_type.constantize.new(group_id: group.id)
+ described_class.send(:integration_type_to_model, service_type).new(group_id: group.id)
end
it { is_expected.to be_falsey }
@@ -446,7 +447,7 @@ RSpec.describe Integration do
describe "for pushover service" do
let!(:service_template) do
- PushoverService.create!(
+ Integrations::Pushover.create!(
template: true,
properties: {
device: 'MyDevice',
@@ -667,16 +668,16 @@ RSpec.describe Integration do
end
end
- describe '.service_name_to_model' do
+ describe '.integration_name_to_model' do
it 'returns the model for the given service name', :aggregate_failures do
- expect(described_class.service_name_to_model('asana')).to eq(Integrations::Asana)
+ expect(described_class.integration_name_to_model('asana')).to eq(Integrations::Asana)
# TODO We can remove this test when all models have been namespaced:
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60968#note_570994955
- expect(described_class.service_name_to_model('youtrack')).to eq(YoutrackService)
+ expect(described_class.integration_name_to_model('prometheus')).to eq(PrometheusService)
end
it 'raises an error if service name is invalid' do
- expect { described_class.service_name_to_model('foo') }.to raise_exception(NameError, /uninitialized constant FooService/)
+ expect { described_class.integration_name_to_model('foo') }.to raise_exception(NameError, /uninitialized constant FooService/)
end
end
@@ -802,7 +803,7 @@ RSpec.describe Integration do
describe 'initialize service with no properties' do
let(:service) do
- BugzillaService.create!(
+ Integrations::Bugzilla.create!(
project: project,
project_url: 'http://gitlab.example.com'
)
@@ -896,20 +897,6 @@ RSpec.describe Integration do
end
end
- describe '#external_wiki?' do
- where(:type, :active, :result) do
- 'ExternalWikiService' | true | true
- 'ExternalWikiService' | false | false
- 'SlackService' | true | false
- end
-
- with_them do
- it 'returns the right result' do
- expect(build(:service, type: type, active: active).external_wiki?).to eq(result)
- end
- end
- end
-
describe '.available_services_names' do
it 'calls the right methods' do
expect(described_class).to receive(:services_names).and_call_original
diff --git a/spec/models/integrations/assembla_spec.rb b/spec/models/integrations/assembla_spec.rb
index bf9033416e9..e5972bce95d 100644
--- a/spec/models/integrations/assembla_spec.rb
+++ b/spec/models/integrations/assembla_spec.rb
@@ -15,8 +15,8 @@ RSpec.describe Integrations::Assembla do
let(:project) { create(:project, :repository) }
before do
- @assembla_service = described_class.new
- allow(@assembla_service).to receive_messages(
+ @assembla_integration = described_class.new
+ allow(@assembla_integration).to receive_messages(
project_id: project.id,
project: project,
service_hook: true,
@@ -29,7 +29,7 @@ RSpec.describe Integrations::Assembla do
end
it "calls Assembla API" do
- @assembla_service.execute(@sample_data)
+ @assembla_integration.execute(@sample_data)
expect(WebMock).to have_requested(:post, stubbed_hostname(@api_url)).with(
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
).once
diff --git a/spec/models/integrations/bamboo_spec.rb b/spec/models/integrations/bamboo_spec.rb
index 0ba1595bbd8..39966f7978d 100644
--- a/spec/models/integrations/bamboo_spec.rb
+++ b/spec/models/integrations/bamboo_spec.rb
@@ -82,45 +82,45 @@ RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
describe 'before_update :reset_password' do
context 'when a password was previously set' do
it 'resets password if url changed' do
- bamboo_service = service
+ bamboo_integration = service
- bamboo_service.bamboo_url = 'http://gitlab1.com'
- bamboo_service.save!
+ bamboo_integration.bamboo_url = 'http://gitlab1.com'
+ bamboo_integration.save!
- expect(bamboo_service.password).to be_nil
+ expect(bamboo_integration.password).to be_nil
end
it 'does not reset password if username changed' do
- bamboo_service = service
+ bamboo_integration = service
- bamboo_service.username = 'some_name'
- bamboo_service.save!
+ bamboo_integration.username = 'some_name'
+ bamboo_integration.save!
- expect(bamboo_service.password).to eq('password')
+ expect(bamboo_integration.password).to eq('password')
end
it "does not reset password if new url is set together with password, even if it's the same password" do
- bamboo_service = service
+ bamboo_integration = service
- bamboo_service.bamboo_url = 'http://gitlab_edited.com'
- bamboo_service.password = 'password'
- bamboo_service.save!
+ bamboo_integration.bamboo_url = 'http://gitlab_edited.com'
+ bamboo_integration.password = 'password'
+ bamboo_integration.save!
- expect(bamboo_service.password).to eq('password')
- expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
+ expect(bamboo_integration.password).to eq('password')
+ expect(bamboo_integration.bamboo_url).to eq('http://gitlab_edited.com')
end
end
it 'saves password if new url is set together with password when no password was previously set' do
- bamboo_service = service
- bamboo_service.password = nil
+ bamboo_integration = service
+ bamboo_integration.password = nil
- bamboo_service.bamboo_url = 'http://gitlab_edited.com'
- bamboo_service.password = 'password'
- bamboo_service.save!
+ bamboo_integration.bamboo_url = 'http://gitlab_edited.com'
+ bamboo_integration.password = 'password'
+ bamboo_integration.save!
- expect(bamboo_service.password).to eq('password')
- expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
+ expect(bamboo_integration.password).to eq('password')
+ expect(bamboo_integration.bamboo_url).to eq('http://gitlab_edited.com')
end
end
end
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 62f97873a06..656eaa3bbdd 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ChatNotificationService do
+RSpec.describe Integrations::BaseChatNotification do
describe 'Associations' do
before do
allow(subject).to receive(:activated?).and_return(true)
@@ -89,12 +89,6 @@ RSpec.describe ChatNotificationService do
let(:data) { Gitlab::DataBuilder::Note.build(note, user) }
- it 'notifies the chat service' do
- expect(chat_service).to receive(:notify).with(any_args)
-
- chat_service.execute(data)
- end
-
shared_examples 'notifies the chat service' do
specify do
expect(chat_service).to receive(:notify).with(any_args)
@@ -111,6 +105,26 @@ RSpec.describe ChatNotificationService do
end
end
+ it_behaves_like 'notifies the chat service'
+
+ context 'with label filter' do
+ subject(:chat_service) { described_class.new(labels_to_be_notified: '~Bug') }
+
+ it_behaves_like 'notifies the chat service'
+
+ context 'MergeRequest events' do
+ let(:data) { create(:merge_request, labels: [label]).to_hook_data(user) }
+
+ it_behaves_like 'notifies the chat service'
+ end
+
+ context 'Issue events' do
+ let(:data) { issue.to_hook_data(user) }
+
+ it_behaves_like 'notifies the chat service'
+ end
+ end
+
context 'when labels_to_be_notified_behavior is not defined' do
subject(:chat_service) { described_class.new(labels_to_be_notified: label_filter) }
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/integrations/base_issue_tracker_spec.rb
index 5b12c7330b8..0f1bc39929a 100644
--- a/spec/models/project_services/issue_tracker_service_spec.rb
+++ b/spec/models/integrations/base_issue_tracker_spec.rb
@@ -2,15 +2,15 @@
require 'spec_helper'
-RSpec.describe IssueTrackerService do
+RSpec.describe Integrations::BaseIssueTracker do
describe 'Validations' do
let(:project) { create :project }
describe 'only one issue tracker per project' do
- let(:service) { RedmineService.new(project: project, active: true, issue_tracker_data: build(:issue_tracker_data)) }
+ let(:service) { Integrations::Redmine.new(project: project, active: true, issue_tracker_data: build(:issue_tracker_data)) }
before do
- create(:custom_issue_tracker_service, project: project)
+ create(:custom_issue_tracker_integration, project: project)
end
context 'when service is changed manually by user' do
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/integrations/bugzilla_spec.rb
index 560c7c3ee83..e75fa8dd4d4 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/integrations/bugzilla_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BugzillaService do
+RSpec.describe Integrations::Bugzilla do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/integrations/buildkite_spec.rb
index f6bf1551bf0..7dc81da7003 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/integrations/buildkite_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do
+RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
include StubRequests
diff --git a/spec/models/integrations/campfire_spec.rb b/spec/models/integrations/campfire_spec.rb
index b23edf03e8a..d68f8e0bd4e 100644
--- a/spec/models/integrations/campfire_spec.rb
+++ b/spec/models/integrations/campfire_spec.rb
@@ -33,8 +33,8 @@ RSpec.describe Integrations::Campfire do
let(:project) { create(:project, :repository) }
before do
- @campfire_service = described_class.new
- allow(@campfire_service).to receive_messages(
+ @campfire_integration = described_class.new
+ allow(@campfire_integration).to receive_messages(
project_id: project.id,
project: project,
service_hook: true,
@@ -62,7 +62,7 @@ RSpec.describe Integrations::Campfire do
speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
stub_full_request(speak_url, method: :post).with(basic_auth: @auth)
- @campfire_service.execute(@sample_data)
+ @campfire_integration.execute(@sample_data)
expect(WebMock).to have_requested(:get, stubbed_hostname(@rooms_url)).once
expect(WebMock).to have_requested(:post, stubbed_hostname(speak_url))
@@ -78,7 +78,7 @@ RSpec.describe Integrations::Campfire do
headers: @headers
)
- @campfire_service.execute(@sample_data)
+ @campfire_integration.execute(@sample_data)
expect(WebMock).to have_requested(:get, 'https://8.8.8.9/rooms.json').once
expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
diff --git a/spec/models/integrations/chat_message/wiki_page_message_spec.rb b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
index e8672a0f9c8..ded467dc910 100644
--- a/spec/models/integrations/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
@@ -5,20 +5,30 @@ require 'spec_helper'
RSpec.describe Integrations::ChatMessage::WikiPageMessage do
subject { described_class.new(args) }
+ let(:name) { 'Test User' }
+ let(:username) { 'test.user' }
+ let(:avatar_url) { 'http://someavatar.com' }
+ let(:project_name) { 'project_name' }
+ let(:project_url) {'http://somewhere.com' }
+ let(:url) { 'http://url.com' }
+ let(:diff_url) { 'http://url.com/diff?version_id=1234' }
+ let(:wiki_page_title) { 'Wiki page title' }
+ let(:commit_message) { 'Wiki page commit message' }
let(:args) do
{
user: {
- name: 'Test User',
- username: 'test.user',
- avatar_url: 'http://someavatar.com'
+ name: name,
+ username: username,
+ avatar_url: avatar_url
},
- project_name: 'project_name',
- project_url: 'http://somewhere.com',
+ project_name: project_name,
+ project_url: project_url,
object_attributes: {
- title: 'Wiki page title',
- url: 'http://url.com',
+ title: wiki_page_title,
+ url: url,
content: 'Wiki page content',
- message: 'Wiki page commit message'
+ message: commit_message,
+ diff_url: diff_url
}
}
end
@@ -32,8 +42,8 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'Test User (test.user) created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
- '*Wiki page title*')
+ "#{name} (#{username}) created <#{url}|wiki page> (<#{diff_url}|Compare changes>) in <#{project_url}|#{project_name}>: "\
+ "*#{wiki_page_title}*")
end
end
@@ -44,8 +54,8 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'Test User (test.user) edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
- '*Wiki page title*')
+ "#{name} (#{username}) edited <#{url}|wiki page> (<#{diff_url}|Compare changes>) in <#{project_url}|#{project_name}>: "\
+ "*#{wiki_page_title}*")
end
end
end
@@ -61,7 +71,7 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
it 'returns the commit message for a new wiki page' do
expect(subject.attachments).to eq([
{
- text: "Wiki page commit message",
+ text: commit_message,
color: color
}
])
@@ -76,7 +86,7 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
it 'returns the commit message for an updated wiki page' do
expect(subject.attachments).to eq([
{
- text: "Wiki page commit message",
+ text: commit_message,
color: color
}
])
@@ -98,7 +108,7 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'Test User (test.user) created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ "#{name} (#{username}) created [wiki page](#{url}) ([Compare changes](#{diff_url})) in [#{project_name}](#{project_url}): *#{wiki_page_title}*")
end
end
@@ -109,7 +119,7 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'Test User (test.user) edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ "#{name} (#{username}) edited [wiki page](#{url}) ([Compare changes](#{diff_url})) in [#{project_name}](#{project_url}): *#{wiki_page_title}*")
end
end
end
@@ -121,7 +131,7 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
end
it 'returns the commit message for a new wiki page' do
- expect(subject.attachments).to eq('Wiki page commit message')
+ expect(subject.attachments).to eq(commit_message)
end
end
@@ -131,7 +141,7 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
end
it 'returns the commit message for an updated wiki page' do
- expect(subject.attachments).to eq('Wiki page commit message')
+ expect(subject.attachments).to eq(commit_message)
end
end
end
diff --git a/spec/models/integrations/confluence_spec.rb b/spec/models/integrations/confluence_spec.rb
index c217573f48d..08e18c99376 100644
--- a/spec/models/integrations/confluence_spec.rb
+++ b/spec/models/integrations/confluence_spec.rb
@@ -72,19 +72,19 @@ RSpec.describe Integrations::Confluence do
subject { project.project_setting.has_confluence? }
it 'sets the property to true when service is active' do
- create(:confluence_service, project: project, active: true)
+ create(:confluence_integration, project: project, active: true)
is_expected.to be(true)
end
it 'sets the property to false when service is not active' do
- create(:confluence_service, project: project, active: false)
+ create(:confluence_integration, project: project, active: false)
is_expected.to be(false)
end
it 'creates a project_setting record if one was not already created' do
- expect { create(:confluence_service) }.to change { ProjectSetting.count }.by(1)
+ expect { create(:confluence_integration) }.to change(ProjectSetting, :count).by(1)
end
end
end
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/integrations/custom_issue_tracker_spec.rb
index 881ae60a680..25f2648e738 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/integrations/custom_issue_tracker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CustomIssueTrackerService do
+RSpec.describe Integrations::CustomIssueTracker do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/discord_service_spec.rb b/spec/models/integrations/discord_spec.rb
index ffe0a36dcdc..bff6a8ee5b2 100644
--- a/spec/models/project_services/discord_service_spec.rb
+++ b/spec/models/integrations/discord_spec.rb
@@ -2,8 +2,8 @@
require "spec_helper"
-RSpec.describe DiscordService do
- it_behaves_like "chat service", "Discord notifications" do
+RSpec.describe Integrations::Discord do
+ it_behaves_like "chat integration", "Discord notifications" do
let(:client) { Discordrb::Webhooks::Client }
let(:client_arguments) { { url: webhook_url } }
let(:payload) do
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/integrations/drone_ci_spec.rb
index 9aaf4f7a644..137f078edca 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/integrations/drone_ci_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
+RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
describe 'associations' do
@@ -31,8 +31,8 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
end
end
- shared_context :drone_ci_service do
- let(:drone) { DroneCiService.new }
+ shared_context :drone_ci_integration do
+ let(:drone) { described_class.new }
let(:project) { create(:project, :repository, name: 'project') }
let(:path) { project.full_path }
let(:drone_url) { 'http://drone.example.com' }
@@ -41,7 +41,7 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
let(:token) { 'secret' }
let(:iid) { rand(1..9999) }
- # URL's
+ # URLs
let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
@@ -67,14 +67,14 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
end
describe "service page/path methods" do
- include_context :drone_ci_service
+ include_context :drone_ci_integration
it { expect(drone.build_page(sha, branch)).to eq(build_page) }
it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) }
end
describe '#commit_status' do
- include_context :drone_ci_service
+ include_context :drone_ci_integration
it 'returns the contents of the reactive cache' do
stub_reactive_cache(drone, { commit_status: 'foo' }, 'sha', 'ref')
@@ -84,7 +84,7 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
end
describe '#calculate_reactive_cache' do
- include_context :drone_ci_service
+ include_context :drone_ci_integration
describe '#commit_status' do
subject { drone.calculate_reactive_cache(sha, branch)[:commit_status] }
@@ -130,7 +130,7 @@ RSpec.describe DroneCiService, :use_clean_rails_memory_store_caching do
end
describe "execute" do
- include_context :drone_ci_service
+ include_context :drone_ci_integration
let(:user) { create(:user, username: 'username') }
let(:push_sample_data) do
diff --git a/spec/models/project_services/ewm_service_spec.rb b/spec/models/integrations/ewm_spec.rb
index 311c456569e..38897adb447 100644
--- a/spec/models/project_services/ewm_service_spec.rb
+++ b/spec/models/integrations/ewm_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe EwmService do
+RSpec.describe Integrations::Ewm do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/integrations/external_wiki_spec.rb
index c6891401a0f..8c20b810301 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/integrations/external_wiki_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ExternalWikiService do
+RSpec.describe Integrations::ExternalWiki do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/integrations/flowdock_spec.rb
index 94a49fb3080..2de6f7dd2f1 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/integrations/flowdock_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe FlowdockService do
+RSpec.describe Integrations::Flowdock do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/hangouts_chat_service_spec.rb b/spec/models/integrations/hangouts_chat_spec.rb
index 9d3bd457fc8..17b40c484f5 100644
--- a/spec/models/project_services/hangouts_chat_service_spec.rb
+++ b/spec/models/integrations/hangouts_chat_spec.rb
@@ -2,8 +2,8 @@
require "spec_helper"
-RSpec.describe HangoutsChatService do
- it_behaves_like "chat service", "Hangouts Chat" do
+RSpec.describe Integrations::HangoutsChat do
+ it_behaves_like "chat integration", "Hangouts Chat" do
let(:client) { HangoutsChat::Sender }
let(:client_arguments) { webhook_url }
let(:payload) do
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/integrations/irker_spec.rb
index 07963947de8..a69be1292e0 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/integrations/irker_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'socket'
require 'json'
-RSpec.describe IrkerService do
+RSpec.describe Integrations::Irker do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/issue_tracker_data_spec.rb b/spec/models/integrations/issue_tracker_data_spec.rb
index a229285f09b..597df237c67 100644
--- a/spec/models/project_services/issue_tracker_data_spec.rb
+++ b/spec/models/integrations/issue_tracker_data_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe IssueTrackerData do
+RSpec.describe Integrations::IssueTrackerData do
describe 'associations' do
it { is_expected.to belong_to :integration }
end
diff --git a/spec/models/project_services/jenkins_service_spec.rb b/spec/models/integrations/jenkins_spec.rb
index 4663e41736a..2374dfe4480 100644
--- a/spec/models/project_services/jenkins_service_spec.rb
+++ b/spec/models/integrations/jenkins_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JenkinsService do
+RSpec.describe Integrations::Jenkins do
let(:project) { create(:project) }
let(:jenkins_url) { 'http://jenkins.example.com/' }
let(:jenkins_hook_url) { jenkins_url + 'project/my_project' }
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/integrations/jira_spec.rb
index 73e91bf9ea8..f6310866773 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraService do
+RSpec.describe Integrations::Jira do
include AssetsHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -35,20 +35,42 @@ RSpec.describe JiraService do
username: 'username',
password: 'test',
jira_issue_transition_id: 24,
- url: 'http://jira.test.com/path/'
+ url: 'http://jira.test.com:1234/path/'
}
end
- let(:service) { described_class.create!(options) }
+ let(:integration) { described_class.create!(options) }
it 'sets the URL properly' do
# jira-ruby gem parses the URI and handles trailing slashes fine:
# https://github.com/sumoheavy/jira-ruby/blob/v1.7.0/lib/jira/http_client.rb#L62
- expect(service.options[:site]).to eq('http://jira.test.com/')
+ expect(integration.options[:site]).to eq('http://jira.test.com:1234')
end
it 'leaves out trailing slashes in context' do
- expect(service.options[:context_path]).to eq('/path')
+ expect(integration.options[:context_path]).to eq('/path')
+ end
+
+ context 'URL without a path' do
+ before do
+ integration.url = 'http://jira.test.com/'
+ end
+
+ it 'leaves out trailing slashes in context' do
+ expect(integration.options[:site]).to eq('http://jira.test.com')
+ expect(integration.options[:context_path]).to eq('')
+ end
+ end
+
+ context 'URL with query string parameters' do
+ before do
+ integration.url << '?nosso&foo=bar'
+ end
+
+ it 'removes query string parameters' do
+ expect(integration.options[:site]).to eq('http://jira.test.com:1234')
+ expect(integration.options[:context_path]).to eq('/path')
+ end
end
context 'username with trailing whitespaces' do
@@ -57,13 +79,13 @@ RSpec.describe JiraService do
end
it 'leaves out trailing whitespaces in username' do
- expect(service.options[:username]).to eq('username')
+ expect(integration.options[:username]).to eq('username')
end
end
it 'provides additional cookies to allow basic auth with oracle webgate' do
- expect(service.options[:use_cookies]).to eq(true)
- expect(service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
+ expect(integration.options[:use_cookies]).to eq(true)
+ expect(integration.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
end
context 'using api URL' do
@@ -72,7 +94,7 @@ RSpec.describe JiraService do
end
it 'leaves out trailing slashes in context' do
- expect(service.options[:context_path]).to eq('/api_path')
+ expect(integration.options[:context_path]).to eq('/api_path')
end
end
end
@@ -117,7 +139,8 @@ RSpec.describe JiraService do
let(:params) do
{
project: project,
- url: url, api_url: api_url,
+ url: url,
+ api_url: api_url,
username: username, password: password,
jira_issue_transition_id: transition_id
}
@@ -141,9 +164,9 @@ RSpec.describe JiraService do
end
context 'when loading serverInfo' do
- let!(:jira_service) { subject }
+ let(:jira_service) { subject }
- context 'Cloud instance' do
+ context 'from a Cloud instance' do
let(:server_info_results) { { 'deploymentType' => 'Cloud' } }
it 'is detected' do
@@ -151,7 +174,7 @@ RSpec.describe JiraService do
end
end
- context 'Server instance' do
+ context 'from a Server instance' do
let(:server_info_results) { { 'deploymentType' => 'Server' } }
it 'is detected' do
@@ -159,11 +182,45 @@ RSpec.describe JiraService do
end
end
- context 'Unknown instance' do
+ context 'from an Unknown instance' do
let(:server_info_results) { { 'deploymentType' => 'FutureCloud' } }
- it 'is detected' do
- expect(jira_service.jira_tracker_data.deployment_unknown?).to be_truthy
+ context 'and URL ends in .atlassian.net' do
+ let(:api_url) { 'http://example-api.atlassian.net' }
+
+ it 'deployment_type is set to cloud' do
+ expect(jira_service.jira_tracker_data.deployment_cloud?).to be_truthy
+ end
+ end
+
+ context 'and URL is something else' do
+ let(:api_url) { 'http://my-jira-api.someserver.com' }
+
+ it 'deployment_type is set to server' do
+ expect(jira_service.jira_tracker_data.deployment_server?).to be_truthy
+ end
+ end
+ end
+
+ context 'and no ServerInfo response is received' do
+ let(:server_info_results) { {} }
+
+ context 'and URL ends in .atlassian.net' do
+ let(:api_url) { 'http://example-api.atlassian.net' }
+
+ it 'deployment_type is set to cloud' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(message: "Jira API returned no ServerInfo, setting deployment_type from URL", server_info: server_info_results, url: api_url)
+ expect(jira_service.jira_tracker_data.deployment_cloud?).to be_truthy
+ end
+ end
+
+ context 'and URL is something else' do
+ let(:api_url) { 'http://my-jira-api.someserver.com' }
+
+ it 'deployment_type is set to server' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(message: "Jira API returned no ServerInfo, setting deployment_type from URL", server_info: server_info_results, url: api_url)
+ expect(jira_service.jira_tracker_data.deployment_server?).to be_truthy
+ end
end
end
end
@@ -229,17 +286,31 @@ RSpec.describe JiraService do
end
context 'when updating the url, api_url, username, or password' do
- it 'updates deployment type' do
- service.update!(url: 'http://first.url')
- service.jira_tracker_data.update!(deployment_type: 'server')
+ context 'when updating the integration' do
+ it 'updates deployment type' do
+ service.update!(url: 'http://first.url')
+ service.jira_tracker_data.update!(deployment_type: 'server')
- expect(service.jira_tracker_data.deployment_server?).to be_truthy
+ expect(service.jira_tracker_data.deployment_server?).to be_truthy
- service.update!(api_url: 'http://another.url')
- service.jira_tracker_data.reload
+ service.update!(api_url: 'http://another.url')
+ service.jira_tracker_data.reload
- expect(service.jira_tracker_data.deployment_cloud?).to be_truthy
- expect(WebMock).to have_requested(:get, /serverInfo/).twice
+ expect(service.jira_tracker_data.deployment_cloud?).to be_truthy
+ expect(WebMock).to have_requested(:get, /serverInfo/).twice
+ end
+ end
+
+ context 'when removing the integration' do
+ let(:server_info_results) { {} }
+
+ it 'updates deployment type' do
+ service.update!(url: nil, api_url: nil, active: false)
+
+ service.jira_tracker_data.reload
+
+ expect(service.jira_tracker_data.deployment_unknown?).to be_truthy
+ end
end
it 'calls serverInfo for url' do
@@ -493,7 +564,7 @@ RSpec.describe JiraService do
before do
jira_service.jira_issue_transition_id = '999'
- # These stubs are needed to test JiraService#close_issue.
+ # These stubs are needed to test Integrations::Jira#close_issue.
# We close the issue then do another request to API to check if it got closed.
# Here is stubbed the API return with a closed and an opened issues.
open_issue = JIRA::Resource::Issue.new(jira_service.client, attrs: issue_fields.deep_stringify_keys)
@@ -829,7 +900,7 @@ RSpec.describe JiraService do
context 'when disabled' do
before do
- allow_next_instance_of(JiraService) do |instance|
+ allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:commit_events) { false }
end
end
@@ -847,7 +918,7 @@ RSpec.describe JiraService do
context 'when disabled' do
before do
- allow_next_instance_of(JiraService) do |instance|
+ allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:merge_requests_events) { false }
end
end
@@ -941,17 +1012,51 @@ RSpec.describe JiraService do
end
context 'generating external URLs' do
- let(:service) { described_class.new(url: 'http://jira.test.com/path/') }
+ let(:integration) { described_class.new(url: 'http://jira.test.com/path/') }
+
+ describe '#web_url' do
+ it 'handles paths, slashes, and query string' do
+ expect(integration.web_url).to eq(integration.url)
+ expect(integration.web_url('subpath/')).to eq('http://jira.test.com/path/subpath')
+ expect(integration.web_url('/subpath/')).to eq('http://jira.test.com/path/subpath')
+ expect(integration.web_url('subpath', foo: :bar)).to eq('http://jira.test.com/path/subpath?foo=bar')
+ end
+
+ it 'preserves existing query string' do
+ integration.url = 'http://jira.test.com/path/?nosso&foo=bar%20bar'
+
+ expect(integration.web_url).to eq("http://jira.test.com/path?foo=bar%20bar&nosso")
+ expect(integration.web_url('subpath/')).to eq('http://jira.test.com/path/subpath?foo=bar%20bar&nosso')
+ expect(integration.web_url('/subpath/')).to eq('http://jira.test.com/path/subpath?foo=bar%20bar&nosso')
+ expect(integration.web_url('subpath', bar: 'baz baz')).to eq('http://jira.test.com/path/subpath?bar=baz%20baz&foo=bar%20bar&nosso')
+ end
+
+ it 'includes Atlassian referrer for gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ expect(integration.web_url).to eq("http://jira.test.com/path?#{described_class::ATLASSIAN_REFERRER_GITLAB_COM.to_query}")
+
+ allow(Gitlab).to receive(:staging?).and_return(true)
+
+ expect(integration.web_url).to eq(integration.url)
+ end
+
+ it 'includes Atlassian referrer for self-managed' do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+
+ expect(integration.web_url).to eq("http://jira.test.com/path?#{described_class::ATLASSIAN_REFERRER_SELF_MANAGED.to_query}")
+ end
+ end
describe '#issues_url' do
- it 'handles trailing slashes' do
- expect(service.issues_url).to eq('http://jira.test.com/path/browse/:id')
+ it 'returns the correct URL' do
+ expect(integration.issues_url).to eq('http://jira.test.com/path/browse/:id')
end
end
describe '#new_issue_url' do
- it 'handles trailing slashes' do
- expect(service.new_issue_url).to eq('http://jira.test.com/path/secure/CreateIssue!default.jspa')
+ it 'returns the correct URL' do
+ expect(integration.new_issue_url).to eq('http://jira.test.com/path/secure/CreateIssue!default.jspa')
end
end
end
diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/integrations/jira_tracker_data_spec.rb
index 72bdbe40a74..5430dd2eb52 100644
--- a/spec/models/project_services/jira_tracker_data_spec.rb
+++ b/spec/models/integrations/jira_tracker_data_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JiraTrackerData do
+RSpec.describe Integrations::JiraTrackerData do
describe 'associations' do
it { is_expected.to belong_to(:integration) }
end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/integrations/mattermost_slash_commands_spec.rb
index 87befdd4303..c8a6584591c 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/integrations/mattermost_slash_commands_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe MattermostSlashCommandsService do
- it_behaves_like "chat slash commands service"
+RSpec.describe Integrations::MattermostSlashCommands do
+ it_behaves_like Integrations::BaseSlashCommands
context 'Mattermost API' do
let(:project) { create(:project) }
@@ -11,10 +11,10 @@ RSpec.describe MattermostSlashCommandsService do
let(:user) { create(:user) }
before do
- session = Mattermost::Session.new(nil)
+ session = ::Mattermost::Session.new(nil)
session.base_uri = 'http://mattermost.example.com'
- allow_any_instance_of(Mattermost::Client).to receive(:with_session)
+ allow_any_instance_of(::Mattermost::Client).to receive(:with_session)
.and_yield(session)
end
diff --git a/spec/models/integrations/mattermost_spec.rb b/spec/models/integrations/mattermost_spec.rb
new file mode 100644
index 00000000000..f7702846b6c
--- /dev/null
+++ b/spec/models/integrations/mattermost_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Mattermost do
+ it_behaves_like Integrations::SlackMattermostNotifier, "Mattermost"
+end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/integrations/microsoft_teams_spec.rb
index 5f3a94a5b99..2f1be233eb2 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/integrations/microsoft_teams_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MicrosoftTeamsService do
+RSpec.describe Integrations::MicrosoftTeams do
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
@@ -64,7 +64,7 @@ RSpec.describe MicrosoftTeamsService do
end
it 'specifies the webhook when it is configured' do
- expect(MicrosoftTeams::Notifier).to receive(:new).with(webhook_url).and_return(double(:microsoft_teams_service).as_null_object)
+ expect(::MicrosoftTeams::Notifier).to receive(:new).with(webhook_url).and_return(double(:microsoft_teams_service).as_null_object)
chat_service.execute(push_sample_data)
end
diff --git a/spec/models/project_services/open_project_service_spec.rb b/spec/models/integrations/open_project_spec.rb
index 1abaab0ceff..e5b976dc91d 100644
--- a/spec/models/project_services/open_project_service_spec.rb
+++ b/spec/models/integrations/open_project_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe OpenProjectService do
+RSpec.describe Integrations::OpenProject do
describe 'Validations' do
context 'when service is active' do
before do
diff --git a/spec/models/project_services/open_project_tracker_data_spec.rb b/spec/models/integrations/open_project_tracker_data_spec.rb
index 1f7f01cfea4..41c913f978c 100644
--- a/spec/models/project_services/open_project_tracker_data_spec.rb
+++ b/spec/models/integrations/open_project_tracker_data_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe OpenProjectTrackerData do
+RSpec.describe Integrations::OpenProjectTrackerData do
describe 'associations' do
it { is_expected.to belong_to(:integration) }
end
diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/integrations/packagist_spec.rb
index 33b5c9809c7..48f7e81adca 100644
--- a/spec/models/project_services/packagist_service_spec.rb
+++ b/spec/models/integrations/packagist_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PackagistService do
+RSpec.describe Integrations::Packagist do
let(:packagist_params) do
{
active: true,
diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/integrations/pipelines_email_spec.rb
index 21cc5d44558..90055b04bb8 100644
--- a/spec/models/project_services/pipelines_email_service_spec.rb
+++ b/spec/models/integrations/pipelines_email_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PipelinesEmailService, :mailer do
+RSpec.describe Integrations::PipelinesEmail, :mailer do
let(:pipeline) do
create(:ci_pipeline, :failed,
project: project,
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/integrations/pivotaltracker_spec.rb
index 8de85cc7fa5..2ce90b6f739 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/integrations/pivotaltracker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PivotaltrackerService do
+RSpec.describe Integrations::Pivotaltracker do
include StubRequests
describe 'Associations' do
@@ -35,7 +35,7 @@ RSpec.describe PivotaltrackerService do
end
end
- let(:url) { PivotaltrackerService::API_ENDPOINT }
+ let(:url) { described_class::API_ENDPOINT }
def push_data(branch: 'master')
{
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/integrations/pushover_spec.rb
index b7d3b8987b8..be8dc5634a0 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/integrations/pushover_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PushoverService do
+RSpec.describe Integrations::Pushover do
include StubRequests
describe 'Associations' do
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/integrations/redmine_spec.rb
index b9be3940d34..083585d4fed 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/integrations/redmine_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RedmineService do
+RSpec.describe Integrations::Redmine do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/slack_slash_commands_service_spec.rb b/spec/models/integrations/slack_slash_commands_spec.rb
index 95c87ef01bc..a9d3c820a3c 100644
--- a/spec/models/project_services/slack_slash_commands_service_spec.rb
+++ b/spec/models/integrations/slack_slash_commands_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe SlackSlashCommandsService do
- it_behaves_like "chat slash commands service"
+RSpec.describe Integrations::SlackSlashCommands do
+ it_behaves_like Integrations::BaseSlashCommands
describe '#trigger' do
context 'when an auth url is generated' do
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/integrations/slack_spec.rb
index 2e2c1c666d9..e598c528967 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/integrations/slack_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
-RSpec.describe SlackService do
- it_behaves_like "slack or mattermost notifications", 'Slack'
+RSpec.describe Integrations::Slack do
+ it_behaves_like Integrations::SlackMattermostNotifier, "Slack"
describe '#execute' do
before do
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/integrations/teamcity_spec.rb
index f71dad86a08..b88a4722ad4 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/integrations/teamcity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
+RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
include StubRequests
diff --git a/spec/models/project_services/unify_circuit_service_spec.rb b/spec/models/integrations/unify_circuit_spec.rb
index 0c749322e07..7a91b2d3c11 100644
--- a/spec/models/project_services/unify_circuit_service_spec.rb
+++ b/spec/models/integrations/unify_circuit_spec.rb
@@ -2,8 +2,8 @@
require "spec_helper"
-RSpec.describe UnifyCircuitService do
- it_behaves_like "chat service", "Unify Circuit" do
+RSpec.describe Integrations::UnifyCircuit do
+ it_behaves_like "chat integration", "Unify Circuit" do
let(:client_arguments) { webhook_url }
let(:payload) do
{
diff --git a/spec/models/project_services/webex_teams_service_spec.rb b/spec/models/integrations/webex_teams_spec.rb
index ed63f5bc48c..b5cba6762aa 100644
--- a/spec/models/project_services/webex_teams_service_spec.rb
+++ b/spec/models/integrations/webex_teams_spec.rb
@@ -2,8 +2,8 @@
require "spec_helper"
-RSpec.describe WebexTeamsService do
- it_behaves_like "chat service", "Webex Teams" do
+RSpec.describe Integrations::WebexTeams do
+ it_behaves_like "chat integration", "Webex Teams" do
let(:client_arguments) { webhook_url }
let(:payload) do
{
diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/integrations/youtrack_spec.rb
index 4339b44e1de..314204f6fb4 100644
--- a/spec/models/project_services/youtrack_service_spec.rb
+++ b/spec/models/integrations/youtrack_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe YoutrackService do
+RSpec.describe Integrations::Youtrack do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 884c476932e..edb93ecf4b6 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Issue do
include ExternalAuthorizationServiceHelpers
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:user) { create(:user) }
let_it_be(:reusable_project) { create(:project) }
@@ -1287,15 +1289,33 @@ RSpec.describe Issue do
end
end
- let(:project) { build_stubbed(:project_empty_repo) }
- let(:issue) { build_stubbed(:issue, relative_position: 100, project: project) }
+ shared_examples 'schedules issues rebalancing' do
+ let(:issue) { build_stubbed(:issue, relative_position: 100, project: project) }
+
+ it 'schedules rebalancing if we time-out when moving' do
+ lhs = build_stubbed(:issue, relative_position: 99, project: project)
+ to_move = build(:issue, project: project)
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project_id, namespace_id)
+
+ expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled)
+ end
+ end
+
+ context 'when project in user namespace' do
+ let(:project) { build_stubbed(:project_empty_repo) }
+ let(:project_id) { project.id }
+ let(:namespace_id) { nil }
+
+ it_behaves_like 'schedules issues rebalancing'
+ end
- it 'schedules rebalancing if we time-out when moving' do
- lhs = build_stubbed(:issue, relative_position: 99, project: project)
- to_move = build(:issue, project: project)
- expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
+ context 'when project in a group namespace' do
+ let(:group) { create(:group) }
+ let(:project) { build_stubbed(:project_empty_repo, group: group) }
+ let(:project_id) { nil }
+ let(:namespace_id) { group.id }
- expect { to_move.move_between(lhs, issue) }.to raise_error(ActiveRecord::QueryCanceled)
+ it_behaves_like 'schedules issues rebalancing'
end
end
@@ -1315,6 +1335,26 @@ RSpec.describe Issue do
end
end
+ describe '#supports_time_tracking?' do
+ let_it_be(:project) { create(:project) }
+ let_it_be_with_refind(:issue) { create(:incident, project: project) }
+
+ where(:issue_type, :supports_time_tracking) do
+ :issue | true
+ :incident | true
+ end
+
+ with_them do
+ before do
+ issue.update!(issue_type: issue_type)
+ end
+
+ it do
+ expect(issue.supports_time_tracking?).to eq(supports_time_tracking)
+ end
+ end
+ end
+
describe '#email_participants_emails' do
let_it_be(:issue) { create(:issue) }
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 0cb20efcb0a..7468c1b9f0a 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -85,9 +85,9 @@ RSpec.describe Key, :mailer do
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 4.days.from_now, user: user, before_expiry_notification_delivered_at: Time.current) }
let_it_be(:future_expiry) { create(:key, expires_at: 1.month.from_now, user: user) }
- describe '.expired_today_and_not_notified' do
- it 'returns keys that expire today' do
- expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified)
+ describe '.expired_and_not_notified' do
+ it 'returns keys that expire today and in the past' do
+ expect(described_class.expired_and_not_notified).to contain_exactly(expired_today_not_notified, expired_yesterday)
end
end
@@ -139,7 +139,7 @@ RSpec.describe Key, :mailer do
end
with_them do
- let!(:key) { create(factory) }
+ let!(:key) { create(factory) } # rubocop:disable Rails/SaveBang
let!(:original_fingerprint) { key.fingerprint }
let!(:original_fingerprint_sha256) { key.fingerprint_sha256 }
@@ -224,7 +224,7 @@ RSpec.describe Key, :mailer do
expect(AuthorizedKeysWorker).to receive(:perform_async).with(:remove_key, key.shell_id)
- key.destroy
+ key.destroy!
end
end
@@ -244,7 +244,7 @@ RSpec.describe Key, :mailer do
expect(AuthorizedKeysWorker).not_to receive(:perform_async)
- key.destroy
+ key.destroy!
end
end
end
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index e5753b34e72..c6bec215145 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -13,27 +13,14 @@ RSpec.describe LabelLink do
let(:invalid_items_for_bulk_insertion) { [] } # class does not have any validations defined
end
- describe 'scopes' do
- describe '.for_target' do
- it 'returns the label links for a given target' do
- label_link = create(:label_link, target: create(:merge_request))
+ describe '.for_target' do
+ it 'returns the label links for a given target' do
+ label_link = create(:label_link, target: create(:merge_request))
- create(:label_link, target: create(:issue))
+ create(:label_link, target: create(:issue))
- expect(described_class.for_target(label_link.target_id, label_link.target_type))
- .to contain_exactly(label_link)
- end
- end
-
- describe '.with_remove_on_close_labels' do
- it 'responds with label_links that can be removed when an issue is closed' do
- issue = create(:issue)
- removable_label = create(:label, project: issue.project, remove_on_close: true)
- create(:label_link, target: issue)
- removable_issue_label_link = create(:label_link, label: removable_label, target: issue)
-
- expect(described_class.with_remove_on_close_labels).to contain_exactly(removable_issue_label_link)
- end
+ expect(described_class.for_target(label_link.target_id, label_link.target_type))
+ .to contain_exactly(label_link)
end
end
end
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index a0f633218b0..5210709a468 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -178,4 +178,34 @@ RSpec.describe LfsObject do
expect(described_class.calculate_oid(path)).to eq expected
end
end
+
+ context 'when an lfs object is associated with a project' do
+ let!(:lfs_object) { create(:lfs_object) }
+ let!(:lfs_object_project) { create(:lfs_objects_project, lfs_object: lfs_object) }
+
+ it 'cannot be deleted' do
+ expect { lfs_object.destroy! }.to raise_error(ActiveRecord::InvalidForeignKey)
+
+ lfs_object_project.destroy!
+
+ expect { lfs_object.destroy! }.not_to raise_error
+ end
+ end
+
+ describe '.unreferenced_in_batches' do
+ let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') }
+ let!(:referenced_lfs_object) { create(:lfs_objects_project).lfs_object }
+ let!(:unreferenced_lfs_object2) { create(:lfs_object, oid: '2') }
+
+ it 'returns lfs objects in batches' do
+ stub_const('LfsObject::BATCH_SIZE', 1)
+
+ batches = []
+ described_class.unreferenced_in_batches { |batch| batches << batch }
+
+ expect(batches.size).to eq(2)
+ expect(batches.first).to eq([unreferenced_lfs_object2])
+ expect(batches.last).to eq([unreferenced_lfs_object1])
+ end
+ end
end
diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb
index 71009a6f28f..df49b60c4fa 100644
--- a/spec/models/lfs_objects_project_spec.rb
+++ b/spec/models/lfs_objects_project_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe LfsObjectsProject do
expect(ProjectCacheWorker).to receive(:perform_async)
.with(project.id, [], [:lfs_objects_size])
- subject.destroy
+ subject.destroy!
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 247be7654d8..372fc40afcc 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -408,6 +408,20 @@ RSpec.describe Member do
it { is_expected.not_to include @member_with_minimal_access }
end
+ describe '.without_invites_and_requests' do
+ subject { described_class.without_invites_and_requests.to_a }
+
+ it { is_expected.to include @owner }
+ it { is_expected.to include @maintainer }
+ it { is_expected.not_to include @invited_member }
+ it { is_expected.to include @accepted_invite_member }
+ it { is_expected.not_to include @requested_member }
+ it { is_expected.to include @accepted_request_member }
+ it { is_expected.to include @blocked_maintainer }
+ it { is_expected.to include @blocked_developer }
+ it { is_expected.not_to include @member_with_minimal_access }
+ end
+
describe '.connected_to_user' do
subject { described_class.connected_to_user.to_a }
@@ -594,6 +608,18 @@ RSpec.describe Member do
end
end
+ context 'when called with a known user secondary email' do
+ let(:secondary_email) { create(:email, email: 'secondary@example.com', user: user) }
+
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, secondary_email.email, :maintainer)
+
+ expect(source.users.reload).to include(user)
+ end
+ end
+
context 'when called with an unknown user email' do
it 'creates an invited member' do
expect(source.users).not_to include(user)
@@ -778,10 +804,27 @@ RSpec.describe Member do
let(:invited_member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:requester) { create(:project_member, requested_at: Time.current.utc) }
- it { expect(invited_member).to be_invite }
+ it { expect(invited_member).to be_pending }
it { expect(requester).to be_pending }
end
+ describe '#hook_prerequisites_met?' do
+ let(:member) { create(:project_member) }
+
+ context 'when the member does not have an associated user' do
+ it 'returns false' do
+ member.update_column(:user_id, nil)
+ expect(member.reload.hook_prerequisites_met?).to eq(false)
+ end
+ end
+
+ context 'when the member has an associated user' do
+ it 'returns true' do
+ expect(member.hook_prerequisites_met?).to eq(true)
+ end
+ end
+ end
+
describe "#accept_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 3a2db5d8516..8c942228059 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -37,6 +37,10 @@ RSpec.describe GroupMember do
end
end
+ describe 'delegations' do
+ it { is_expected.to delegate_method(:update_two_factor_requirement).to(:user).allow_nil }
+ end
+
describe '.access_level_roles' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner)
@@ -93,6 +97,18 @@ RSpec.describe GroupMember do
end
end
+ describe '#destroy' do
+ context 'for an orphaned member' do
+ let!(:orphaned_group_member) do
+ create(:group_member).tap { |member| member.update_column(:user_id, nil) }
+ end
+
+ it 'does not raise an error' do
+ expect { orphaned_group_member.destroy! }.not_to raise_error
+ end
+ end
+ end
+
describe '#after_accept_invite' do
it 'calls #update_two_factor_requirement' do
email = 'foo@email.com'
diff --git a/spec/models/members/last_group_owner_assigner_spec.rb b/spec/models/members/last_group_owner_assigner_spec.rb
index 3c9a7a11555..bb0f751e7d5 100644
--- a/spec/models/members/last_group_owner_assigner_spec.rb
+++ b/spec/models/members/last_group_owner_assigner_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Members::LastGroupOwnerAssigner do
+RSpec.describe LastGroupOwnerAssigner do
describe "#execute" do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index fa77e319c2c..b84b408cb4b 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -58,6 +58,16 @@ RSpec.describe ProjectMember do
maintainer.destroy!
expect(Event.recent.first).to be_left_action
end
+
+ context 'for an orphaned member' do
+ let!(:orphaned_project_member) do
+ owner.tap { |member| member.update_column(:user_id, nil) }
+ end
+
+ it 'does not raise an error' do
+ expect { orphaned_project_member.destroy! }.not_to raise_error
+ end
+ end
end
describe '.import_team' do
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 4075eb96fc2..3741e01e99a 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -418,8 +418,8 @@ RSpec.describe MergeRequestDiff do
shared_examples_for 'fetching full diffs' do
it 'returns diffs from repository comparison' do
expect_next_instance_of(Compare) do |comparison|
- expect(comparison).to receive(:diffs_in_batch)
- .with(1, 10, diff_options: diff_options)
+ expect(comparison).to receive(:diffs)
+ .with(diff_options)
.and_call_original
end
@@ -448,13 +448,13 @@ RSpec.describe MergeRequestDiff do
end
it_behaves_like 'fetching full diffs'
- end
- context 'when diff_options include ignore_whitespace_change' do
- it_behaves_like 'fetching full diffs' do
+ context 'when diff_options include ignore_whitespace_change' do
let(:diff_options) do
{ ignore_whitespace_change: true }
end
+
+ it_behaves_like 'fetching full diffs'
end
end
@@ -485,6 +485,51 @@ RSpec.describe MergeRequestDiff do
'files/whitespace'
])
end
+
+ context 'when diff_options include ignore_whitespace_change' do
+ let(:diff_options) do
+ { ignore_whitespace_change: true }
+ end
+
+ it 'returns a Gitlab::Diff::FileCollection::Compare with paginated diffs' do
+ diffs = diff_with_commits.diffs_in_batch(1, 10, diff_options: diff_options)
+
+ expect(diffs).to be_a(Gitlab::Diff::FileCollection::Compare)
+ expect(diffs.diff_files.size).to eq 10
+ expect(diffs.pagination_data).to eq(current_page: 1, next_page: 2, total_pages: 2)
+ end
+
+ it 'returns an empty MergeRequestBatch with empty pagination data when the batch is empty' do
+ diffs = diff_with_commits.diffs_in_batch(3, 10, diff_options: diff_options)
+
+ expect(diffs).to be_a(Gitlab::Diff::FileCollection::MergeRequestDiffBatch)
+ expect(diffs.diff_files.size).to eq 0
+ expect(diffs.pagination_data).to eq(current_page: nil, next_page: nil, total_pages: nil)
+ end
+
+ context 'with gradual load enabled' do
+ before do
+ stub_feature_flags(diffs_gradual_load: true)
+ end
+
+ it 'returns pagination data from MergeRequestDiffBatch' do
+ diffs = diff_with_commits.diffs_in_batch(1, 10, diff_options: diff_options)
+ file_count = diff_with_commits.merge_request_diff_files.count
+
+ expect(diffs).to be_a(Gitlab::Diff::FileCollection::Compare)
+ expect(diffs.diff_files.size).to eq 10
+ expect(diffs.pagination_data).to eq(current_page: nil, next_page: nil, total_pages: file_count)
+ end
+
+ it 'returns an empty MergeRequestBatch with empty pagination data when the batch is empty' do
+ diffs = diff_with_commits.diffs_in_batch(30, 10, diff_options: diff_options)
+
+ expect(diffs).to be_a(Gitlab::Diff::FileCollection::MergeRequestDiffBatch)
+ expect(diffs.diff_files.size).to eq 0
+ expect(diffs.pagination_data).to eq(current_page: nil, next_page: nil, total_pages: nil)
+ end
+ end
+ end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a77ca1e9a51..94b4c1901b8 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -99,16 +99,17 @@ RSpec.describe MergeRequest, factory_default: :keep do
let_it_be(:merge_request1) { create(:merge_request, :unique_branches, reviewers: [user1])}
let_it_be(:merge_request2) { create(:merge_request, :unique_branches, reviewers: [user2])}
let_it_be(:merge_request3) { create(:merge_request, :unique_branches, reviewers: [])}
+ let_it_be(:merge_request4) { create(:merge_request, :draft_merge_request)}
describe '.review_requested' do
- it 'returns MRs that has any review requests' do
+ it 'returns MRs that have any review requests' do
expect(described_class.review_requested).to eq([merge_request1, merge_request2])
end
end
describe '.no_review_requested' do
- it 'returns MRs that has no review requests' do
- expect(described_class.no_review_requested).to eq([merge_request3])
+ it 'returns MRs that have no review requests' do
+ expect(described_class.no_review_requested).to eq([merge_request3, merge_request4])
end
end
@@ -119,8 +120,15 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
describe '.no_review_requested_to' do
- it 'returns MRs that the user has been requested to review' do
- expect(described_class.no_review_requested_to(user1)).to eq([merge_request2, merge_request3])
+ it 'returns MRs that the user has not been requested to review' do
+ expect(described_class.no_review_requested_to(user1))
+ .to eq([merge_request2, merge_request3, merge_request4])
+ end
+ end
+
+ describe '.drafts' do
+ it 'returns MRs where draft == true' do
+ expect(described_class.drafts).to eq([merge_request4])
end
end
end
@@ -296,7 +304,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
it 'does not create duplicated metrics records when MR is concurrently updated' do
- merge_request.metrics.destroy
+ merge_request.metrics.destroy!
instance1 = MergeRequest.find(merge_request.id)
instance2 = MergeRequest.find(merge_request.id)
@@ -317,6 +325,38 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(merge_request.target_project_id).to eq(merge_request.metrics.target_project_id)
end
end
+
+ describe '#set_draft_status' do
+ let(:merge_request) { create(:merge_request) }
+
+ context 'MR is a draft' do
+ before do
+ expect(merge_request.draft).to be_falsy
+
+ merge_request.title = "Draft: #{merge_request.title}"
+ end
+
+ it 'sets draft to true' do
+ merge_request.save!
+
+ expect(merge_request.draft).to be_truthy
+ end
+ end
+
+ context 'MR is not a draft' do
+ before do
+ expect(merge_request.draft).to be_falsey
+
+ merge_request.title = "This is not a draft"
+ end
+
+ it 'sets draft to true' do
+ merge_request.save!
+
+ expect(merge_request.draft).to be_falsey
+ end
+ end
+ end
end
describe 'respond to' do
@@ -347,7 +387,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
it 'returns empty requests' do
- latest_merge_request_diff = merge_request.merge_request_diffs.create
+ latest_merge_request_diff = merge_request.merge_request_diffs.create!
MergeRequestDiffCommit.where(
merge_request_diff_id: latest_merge_request_diff,
@@ -459,7 +499,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
}
create(:merge_request, params).tap do |mr|
- diffs.times { mr.merge_request_diffs.create }
+ diffs.times { mr.merge_request_diffs.create! }
mr.create_merge_head_diff
end
end
@@ -891,7 +931,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'when there are MR diffs' do
it 'delegates to the MR diffs' do
- merge_request.save
+ merge_request.save!
expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options)).and_call_original
@@ -1036,20 +1076,20 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'when there are MR diffs' do
it 'returns the correct count' do
- merge_request.save
+ merge_request.save!
expect(merge_request.diff_size).to eq('105')
end
it 'returns the correct overflow count' do
allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
- merge_request.save
+ merge_request.save!
expect(merge_request.diff_size).to eq('2+')
end
it 'does not perform highlighting' do
- merge_request.save
+ merge_request.save!
expect(Gitlab::Diff::Highlight).not_to receive(:new)
@@ -1470,7 +1510,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
it "can't remove a root ref" do
- subject.update(source_branch: 'master', target_branch: 'feature')
+ subject.update!(source_branch: 'master', target_branch: 'feature')
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
@@ -2501,7 +2541,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'with a completely different branch' do
before do
- subject.update(target_branch: 'csv')
+ subject.update!(target_branch: 'csv')
end
it_behaves_like 'returning all SHA'
@@ -2509,7 +2549,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'with a branch having no difference' do
before do
- subject.update(target_branch: 'branch-merged')
+ subject.update!(target_branch: 'branch-merged')
subject.reload # make sure commits were not cached
end
@@ -3207,7 +3247,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'and a failed pipeline is associated' do
before do
- pipeline.update(status: 'failed', sha: subject.diff_head_sha)
+ pipeline.update!(status: 'failed', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -3216,7 +3256,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'and a successful pipeline is associated' do
before do
- pipeline.update(status: 'success', sha: subject.diff_head_sha)
+ pipeline.update!(status: 'success', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -3225,7 +3265,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
context 'and a skipped pipeline is associated' do
before do
- pipeline.update(status: 'skipped', sha: subject.diff_head_sha)
+ pipeline.update!(status: 'skipped', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline).and_return(pipeline)
end
@@ -3530,7 +3570,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
before do
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
allow(subject).to receive(:create_merge_request_diff) do
- subject.merge_request_diffs.create(
+ subject.merge_request_diffs.create!(
base_commit_sha: commit.parent_id,
start_commit_sha: commit.parent_id,
head_commit_sha: commit.sha
@@ -3800,7 +3840,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
it 'returns false if the merge request is merged' do
- merge_request.update(state: 'merged')
+ merge_request.update!(state: 'merged')
expect(merge_request.reload.reopenable?).to be_falsey
end
@@ -3880,14 +3920,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
let(:service_class) { 'Ci::CompareMetricsReportsService' }
it { is_expected.to be_truthy }
-
- context 'with the metrics report flag disabled' do
- before do
- stub_feature_flags(merge_base_pipeline_for_metrics_comparison: false)
- end
-
- it { is_expected.to be_falsey }
- end
end
context 'when service class is Ci::CompareCodequalityReportsService' do
@@ -4029,9 +4061,9 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject { create(:merge_request, importing: true, source_project: project) }
- let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
- let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ let!(:merge_request_diff1) { subject.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { subject.merge_request_diffs.create!(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { subject.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
context 'with diff refs' do
it 'returns the diffs' do
@@ -4062,9 +4094,9 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject { create(:merge_request, importing: true, source_project: project) }
- let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
- let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ let!(:merge_request_diff1) { subject.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { subject.merge_request_diffs.create!(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { subject.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
context 'when the diff refs are for an older merge request version' do
let(:diff_refs) { merge_request_diff1.diff_refs }
@@ -4108,7 +4140,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
it 'refreshes the number of open merge requests of the target project' do
project = subject.target_project
- expect { subject.destroy }
+ expect { subject.destroy! }
.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
@@ -4874,7 +4906,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject { merge_request.enabled_reports[report_type] }
before do
- stub_feature_flags(drop_license_management_artifact: false)
stub_licensed_features({ feature => true })
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 20dee288052..7cf7c360dff 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -8,7 +8,46 @@ RSpec.describe Milestone do
let(:milestone) { create(:milestone, project: project) }
let(:project) { create(:project, :public) }
- it_behaves_like 'a timebox', :milestone
+ it_behaves_like 'a timebox', :milestone do
+ describe "#uniqueness_of_title" do
+ context "per project" do
+ it "does not accept the same title in a project twice" do
+ new_timebox = timebox.dup
+ expect(new_timebox).not_to be_valid
+ end
+
+ it "accepts the same title in another project" do
+ project = create(:project)
+ new_timebox = timebox.dup
+ new_timebox.project = project
+
+ expect(new_timebox).to be_valid
+ end
+ end
+
+ context "per group" do
+ let(:timebox) { create(:milestone, *timebox_args, group: group) }
+
+ before do
+ project.update!(group: group)
+ end
+
+ it "does not accept the same title in a group twice" do
+ new_timebox = described_class.new(group: group, title: timebox.title)
+
+ expect(new_timebox).not_to be_valid
+ end
+
+ it "does not accept the same title of a child project timebox" do
+ create(:milestone, *timebox_args, project: group.projects.first)
+
+ new_timebox = described_class.new(group: group, title: timebox.title)
+
+ expect(new_timebox).not_to be_valid
+ end
+ end
+ end
+ end
describe 'MilestoneStruct#serializable_hash' do
let(:predefined_milestone) { described_class::TimeboxStruct.new('Test Milestone', '#test', 1) }
@@ -158,7 +197,7 @@ RSpec.describe Milestone do
it 'returns false if milestone active and not all nested issues closed' do
issue.milestone = milestone
- issue.save
+ issue.save!
expect(milestone.can_be_closed?).to be_falsey
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 56afe49e15f..2c514593de8 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Namespace do
include ProjectForksHelper
include GitHelpers
+ include ReloadHelpers
let!(:namespace) { create(:namespace, :with_namespace_settings) }
let(:gitlab_shell) { Gitlab::Shell.new }
@@ -199,6 +200,8 @@ RSpec.describe Namespace do
it { is_expected.to include_module(Namespaces::Traversal::Linear) }
end
+ it_behaves_like 'linear namespace traversal'
+
context 'traversal_ids on create' do
context 'default traversal_ids' do
let(:namespace) { build(:namespace) }
@@ -804,9 +807,9 @@ RSpec.describe Namespace do
end
it 'updates the project storage location' do
- repository_project_in_parent_group = create(:project_repository, project: project_in_parent_group)
- repository_hashed_project_in_subgroup = create(:project_repository, project: hashed_project_in_subgroup)
- repository_legacy_project_in_subgroup = create(:project_repository, project: legacy_project_in_subgroup)
+ repository_project_in_parent_group = project_in_parent_group.project_repository
+ repository_hashed_project_in_subgroup = hashed_project_in_subgroup.project_repository
+ repository_legacy_project_in_subgroup = legacy_project_in_subgroup.project_repository
parent.update(path: 'mygroup_moved')
@@ -1010,47 +1013,52 @@ RSpec.describe Namespace do
end
end
- describe '#all_projects' do
+ shared_examples '#all_projects' do
context 'when namespace is a group' do
- let(:namespace) { create(:group) }
- let(:child) { create(:group, parent: namespace) }
- let!(:project1) { create(:project_empty_repo, namespace: namespace) }
- let!(:project2) { create(:project_empty_repo, namespace: child) }
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:child) { create(:group, parent: namespace) }
+ let_it_be(:project1) { create(:project_empty_repo, namespace: namespace) }
+ let_it_be(:project2) { create(:project_empty_repo, namespace: child) }
+ let_it_be(:other_project) { create(:project_empty_repo) }
+
+ before do
+ reload_models(namespace, child)
+ end
it { expect(namespace.all_projects.to_a).to match_array([project2, project1]) }
it { expect(child.all_projects.to_a).to match_array([project2]) }
-
- it 'queries for the namespace and its descendants' do
- expect(Project).to receive(:where).with(namespace: [namespace, child])
-
- namespace.all_projects
- end
end
context 'when namespace is a user namespace' do
let_it_be(:user) { create(:user) }
let_it_be(:user_namespace) { create(:namespace, owner: user) }
let_it_be(:project) { create(:project, namespace: user_namespace) }
+ let_it_be(:other_project) { create(:project_empty_repo) }
- it { expect(user_namespace.all_projects.to_a).to match_array([project]) }
+ before do
+ reload_models(user_namespace)
+ end
- it 'only queries for the namespace itself' do
- expect(Project).to receive(:where).with(namespace: user_namespace)
+ it { expect(user_namespace.all_projects.to_a).to match_array([project]) }
+ end
+ end
- user_namespace.all_projects
+ describe '#all_projects' do
+ context 'with use_traversal_ids feature flag enabled' do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
end
+
+ include_examples '#all_projects'
end
- end
- describe '#all_pipelines' do
- let(:group) { create(:group) }
- let(:child) { create(:group, parent: group) }
- let!(:project1) { create(:project_empty_repo, namespace: group) }
- let!(:project2) { create(:project_empty_repo, namespace: child) }
- let!(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
- let!(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
+ context 'with use_traversal_ids feature flag disabled' do
+ before do
+ stub_feature_flags(use_traversal_ids: false)
+ end
- it { expect(group.all_pipelines.to_a).to match_array([pipeline1, pipeline2]) }
+ include_examples '#all_projects'
+ end
end
describe '#share_with_group_lock with subgroups' do
@@ -1379,36 +1387,14 @@ RSpec.describe Namespace do
describe '#pages_virtual_domain' do
let(:project) { create(:project, namespace: namespace) }
- context 'when there are pages deployed for the project' do
- context 'but pages metadata is not migrated' do
- before do
- generic_commit_status = create(:generic_commit_status, :success, stage: 'deploy', name: 'pages:deploy')
- generic_commit_status.update!(project: project)
- project.pages_metadatum.destroy!
- end
-
- it 'migrates pages metadata and returns the virual domain' do
- virtual_domain = namespace.pages_virtual_domain
+ it 'returns the virual domain' do
+ project.mark_pages_as_deployed
+ project.update_pages_deployment!(create(:pages_deployment, project: project))
- expect(project.reload.pages_metadatum.deployed).to eq(true)
+ virtual_domain = namespace.pages_virtual_domain
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- end
- end
-
- context 'and pages metadata is migrated' do
- before do
- project.mark_pages_as_deployed
- end
-
- it 'returns the virual domain' do
- virtual_domain = namespace.pages_virtual_domain
-
- expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- expect(virtual_domain.lookup_paths).not_to be_empty
- end
- end
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths).not_to be_empty
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 4eabc266b40..d9f566f9383 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1522,4 +1522,16 @@ RSpec.describe Note do
it { is_expected.to be_truthy }
end
end
+
+ describe '#attachment' do
+ it 'is cleaned up correctly when project is destroyed' do
+ note = create(:note_on_issue, :with_attachment)
+
+ attachment = note.attachment
+
+ note.project.destroy!
+
+ expect(attachment).not_to be_exist
+ end
+ end
end
diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding_progress_spec.rb
index 779312c9fa0..deac8d29196 100644
--- a/spec/models/onboarding_progress_spec.rb
+++ b/spec/models/onboarding_progress_spec.rb
@@ -211,4 +211,26 @@ RSpec.describe OnboardingProgress do
it { is_expected.to eq(:subscription_created_at) }
end
+
+ describe '#number_of_completed_actions' do
+ subject { build(:onboarding_progress, actions.map { |x| { x => Time.current } }.inject(:merge)).number_of_completed_actions }
+
+ context '0 completed actions' do
+ let(:actions) { [:created_at, :updated_at] }
+
+ it { is_expected.to eq(0) }
+ end
+
+ context '1 completed action' do
+ let(:actions) { [:created_at, :subscription_created_at] }
+
+ it { is_expected.to eq(1) }
+ end
+
+ context '2 completed actions' do
+ let(:actions) { [:subscription_created_at, :git_write_at] }
+
+ it { is_expected.to eq(2) }
+ end
+ end
end
diff --git a/spec/models/operations/feature_flag_scope_spec.rb b/spec/models/operations/feature_flag_scope_spec.rb
index 29d338d8b29..dc83789fade 100644
--- a/spec/models/operations/feature_flag_scope_spec.rb
+++ b/spec/models/operations/feature_flag_scope_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe Operations::FeatureFlagScope do
end
context 'when environment scope of a default scope is updated' do
- let!(:feature_flag) { create(:operations_feature_flag) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag) }
let!(:scope_default) { feature_flag.default_scope }
it 'keeps default scope intact' do
@@ -41,7 +41,7 @@ RSpec.describe Operations::FeatureFlagScope do
end
context 'when a default scope is destroyed' do
- let!(:feature_flag) { create(:operations_feature_flag) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag) }
let!(:scope_default) { feature_flag.default_scope }
it 'prevents from destroying the default scope' do
diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb
index d5b3c7a8582..55682e12642 100644
--- a/spec/models/operations/feature_flag_spec.rb
+++ b/spec/models/operations/feature_flag_spec.rb
@@ -181,7 +181,7 @@ RSpec.describe Operations::FeatureFlag do
end
context 'when the feature flag is active and all scopes are inactive' do
- let!(:feature_flag) { create(:operations_feature_flag, active: true) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: true) }
it 'returns the flag' do
feature_flag.default_scope.update!(active: false)
@@ -199,7 +199,7 @@ RSpec.describe Operations::FeatureFlag do
end
context 'when the feature flag is inactive and all scopes are active' do
- let!(:feature_flag) { create(:operations_feature_flag, active: false) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: false) }
it 'does not return the flag' do
feature_flag.default_scope.update!(active: true)
@@ -221,7 +221,7 @@ RSpec.describe Operations::FeatureFlag do
end
context 'when the feature flag is active and all scopes are inactive' do
- let!(:feature_flag) { create(:operations_feature_flag, active: true) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: true) }
it 'does not return the flag' do
feature_flag.default_scope.update!(active: false)
@@ -239,7 +239,7 @@ RSpec.describe Operations::FeatureFlag do
end
context 'when the feature flag is inactive and all scopes are active' do
- let!(:feature_flag) { create(:operations_feature_flag, active: false) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, active: false) }
it 'returns the flag' do
feature_flag.default_scope.update!(active: true)
diff --git a/spec/models/packages/debian/file_entry_spec.rb b/spec/models/packages/debian/file_entry_spec.rb
index 7aa16bc0cce..e981adf69bc 100644
--- a/spec/models/packages/debian/file_entry_spec.rb
+++ b/spec/models/packages/debian/file_entry_spec.rb
@@ -7,11 +7,11 @@ RSpec.describe Packages::Debian::FileEntry, type: :model do
let(:filename) { 'sample_1.2.3~alpha2.dsc' }
let(:size) { 671 }
- let(:md5sum) { '3b0817804f669e16cdefac583ad88f0e' }
+ let(:md5sum) { package_file.file_md5 }
let(:section) { 'libs' }
let(:priority) { 'optional' }
- let(:sha1sum) { '32ecbd674f0bfd310df68484d87752490685a8d6' }
- let(:sha256sum) { '844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba' }
+ let(:sha1sum) { package_file.file_sha1 }
+ let(:sha256sum) { package_file.file_sha256 }
let(:file_entry) do
described_class.new(
@@ -42,7 +42,7 @@ RSpec.describe Packages::Debian::FileEntry, type: :model do
describe '#md5sum' do
it { is_expected.to validate_presence_of(:md5sum) }
- it { is_expected.not_to allow_value('12345678901234567890123456789012').for(:md5sum).with_message('mismatch for sample_1.2.3~alpha2.dsc: 3b0817804f669e16cdefac583ad88f0e != 12345678901234567890123456789012') }
+ it { is_expected.not_to allow_value('12345678901234567890123456789012').for(:md5sum).with_message("mismatch for sample_1.2.3~alpha2.dsc: #{package_file.file_md5} != 12345678901234567890123456789012") }
end
describe '#section' do
@@ -55,12 +55,12 @@ RSpec.describe Packages::Debian::FileEntry, type: :model do
describe '#sha1sum' do
it { is_expected.to validate_presence_of(:sha1sum) }
- it { is_expected.not_to allow_value('1234567890123456789012345678901234567890').for(:sha1sum).with_message('mismatch for sample_1.2.3~alpha2.dsc: 32ecbd674f0bfd310df68484d87752490685a8d6 != 1234567890123456789012345678901234567890') }
+ it { is_expected.not_to allow_value('1234567890123456789012345678901234567890').for(:sha1sum).with_message("mismatch for sample_1.2.3~alpha2.dsc: #{package_file.file_sha1} != 1234567890123456789012345678901234567890") }
end
describe '#sha256sum' do
it { is_expected.to validate_presence_of(:sha256sum) }
- it { is_expected.not_to allow_value('1234567890123456789012345678901234567890123456789012345678901234').for(:sha256sum).with_message('mismatch for sample_1.2.3~alpha2.dsc: 844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba != 1234567890123456789012345678901234567890123456789012345678901234') }
+ it { is_expected.not_to allow_value('1234567890123456789012345678901234567890123456789012345678901234').for(:sha256sum).with_message("mismatch for sample_1.2.3~alpha2.dsc: #{package_file.file_sha256} != 1234567890123456789012345678901234567890123456789012345678901234") }
end
describe '#package_file' do
diff --git a/spec/models/packages/debian/group_distribution_key_spec.rb b/spec/models/packages/debian/group_distribution_key_spec.rb
new file mode 100644
index 00000000000..9ba163012b0
--- /dev/null
+++ b/spec/models/packages/debian/group_distribution_key_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::GroupDistributionKey do
+ it_behaves_like 'Debian Distribution Key', :group
+end
diff --git a/spec/models/packages/debian/project_distribution_key_spec.rb b/spec/models/packages/debian/project_distribution_key_spec.rb
new file mode 100644
index 00000000000..3dd723423f1
--- /dev/null
+++ b/spec/models/packages/debian/project_distribution_key_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::ProjectDistributionKey do
+ it_behaves_like 'Debian Distribution Key', :project
+end
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index f8ddd59ddc8..7f2f22c815c 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -116,6 +116,22 @@ RSpec.describe Packages::PackageFile, type: :model do
end
end
+ describe '.for_helm_with_channel' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:non_helm_package) { create(:nuget_package, project: project, package_type: :nuget) }
+ let_it_be(:helm_package1) { create(:helm_package, project: project, package_type: :helm) }
+ let_it_be(:helm_package2) { create(:helm_package, project: project, package_type: :helm) }
+ let_it_be(:channel) { 'some-channel' }
+
+ let_it_be(:non_helm_file) { create(:package_file, :nuget, package: non_helm_package) }
+ let_it_be(:helm_file1) { create(:helm_package_file, package: helm_package1) }
+ let_it_be(:helm_file2) { create(:helm_package_file, package: helm_package2, channel: channel) }
+
+ it 'returns the matching file only for Helm packages' do
+ expect(described_class.for_helm_with_channel(project, channel)).to contain_exactly(helm_file2)
+ end
+ end
+
describe '#update_file_store callback' do
let_it_be(:package_file) { build(:package_file, :nuget, size: nil) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 52ef61e3d44..1e44327c089 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -404,7 +404,8 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value(nil).for(:version) }
it { is_expected.not_to allow_value('').for(:version) }
it { is_expected.to allow_value('v1.2.3').for(:version) }
- it { is_expected.not_to allow_value('1.2.3').for(:version) }
+ it { is_expected.to allow_value('1.2.3').for(:version) }
+ it { is_expected.not_to allow_value('v1.2').for(:version) }
end
it_behaves_like 'validating version to be SemVer compliant for', :npm_package
@@ -729,6 +730,38 @@ RSpec.describe Packages::Package, type: :model do
end
end
+ context 'sorting' do
+ let_it_be(:project) { create(:project, name: 'aaa' ) }
+ let_it_be(:project2) { create(:project, name: 'bbb' ) }
+ let_it_be(:package1) { create(:package, project: project ) }
+ let_it_be(:package2) { create(:package, project: project2 ) }
+ let_it_be(:package3) { create(:package, project: project2 ) }
+ let_it_be(:package4) { create(:package, project: project ) }
+
+ it 'orders packages by their projects name ascending' do
+ expect(Packages::Package.order_project_name).to eq([package1, package4, package2, package3])
+ end
+
+ it 'orders packages by their projects name descending' do
+ expect(Packages::Package.order_project_name_desc).to eq([package2, package3, package1, package4])
+ end
+
+ shared_examples 'order_project_path scope' do
+ it 'orders packages by their projects path asc, then package id asc' do
+ expect(Packages::Package.order_project_path).to eq([package1, package4, package2, package3])
+ end
+ end
+
+ shared_examples 'order_project_path_desc scope' do
+ it 'orders packages by their projects path desc, then package id desc' do
+ expect(Packages::Package.order_project_path_desc).to eq([package3, package2, package4, package1])
+ end
+ end
+
+ it_behaves_like 'order_project_path scope'
+ it_behaves_like 'order_project_path_desc scope'
+ end
+
describe '.order_by_package_file' do
let_it_be(:project) { create(:project) }
let_it_be(:package1) { create(:maven_package, project: project) }
@@ -743,6 +776,33 @@ RSpec.describe Packages::Package, type: :model do
end
end
+ describe '.keyset_pagination_order' do
+ let(:join_class) { nil }
+ let(:column_name) { nil }
+ let(:direction) { nil }
+
+ subject { described_class.keyset_pagination_order(join_class: join_class, column_name: column_name, direction: direction) }
+
+ it { expect { subject }.to raise_error(NoMethodError) }
+
+ context 'with valid params' do
+ let(:join_class) { Project }
+ let(:column_name) { :name }
+
+ context 'ascending direction' do
+ let(:direction) { :asc }
+
+ it { is_expected.to eq('projects.name asc NULLS LAST, "packages_packages"."id" ASC') }
+ end
+
+ context 'descending direction' do
+ let(:direction) { :desc }
+
+ it { is_expected.to eq('projects.name desc NULLS FIRST, "packages_packages"."id" DESC') }
+ end
+ end
+ end
+
describe '#versions' do
let_it_be(:project) { create(:project) }
let_it_be(:package) { create(:maven_package, project: project) }
@@ -838,6 +898,26 @@ RSpec.describe Packages::Package, type: :model do
end
end
+ describe '#infrastructure_package?' do
+ let(:package) { create(:package) }
+
+ subject { package.infrastructure_package? }
+
+ it { is_expected.to eq(false) }
+
+ context 'with generic package' do
+ let(:package) { create(:generic_package) }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with terraform module package' do
+ let(:package) { create(:terraform_module_package) }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
describe 'plan_limits' do
Packages::Package.package_types.keys.without('composer').each do |pt|
plan_limit_name = if pt == 'generic'
diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb
index 735f2225c21..2d7ee8ba3be 100644
--- a/spec/models/pages/lookup_path_spec.rb
+++ b/spec/models/pages/lookup_path_spec.rb
@@ -47,14 +47,7 @@ RSpec.describe Pages::LookupPath do
describe '#source' do
let(:source) { lookup_path.source }
- it 'uses disk storage', :aggregate_failures do
- expect(source[:type]).to eq('file')
- expect(source[:path]).to eq(project.full_path + "/public/")
- end
-
- it 'return nil when local storage is disabled and there is no deployment' do
- allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
-
+ it 'returns nil' do
expect(source).to eq(nil)
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 1d5369e608e..7b997f0d4e1 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -12,6 +12,15 @@ RSpec.describe PagesDomain do
it { is_expected.to have_many(:serverless_domain_clusters) }
end
+ describe '.for_project' do
+ it 'returns domains assigned to project' do
+ domain = create(:pages_domain, :with_project)
+ create(:pages_domain) # unrelated domain
+
+ expect(described_class.for_project(domain.project)).to eq([domain])
+ end
+ end
+
describe 'validate domain' do
subject(:pages_domain) { build(:pages_domain, domain: domain) }
@@ -655,25 +664,16 @@ RSpec.describe PagesDomain do
end
end
- context 'when there are pages deployed for the project' do
- before do
- generic_commit_status = create(:generic_commit_status, :success, stage: 'deploy', name: 'pages:deploy')
- generic_commit_status.update!(project: project)
- project.pages_metadatum.destroy!
- project.reload
- end
+ it 'returns the virual domain when there are pages deployed for the project' do
+ project.mark_pages_as_deployed
+ project.update_pages_deployment!(create(:pages_deployment, project: project))
- it 'returns the virual domain' do
- expect(Pages::VirtualDomain).to receive(:new).with([project], domain: pages_domain).and_call_original
+ expect(Pages::VirtualDomain).to receive(:new).with([project], domain: pages_domain).and_call_original
- expect(pages_domain.pages_virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
- end
+ virtual_domain = pages_domain.pages_virtual_domain
- it 'migrates project pages metadata' do
- expect { pages_domain.pages_virtual_domain }.to change {
- project.reload.pages_metadatum&.deployed
- }.from(nil).to(true)
- end
+ expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain)
+ expect(virtual_domain.lookup_paths).not_to be_empty
end
end
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
index b8c723b3847..cf8e30023eb 100644
--- a/spec/models/plan_limits_spec.rb
+++ b/spec/models/plan_limits_spec.rb
@@ -211,6 +211,7 @@ RSpec.describe PlanLimits do
storage_size_limit
daily_invites
web_hook_calls
+ ci_daily_pipeline_schedule_triggers
] + disabled_max_artifact_size_columns
end
diff --git a/spec/models/postgresql/replication_slot_spec.rb b/spec/models/postgresql/replication_slot_spec.rb
index 02a4d783b84..4bad8a3f0c0 100644
--- a/spec/models/postgresql/replication_slot_spec.rb
+++ b/spec/models/postgresql/replication_slot_spec.rb
@@ -24,6 +24,10 @@ RSpec.describe Postgresql::ReplicationSlot do
expect(described_class).to receive(:in_use?).and_return(true)
end
+ it 'does not raise an exception' do
+ expect { described_class.lag_too_great? }.not_to raise_error
+ end
+
it 'returns true when replication lag is too great' do
expect(described_class)
.to receive(:pluck)
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 406485d8cc8..c206ba27ec1 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -21,6 +21,12 @@ RSpec.describe ProjectCiCdSetting do
end
end
+ describe '#job_token_scope_enabled' do
+ it 'is true by default' do
+ expect(described_class.new.job_token_scope_enabled).to be_truthy
+ end
+ end
+
describe '#default_git_depth' do
let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
diff --git a/spec/models/project_feature_usage_spec.rb b/spec/models/project_feature_usage_spec.rb
index 4baa59535e4..6ef407432b0 100644
--- a/spec/models/project_feature_usage_spec.rb
+++ b/spec/models/project_feature_usage_spec.rb
@@ -126,4 +126,54 @@ RSpec.describe ProjectFeatureUsage, type: :model do
end
end
end
+
+ context 'ProjectFeatureUsage with DB Load Balancing', :request_store do
+ include_context 'clear DB Load Balancing configuration'
+
+ describe '#log_jira_dvcs_integration_usage' do
+ let!(:project) { create(:project) }
+
+ subject { project.feature_usage }
+
+ context 'database load balancing is configured' do
+ before do
+ # Do not pollute AR for other tests, but rather simulate effect of configure_proxy.
+ allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+ ::Gitlab::Database::LoadBalancing.configure_proxy
+ allow(ActiveRecord::Base).to receive(:connection).and_return(::Gitlab::Database::LoadBalancing.proxy)
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+
+ it 'logs Jira DVCS Cloud last sync' do
+ freeze_time do
+ subject.log_jira_dvcs_integration_usage
+
+ expect(subject.jira_dvcs_server_last_sync_at).to be_nil
+ expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
+ end
+ end
+
+ it 'does not stick to primary' do
+ expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_performed_write
+ expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_using_primary
+
+ subject.log_jira_dvcs_integration_usage
+
+ expect(::Gitlab::Database::LoadBalancing::Session.current).to be_performed_write
+ expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_using_primary
+ end
+ end
+
+ context 'database load balancing is not cofigured' do
+ it 'logs Jira DVCS Cloud last sync' do
+ freeze_time do
+ subject.log_jira_dvcs_integration_usage
+
+ expect(subject.jira_dvcs_server_last_sync_at).to be_nil
+ expect(subject.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.current)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_repository_storage_move_spec.rb b/spec/models/project_repository_storage_move_spec.rb
deleted file mode 100644
index eb193a44680..00000000000
--- a/spec/models/project_repository_storage_move_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProjectRepositoryStorageMove, type: :model do
- let_it_be_with_refind(:project) { create(:project) }
-
- it_behaves_like 'handles repository moves' do
- let(:container) { project }
- let(:repository_storage_factory_key) { :project_repository_storage_move }
- let(:error_key) { :project }
- let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker }
- end
-
- describe 'state transitions' do
- let(:storage) { 'test_second_storage' }
-
- before do
- stub_storage_settings(storage => { 'path' => 'tmp/tests/extra_storage' })
- end
-
- context 'when started' do
- subject(:storage_move) { create(:project_repository_storage_move, :started, container: project, destination_storage_name: storage) }
-
- context 'and transits to replicated' do
- it 'sets the repository storage and marks the container as writable' do
- storage_move.finish_replication!
-
- expect(project.repository_storage).to eq(storage)
- expect(project).not_to be_repository_read_only
- end
- end
- end
- end
-end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
deleted file mode 100644
index 42368c31ba0..00000000000
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# HipchatService is partially removed and it will be remove completely
-# after the deletion of all the database records.
-# https://gitlab.com/gitlab-org/gitlab/-/issues/27954
-RSpec.describe HipchatService do
- let_it_be(:project) { create(:project) }
-
- subject(:service) { described_class.new(project: project) }
-
- it { is_expected.to be_valid }
-
- describe '#to_param' do
- subject { service.to_param }
-
- it { is_expected.to eq('hipchat') }
- end
-
- describe '#supported_events' do
- subject { service.supported_events }
-
- it { is_expected.to be_empty }
- end
-
- describe '#save' do
- it 'prevents records from being created or updated' do
- expect(service.save).to be_falsey
-
- expect(service.errors.full_messages).to include(
- 'HipChat endpoint is deprecated and should not be created or modified.'
- )
- end
- end
-end
diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb
deleted file mode 100644
index af1944ea77d..00000000000
--- a/spec/models/project_services/mattermost_service_spec.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MattermostService do
- it_behaves_like "slack or mattermost notifications", "Mattermost"
-end
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index 37a6d49ff74..a2025388fab 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -323,9 +323,9 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
end
describe '#prometheus_available?' do
- context 'clusters with installed prometheus' do
+ context 'clusters with enabled prometheus' do
before do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
end
context 'cluster belongs to project' do
@@ -340,7 +340,7 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
let_it_be(:group) { create(:group) }
let(:project) { create(:prometheus_project, group: group) }
- let(:cluster) { create(:cluster_for_group, :with_installed_helm, groups: [group]) }
+ let(:cluster) { create(:cluster_for_group, groups: [group]) }
it 'returns true' do
expect(service.prometheus_available?).to be(true)
@@ -349,8 +349,8 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
it 'avoids N+1 queries' do
service
5.times do |i|
- other_cluster = create(:cluster_for_group, :with_installed_helm, groups: [group], environment_scope: i)
- create(:clusters_applications_prometheus, :installing, cluster: other_cluster)
+ other_cluster = create(:cluster_for_group, groups: [group], environment_scope: i)
+ create(:clusters_integrations_prometheus, cluster: other_cluster)
end
expect { service.prometheus_available? }.not_to exceed_query_limit(1)
end
@@ -365,18 +365,9 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
end
end
- context 'clusters with updated prometheus' do
- let!(:cluster) { create(:cluster, projects: [project]) }
- let!(:prometheus) { create(:clusters_applications_prometheus, :updated, cluster: cluster) }
-
- it 'returns true' do
- expect(service.prometheus_available?).to be(true)
- end
- end
-
- context 'clusters without prometheus installed' do
+ context 'clusters with prometheus disabled' do
let(:cluster) { create(:cluster, projects: [project]) }
- let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_integrations_prometheus, :disabled, cluster: cluster) }
it 'returns false' do
expect(service.prometheus_available?).to be(false)
@@ -491,13 +482,13 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
expect(service.editable?).to be(true)
end
- context 'when cluster exists with prometheus installed' do
+ context 'when cluster exists with prometheus enabled' do
let(:cluster) { create(:cluster, projects: [project]) }
before do
service.update!(manual_configuration: false)
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
end
it 'remains editable' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c57c2792f87..78e32571d7d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:project_members).dependent(:delete_all) }
it { is_expected.to have_many(:users).through(:project_members) }
it { is_expected.to have_many(:requesters).dependent(:delete_all) }
- it { is_expected.to have_many(:notes) }
+ it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:snippets).class_name('ProjectSnippet') }
it { is_expected.to have_many(:deploy_keys_projects) }
it { is_expected.to have_many(:deploy_keys) }
@@ -43,31 +43,31 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_one(:webex_teams_service) }
it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
- it { is_expected.to have_one(:asana_service) }
+ it { is_expected.to have_one(:asana_integration) }
it { is_expected.to have_many(:boards) }
- it { is_expected.to have_one(:campfire_service) }
- it { is_expected.to have_one(:datadog_service) }
- it { is_expected.to have_one(:discord_service) }
- it { is_expected.to have_one(:drone_ci_service) }
+ it { is_expected.to have_one(:campfire_integration) }
+ it { is_expected.to have_one(:datadog_integration) }
+ it { is_expected.to have_one(:discord_integration) }
+ it { is_expected.to have_one(:drone_ci_integration) }
it { is_expected.to have_one(:emails_on_push_service) }
it { is_expected.to have_one(:pipelines_email_service) }
it { is_expected.to have_one(:irker_service) }
it { is_expected.to have_one(:pivotaltracker_service) }
it { is_expected.to have_one(:flowdock_service) }
- it { is_expected.to have_one(:assembla_service) }
+ it { is_expected.to have_one(:assembla_integration) }
it { is_expected.to have_one(:slack_slash_commands_service) }
it { is_expected.to have_one(:mattermost_slash_commands_service) }
- it { is_expected.to have_one(:buildkite_service) }
- it { is_expected.to have_one(:bamboo_service) }
+ it { is_expected.to have_one(:buildkite_integration) }
+ it { is_expected.to have_one(:bamboo_integration) }
it { is_expected.to have_one(:teamcity_service) }
it { is_expected.to have_one(:jira_service) }
it { is_expected.to have_one(:redmine_service) }
it { is_expected.to have_one(:youtrack_service) }
- it { is_expected.to have_one(:custom_issue_tracker_service) }
- it { is_expected.to have_one(:bugzilla_service) }
+ it { is_expected.to have_one(:custom_issue_tracker_integration) }
+ it { is_expected.to have_one(:bugzilla_integration) }
it { is_expected.to have_one(:ewm_service) }
it { is_expected.to have_one(:external_wiki_service) }
- it { is_expected.to have_one(:confluence_service) }
+ it { is_expected.to have_one(:confluence_integration) }
it { is_expected.to have_one(:project_feature) }
it { is_expected.to have_one(:project_repository) }
it { is_expected.to have_one(:container_expiration_policy) }
@@ -472,6 +472,23 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#merge_requests_author_approval' do
+ where(:attribute_value, :return_value) do
+ true | true
+ false | false
+ nil | false
+ end
+
+ with_them do
+ let(:project) { create(:project, merge_requests_author_approval: attribute_value) }
+
+ it 'returns expected value' do
+ expect(project.merge_requests_author_approval).to eq(return_value)
+ expect(project.merge_requests_author_approval?).to eq(return_value)
+ end
+ end
+ end
+
describe '#all_pipelines' do
let_it_be(:project) { create(:project) }
@@ -636,6 +653,16 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
it { is_expected.to delegate_method(:allow_editing_commit_messages?).to(:project_setting) }
+ it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) }
+ it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
+
+ context 'when read_container_registry_access_level is disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ it { is_expected.not_to delegate_method(:container_registry_enabled?).to(:project_feature) }
+ end
end
describe 'reference methods' do
@@ -967,6 +994,39 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#open_issues_count', :aggregate_failures do
+ let(:project) { build(:project) }
+
+ it 'provides the issue count' do
+ expect(project.open_issues_count).to eq 0
+ end
+
+ it 'invokes the count service with current_user' do
+ user = build(:user)
+ count_service = instance_double(Projects::OpenIssuesCountService)
+ expect(Projects::OpenIssuesCountService).to receive(:new).with(project, user).and_return(count_service)
+ expect(count_service).to receive(:count)
+
+ project.open_issues_count(user)
+ end
+
+ it 'invokes the count service with no current_user' do
+ count_service = instance_double(Projects::OpenIssuesCountService)
+ expect(Projects::OpenIssuesCountService).to receive(:new).with(project, nil).and_return(count_service)
+ expect(count_service).to receive(:count)
+
+ project.open_issues_count
+ end
+ end
+
+ describe '#open_merge_requests_count' do
+ it 'provides the merge request count' do
+ project = build(:project)
+
+ expect(project.open_merge_requests_count).to eq 0
+ end
+ end
+
describe '#issue_exists?' do
let_it_be(:project) { create(:project) }
@@ -1086,7 +1146,7 @@ RSpec.describe Project, factory_default: :keep do
project = create(:redmine_project)
expect(project).to receive(:integrations).once.and_call_original
- 2.times { expect(project.external_issue_tracker).to be_a_kind_of(RedmineService) }
+ 2.times { expect(project.external_issue_tracker).to be_a_kind_of(Integrations::Redmine) }
end
end
@@ -1158,7 +1218,7 @@ RSpec.describe Project, factory_default: :keep do
it 'returns an active external wiki' do
create(:service, project: project, type: 'ExternalWikiService', active: true)
- is_expected.to be_kind_of(ExternalWikiService)
+ is_expected.to be_kind_of(Integrations::ExternalWiki)
end
it 'does not return an inactive external wiki' do
@@ -1584,19 +1644,20 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '.find_by_service_desk_project_key' do
- it 'returns the correct project' do
+ describe '.with_service_desk_key' do
+ it 'returns projects with given key' do
project1 = create(:project)
project2 = create(:project)
create(:service_desk_setting, project: project1, project_key: 'key1')
- create(:service_desk_setting, project: project2, project_key: 'key2')
+ create(:service_desk_setting, project: project2, project_key: 'key1')
+ create(:service_desk_setting, project_key: 'key2')
+ create(:service_desk_setting)
- expect(Project.find_by_service_desk_project_key('key1')).to eq(project1)
- expect(Project.find_by_service_desk_project_key('key2')).to eq(project2)
+ expect(Project.with_service_desk_key('key1')).to contain_exactly(project1, project2)
end
- it 'returns nil if there is no project with the key' do
- expect(Project.find_by_service_desk_project_key('some_key')).to be_nil
+ it 'returns empty if there is no project with the key' do
+ expect(Project.with_service_desk_key('key1')).to be_empty
end
end
@@ -1632,112 +1693,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#any_active_runners?' do
- subject { project.any_active_runners? }
-
- context 'shared runners' do
- let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
- let(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
- let(:shared_runner) { create(:ci_runner, :instance) }
-
- context 'for shared runners disabled' do
- let(:shared_runners_enabled) { false }
-
- it 'has no runners available' do
- is_expected.to be_falsey
- end
-
- it 'has a specific runner' do
- specific_runner
-
- is_expected.to be_truthy
- end
-
- it 'has a shared runner, but they are prohibited to use' do
- shared_runner
-
- is_expected.to be_falsey
- end
-
- it 'checks the presence of specific runner' do
- specific_runner
-
- expect(project.any_active_runners? { |runner| runner == specific_runner }).to be_truthy
- end
-
- it 'returns false if match cannot be found' do
- specific_runner
-
- expect(project.any_active_runners? { false }).to be_falsey
- end
- end
-
- context 'for shared runners enabled' do
- let(:shared_runners_enabled) { true }
-
- it 'has a shared runner' do
- shared_runner
-
- is_expected.to be_truthy
- end
-
- it 'checks the presence of shared runner' do
- shared_runner
-
- expect(project.any_active_runners? { |runner| runner == shared_runner }).to be_truthy
- end
-
- it 'returns false if match cannot be found' do
- shared_runner
-
- expect(project.any_active_runners? { false }).to be_falsey
- end
- end
- end
-
- context 'group runners' do
- let(:project) { create(:project, group_runners_enabled: group_runners_enabled) }
- let(:group) { create(:group, projects: [project]) }
- let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
-
- context 'for group runners disabled' do
- let(:group_runners_enabled) { false }
-
- it 'has no runners available' do
- is_expected.to be_falsey
- end
-
- it 'has a group runner, but they are prohibited to use' do
- group_runner
-
- is_expected.to be_falsey
- end
- end
-
- context 'for group runners enabled' do
- let(:group_runners_enabled) { true }
-
- it 'has a group runner' do
- group_runner
-
- is_expected.to be_truthy
- end
-
- it 'checks the presence of group runner' do
- group_runner
-
- expect(project.any_active_runners? { |runner| runner == group_runner }).to be_truthy
- end
-
- it 'returns false if match cannot be found' do
- group_runner
-
- expect(project.any_active_runners? { false }).to be_falsey
- end
- end
- end
- end
-
describe '#any_online_runners?' do
subject { project.any_online_runners? }
@@ -1858,13 +1813,13 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#shared_runners' do
- let!(:runner) { create(:ci_runner, :instance) }
+ shared_examples 'shared_runners' do
+ let_it_be(:runner) { create(:ci_runner, :instance) }
subject { project.shared_runners }
context 'when shared runners are enabled for project' do
- let!(:project) { create(:project, shared_runners_enabled: true) }
+ let(:project) { build_stubbed(:project, shared_runners_enabled: true) }
it "returns a list of shared runners" do
is_expected.to eq([runner])
@@ -1872,7 +1827,7 @@ RSpec.describe Project, factory_default: :keep do
end
context 'when shared runners are disabled for project' do
- let!(:project) { create(:project, shared_runners_enabled: false) }
+ let(:project) { build_stubbed(:project, shared_runners_enabled: false) }
it "returns a empty list" do
is_expected.to be_empty
@@ -1880,6 +1835,16 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#shared_runners' do
+ it_behaves_like 'shared_runners'
+ end
+
+ describe '#available_shared_runners' do
+ it_behaves_like 'shared_runners' do
+ subject { project.available_shared_runners }
+ end
+ end
+
describe '#visibility_level' do
let(:project) { build(:project) }
@@ -2238,13 +2203,13 @@ RSpec.describe Project, factory_default: :keep do
end
context 'with projects on legacy storage' do
- let(:project) { create(:project, :repository, :legacy_storage) }
+ let(:project) { create(:project, :legacy_storage) }
it_behaves_like 'tracks storage location'
end
context 'with projects on hashed storage' do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project) }
it_behaves_like 'tracks storage location'
end
@@ -2363,35 +2328,55 @@ RSpec.describe Project, factory_default: :keep do
it 'updates project_feature', :aggregate_failures do
# Simulate an existing project that has container_registry enabled
project.update_column(:container_registry_enabled, true)
- project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
-
- expect(project.container_registry_enabled).to eq(true)
- expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::ENABLED)
project.update!(container_registry_enabled: false)
- expect(project.container_registry_enabled).to eq(false)
+ expect(project.read_attribute(:container_registry_enabled)).to eq(false)
expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED)
project.update!(container_registry_enabled: true)
- expect(project.container_registry_enabled).to eq(true)
+ expect(project.read_attribute(:container_registry_enabled)).to eq(true)
expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::ENABLED)
end
it 'rollsback both projects and project_features row in case of error', :aggregate_failures do
project.update_column(:container_registry_enabled, true)
- project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
-
- expect(project.container_registry_enabled).to eq(true)
- expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::ENABLED)
allow(project).to receive(:valid?).and_return(false)
expect { project.update!(container_registry_enabled: false) }.to raise_error(ActiveRecord::RecordInvalid)
- expect(project.reload.container_registry_enabled).to eq(true)
- expect(project.project_feature.reload.container_registry_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.reload.read_attribute(:container_registry_enabled)).to eq(true)
+ expect(project.project_feature.reload.container_registry_access_level).to eq(ProjectFeature::ENABLED)
+ end
+ end
+
+ describe '#container_registry_enabled' do
+ let_it_be_with_reload(:project) { create(:project) }
+
+ it 'delegates to project_feature', :aggregate_failures do
+ project.update_column(:container_registry_enabled, true)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
+
+ expect(project.container_registry_enabled).to eq(false)
+ expect(project.container_registry_enabled?).to eq(false)
+ end
+
+ context 'with read_container_registry_access_level disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ it 'reads project.container_registry_enabled' do
+ project.update_column(:container_registry_enabled, true)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
+
+ expect(project.container_registry_enabled).to eq(true)
+ expect(project.container_registry_enabled?).to eq(true)
+ end
end
end
@@ -2932,6 +2917,16 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#mark_primary_write_location' do
+ let(:project) { create(:project) }
+
+ it 'marks the location with project ID' do
+ expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:mark_primary_write_location).with(:project, project.id)
+
+ project.mark_primary_write_location
+ end
+ end
+
describe '#mark_stuck_remote_mirrors_as_failed!' do
it 'fails stuck remote mirrors' do
project = create(:project, :repository, :remote_mirror)
@@ -4414,6 +4409,18 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ context 'with export' do
+ let(:project) { create(:project, :with_export) }
+
+ it '#export_file_exists? returns true' do
+ expect(project.export_file_exists?).to be true
+ end
+
+ it '#export_archive_exists? returns false' do
+ expect(project.export_archive_exists?).to be true
+ end
+ end
+
describe '#forks_count' do
it 'returns the number of forks' do
project = build(:project)
@@ -4692,7 +4699,6 @@ RSpec.describe Project, factory_default: :keep do
specify do
expect(subject).to include
[
- { key: 'CI_PROJECT_CONFIG_PATH', value: Ci::Pipeline::DEFAULT_CONFIG_PATH, public: true, masked: false },
{ key: 'CI_CONFIG_PATH', value: Ci::Pipeline::DEFAULT_CONFIG_PATH, public: true, masked: false }
]
end
@@ -4705,7 +4711,6 @@ RSpec.describe Project, factory_default: :keep do
it do
expect(subject).to include
[
- { key: 'CI_PROJECT_CONFIG_PATH', value: 'random.yml', public: true, masked: false },
{ key: 'CI_CONFIG_PATH', value: 'random.yml', public: true, masked: false }
]
end
@@ -5328,7 +5333,7 @@ RSpec.describe Project, factory_default: :keep do
it 'executes services with the specified scope' do
data = 'any data'
- expect_next_found_instance_of(SlackService) do |instance|
+ expect_next_found_instance_of(Integrations::Slack) do |instance|
expect(instance).to receive(:async_execute).with(data).once
end
@@ -5336,7 +5341,7 @@ RSpec.describe Project, factory_default: :keep do
end
it 'does not execute services that don\'t match the specified scope' do
- expect(SlackService).not_to receive(:allocate).and_wrap_original do |method|
+ expect(Integrations::Slack).not_to receive(:allocate).and_wrap_original do |method|
method.call.tap do |instance|
expect(instance).not_to receive(:async_execute)
end
@@ -5379,7 +5384,7 @@ RSpec.describe Project, factory_default: :keep do
it { expect(project.has_active_services?).to be_falsey }
it 'returns true when a matching service exists' do
- create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project)
+ create(:custom_issue_tracker_integration, push_events: true, merge_requests_events: false, project: project)
expect(project.has_active_services?(:merge_request_hooks)).to be_falsey
expect(project.has_active_services?).to be_truthy
@@ -6307,14 +6312,16 @@ RSpec.describe Project, factory_default: :keep do
let_it_be(:project) { create(:project, group: create(:group, :public)) }
it 'returns a maximum of ten maintainers of the project in recent_sign_in descending order' do
- users = create_list(:user, 12, :with_sign_ins)
+ limit = 2
+ stub_const("Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT", limit)
+ users = create_list(:user, limit + 1, :with_sign_ins)
active_maintainers = users.map do |user|
create(:project_member, :maintainer, user: user, project: project)
end
active_maintainers_in_recent_sign_in_desc_order = project.members_and_requesters
.id_in(active_maintainers)
- .order_recent_sign_in.limit(10)
+ .order_recent_sign_in.limit(limit)
expect(project.access_request_approvers_to_be_notified).to eq(active_maintainers_in_recent_sign_in_desc_order)
end
@@ -6558,6 +6565,14 @@ RSpec.describe Project, factory_default: :keep do
expect(subject).to eq([lfs_object.oid])
end
end
+
+ it 'lfs_objects_projects associations are deleted along with project' do
+ expect { project.delete }.to change(LfsObjectsProject, :count).by(-2)
+ end
+
+ it 'lfs_objects associations are unchanged when the assicated project is removed' do
+ expect { project.delete }.not_to change(LfsObject, :count)
+ end
end
context 'when project has no associated LFS objects' do
@@ -6666,7 +6681,7 @@ RSpec.describe Project, factory_default: :keep do
context 'when project export is completed' do
before do
finish_job(project_export_job)
- allow(project).to receive(:export_file).and_return(double(ImportExportUploader, file: 'exists.zip'))
+ allow(project).to receive(:export_file_exists?).and_return(true)
end
it { expect(project.export_status).to eq :finished }
@@ -6677,7 +6692,7 @@ RSpec.describe Project, factory_default: :keep do
before do
finish_job(project_export_job)
- allow(project).to receive(:export_file).and_return(double(ImportExportUploader, file: 'exists.zip'))
+ allow(project).to receive(:export_file_exists?).and_return(true)
end
it { expect(project.export_status).to eq :regeneration_in_progress }
@@ -6973,7 +6988,7 @@ RSpec.describe Project, factory_default: :keep do
end
describe 'topics' do
- let_it_be(:project) { create(:project, tag_list: 'topic1, topic2, topic3') }
+ let_it_be(:project) { create(:project, topic_list: 'topic1, topic2, topic3') }
it 'topic_list returns correct string array' do
expect(project.topic_list).to match_array(%w[topic1 topic2 topic3])
@@ -6983,44 +6998,69 @@ RSpec.describe Project, factory_default: :keep do
expect(project.topics.first.class.name).to eq('ActsAsTaggableOn::Tag')
expect(project.topics.map(&:name)).to match_array(%w[topic1 topic2 topic3])
end
+ end
- context 'aliases' do
- it 'tag_list returns correct string array' do
- expect(project.tag_list).to match_array(%w[topic1 topic2 topic3])
+ shared_examples 'all_runners' do
+ let_it_be_with_refind(:project) { create(:project, group: create(:group)) }
+ let_it_be(:instance_runner) { create(:ci_runner, :instance) }
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [project.group]) }
+ let_it_be(:other_group_runner) { create(:ci_runner, :group) }
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let_it_be(:other_project_runner) { create(:ci_runner, :project) }
+
+ subject { project.all_runners }
+
+ context 'when shared runners are enabled for project' do
+ before do
+ project.update!(shared_runners_enabled: true)
end
- it 'tags returns correct tag records' do
- expect(project.tags.first.class.name).to eq('ActsAsTaggableOn::Tag')
- expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3])
+ it 'returns a list with all runners' do
+ is_expected.to match_array([instance_runner, group_runner, project_runner])
end
end
- context 'intermediate state during background migration' do
+ context 'when shared runners are disabled for project' do
before do
- project.taggings.first.update!(context: 'tags')
- project.instance_variable_set("@tag_list", nil)
- project.reload
+ project.update!(shared_runners_enabled: false)
end
- it 'tag_list returns string array including old and new topics' do
- expect(project.tag_list).to match_array(%w[topic1 topic2 topic3])
+ it 'returns a list without shared runners' do
+ is_expected.to match_array([group_runner, project_runner])
+ end
+ end
+
+ context 'when group runners are enabled for project' do
+ before do
+ project.update!(group_runners_enabled: true)
end
- it 'tags returns old and new tag records' do
- expect(project.tags.first.class.name).to eq('ActsAsTaggableOn::Tag')
- expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3])
- expect(project.taggings.map(&:context)).to match_array(%w[tags topics topics])
+ it 'returns a list with all runners' do
+ is_expected.to match_array([instance_runner, group_runner, project_runner])
end
+ end
- it 'update tag_list adds new topics and removes old topics' do
- project.update!(tag_list: 'topic1, topic2, topic3, topic4')
+ context 'when group runners are disabled for project' do
+ before do
+ project.update!(group_runners_enabled: false)
+ end
- expect(project.tags.map(&:name)).to match_array(%w[topic1 topic2 topic3 topic4])
- expect(project.taggings.map(&:context)).to match_array(%w[topics topics topics topics])
+ it 'returns a list without group runners' do
+ is_expected.to match_array([instance_runner, project_runner])
end
end
end
+ describe '#all_runners' do
+ it_behaves_like 'all_runners'
+ end
+
+ describe '#all_available_runners' do
+ it_behaves_like 'all_runners' do
+ subject { project.all_available_runners }
+ end
+ end
+
def finish_job(export_job)
export_job.start
export_job.finish
diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb
index b4dff4c33ff..a5441e2f47b 100644
--- a/spec/models/release_highlight_spec.rb
+++ b/spec/models/release_highlight_spec.rb
@@ -67,12 +67,12 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
expect(subject[:next_page]).to eq(2)
end
- it 'parses the body as markdown and returns html' do
- expect(subject[:items].first['body']).to match("<h2 id=\"bright-and-sunshinin-day\">bright and sunshinin’ day</h2>")
+ it 'parses the body as markdown and returns html, and links are target="_blank"' do
+ expect(subject[:items].first['body']).to match('<p data-sourcepos="1:1-1:62" dir="auto">bright and sunshinin\' <a href="https://en.wikipedia.org/wiki/Day" rel="nofollow noreferrer noopener" target="_blank">day</a></p>')
end
it 'logs an error if theres an error parsing markdown for an item, and skips it' do
- allow(Kramdown::Document).to receive(:new).and_raise
+ allow(Banzai).to receive(:render).and_raise
expect(Gitlab::ErrorTracking).to receive(:track_exception)
expect(subject[:items]).to be_empty
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7748846f6a5..b6f09babb4b 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1123,6 +1123,70 @@ RSpec.describe Repository do
end
end
+ describe '#fetch_as_mirror' do
+ let(:url) { "http://example.com" }
+
+ context 'when :fetch_remote_params is enabled' do
+ let(:remote_name) { "remote-name" }
+
+ before do
+ stub_feature_flags(fetch_remote_params: true)
+ end
+
+ it 'fetches the URL without creating a remote' do
+ expect(repository).not_to receive(:add_remote)
+ expect(repository)
+ .to receive(:fetch_remote)
+ .with(remote_name, url: url, forced: false, prune: true, refmap: :all_refs)
+ .and_return(nil)
+
+ repository.fetch_as_mirror(url, remote_name: remote_name)
+ end
+ end
+
+ context 'when :fetch_remote_params is disabled' do
+ before do
+ stub_feature_flags(fetch_remote_params: false)
+ end
+
+ shared_examples 'a fetch' do
+ it 'adds and fetches a remote' do
+ expect(repository)
+ .to receive(:add_remote)
+ .with(expected_remote, url, mirror_refmap: :all_refs)
+ .and_return(nil)
+ expect(repository)
+ .to receive(:fetch_remote)
+ .with(expected_remote, forced: false, prune: true)
+ .and_return(nil)
+
+ repository.fetch_as_mirror(url, remote_name: remote_name)
+ end
+ end
+
+ context 'with temporary remote' do
+ let(:remote_name) { nil }
+ let(:expected_remote_suffix) { "123456" }
+ let(:expected_remote) { "tmp-#{expected_remote_suffix}" }
+
+ before do
+ expect(repository)
+ .to receive(:async_remove_remote).with(expected_remote).and_return(nil)
+ allow(SecureRandom).to receive(:hex).and_return(expected_remote_suffix)
+ end
+
+ it_behaves_like 'a fetch'
+ end
+
+ context 'with remote name' do
+ let(:remote_name) { "foo" }
+ let(:expected_remote) { "foo" }
+
+ it_behaves_like 'a fetch'
+ end
+ end
+ end
+
describe '#fetch_ref' do
let(:broken_repository) { create(:project, :broken_storage).repository }
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index ca57a5d4087..8ccbd983ba1 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -31,6 +31,37 @@ RSpec.describe ServiceDeskSetting do
end
end
+ describe '.valid_project_key' do
+ # Creates two projects with same full path slug
+ # group1/test/one and group1/test-one will both have 'group-test-one' slug
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group, name: 'test') }
+ let_it_be(:project1) { create(:project, name: 'test-one', group: group) }
+ let_it_be(:project2) { create(:project, name: 'one', group: subgroup) }
+ let_it_be(:project_key) { 'key' }
+
+ before_all do
+ create(:service_desk_setting, project: project1, project_key: project_key)
+ end
+
+ context 'when project_key is unique for every project slug' do
+ it 'does not add error' do
+ settings = build(:service_desk_setting, project: project2, project_key: 'otherkey')
+
+ expect(settings).to be_valid
+ end
+ end
+
+ context 'when project with same slug and settings project_key exists' do
+ it 'adds error' do
+ settings = build(:service_desk_setting, project: project2, project_key: project_key)
+
+ expect(settings).to be_invalid
+ expect(settings.errors[:project_key].first).to eq('already in use for another service desk address.')
+ end
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
end
diff --git a/spec/models/snippet_repository_storage_move_spec.rb b/spec/models/snippet_repository_storage_move_spec.rb
deleted file mode 100644
index f5ad837fb36..00000000000
--- a/spec/models/snippet_repository_storage_move_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe SnippetRepositoryStorageMove, type: :model do
- it_behaves_like 'handles repository moves' do
- let_it_be_with_refind(:container) { create(:snippet) }
-
- let(:repository_storage_factory_key) { :snippet_repository_storage_move }
- let(:error_key) { :snippet }
- let(:repository_storage_worker) { Snippets::UpdateRepositoryStorageWorker }
- end
-end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 41991821922..06e9899c0bd 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -750,7 +750,7 @@ RSpec.describe Snippet do
end
it 'returns an empty array' do
- expect(subject).to eq []
+ expect(subject).to be_empty
end
end
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index c3432907112..bc042f7a639 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -64,25 +64,57 @@ RSpec.describe Timelog do
let_it_be(:subgroup_issue) { create(:issue, project: subgroup_project) }
let_it_be(:subgroup_merge_request) { create(:merge_request, source_project: subgroup_project) }
- let_it_be(:timelog) { create(:issue_timelog, spent_at: 65.days.ago) }
- let_it_be(:timelog1) { create(:issue_timelog, spent_at: 15.days.ago, issue: group_issue) }
- let_it_be(:timelog2) { create(:issue_timelog, spent_at: 5.days.ago, issue: subgroup_issue) }
- let_it_be(:timelog3) { create(:merge_request_timelog, spent_at: 65.days.ago) }
- let_it_be(:timelog4) { create(:merge_request_timelog, spent_at: 15.days.ago, merge_request: group_merge_request) }
- let_it_be(:timelog5) { create(:merge_request_timelog, spent_at: 5.days.ago, merge_request: subgroup_merge_request) }
-
- describe 'in_group' do
+ let_it_be(:short_time_ago) { 5.days.ago }
+ let_it_be(:medium_time_ago) { 15.days.ago }
+ let_it_be(:long_time_ago) { 65.days.ago }
+
+ let_it_be(:timelog) { create(:issue_timelog, spent_at: long_time_ago) }
+ let_it_be(:timelog1) { create(:issue_timelog, spent_at: medium_time_ago, issue: group_issue) }
+ let_it_be(:timelog2) { create(:issue_timelog, spent_at: short_time_ago, issue: subgroup_issue) }
+ let_it_be(:timelog3) { create(:merge_request_timelog, spent_at: long_time_ago) }
+ let_it_be(:timelog4) { create(:merge_request_timelog, spent_at: medium_time_ago, merge_request: group_merge_request) }
+ let_it_be(:timelog5) { create(:merge_request_timelog, spent_at: short_time_ago, merge_request: subgroup_merge_request) }
+
+ describe '.in_group' do
it 'return timelogs created for group issues and merge requests' do
expect(described_class.in_group(group)).to contain_exactly(timelog1, timelog2, timelog4, timelog5)
end
end
- describe 'between_times' do
- it 'returns collection of timelogs within given times' do
- timelogs = described_class.between_times(20.days.ago, 10.days.ago)
+ describe '.at_or_after' do
+ it 'returns timelogs at the time limit' do
+ timelogs = described_class.at_or_after(short_time_ago)
- expect(timelogs).to contain_exactly(timelog1, timelog4)
+ expect(timelogs).to contain_exactly(timelog2, timelog5)
end
+
+ it 'returns timelogs after given time' do
+ timelogs = described_class.at_or_after(just_before(short_time_ago))
+
+ expect(timelogs).to contain_exactly(timelog2, timelog5)
+ end
+ end
+
+ describe '.at_or_before' do
+ it 'returns timelogs at the time limit' do
+ timelogs = described_class.at_or_before(long_time_ago)
+
+ expect(timelogs).to contain_exactly(timelog, timelog3)
+ end
+
+ it 'returns timelogs before given time' do
+ timelogs = described_class.at_or_before(just_after(long_time_ago))
+
+ expect(timelogs).to contain_exactly(timelog, timelog3)
+ end
+ end
+
+ def just_before(time)
+ time - 1.day
+ end
+
+ def just_after(time)
+ time + 1.day
end
end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index caa0a886abf..651e2cf273f 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -376,6 +376,18 @@ RSpec.describe Todo do
end
end
+ describe '.for_note' do
+ it 'returns todos that belongs to notes' do
+ note_1 = create(:note, noteable: issue, project: issue.project)
+ note_2 = create(:note, noteable: issue, project: issue.project)
+ todo_1 = create(:todo, note: note_1)
+ todo_2 = create(:todo, note: note_2)
+ create(:todo, note: create(:note))
+
+ expect(described_class.for_note([note_1, note_2])).to contain_exactly(todo_1, todo_2)
+ end
+ end
+
describe '.group_by_user_id_and_state' do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 041af5b9c31..c2d9b916a1c 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -11,6 +11,11 @@ RSpec.describe UserDetail do
it { is_expected.to validate_length_of(:job_title).is_at_most(200) }
end
+ describe '#pronouns' do
+ it { is_expected.not_to validate_presence_of(:pronouns) }
+ it { is_expected.to validate_length_of(:pronouns).is_at_most(50) }
+ end
+
describe '#bio' do
it { is_expected.to validate_length_of(:bio).is_at_most(255) }
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index cb34917f073..f1c30a646f5 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe User do
describe 'constants' do
it { expect(described_class::COUNT_CACHE_VALIDITY_PERIOD).to be_a(Integer) }
+ it { expect(described_class::MAX_USERNAME_LENGTH).to be_a(Integer) }
+ it { expect(described_class::MIN_USERNAME_LENGTH).to be_a(Integer) }
end
describe 'delegations' do
@@ -72,6 +74,9 @@ RSpec.describe User do
it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil }
+ it { is_expected.to delegate_method(:pronouns).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:pronouns=).to(:user_detail).with_arguments(:args).allow_nil }
+
it { is_expected.to delegate_method(:bio).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:bio=).to(:user_detail).with_arguments(:args).allow_nil }
it { is_expected.to delegate_method(:bio_html).to(:user_detail).allow_nil }
@@ -90,7 +95,7 @@ RSpec.describe User do
it { is_expected.to have_many(:group_members) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_many(:keys).dependent(:destroy) }
- it { is_expected.to have_many(:expired_today_and_unnotified_keys) }
+ it { is_expected.to have_many(:expired_and_unnotified_keys) }
it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
it { is_expected.to have_many(:group_deploy_keys) }
it { is_expected.to have_many(:events).dependent(:delete_all) }
@@ -134,6 +139,12 @@ RSpec.describe User do
expect(user.bio).to eq(user.user_detail.bio)
end
+ it 'delegates `pronouns` to `user_detail`' do
+ user = create(:user, pronouns: 'they/them')
+
+ expect(user.pronouns).to eq(user.user_detail.pronouns)
+ end
+
it 'creates `user_detail` when `bio` is first updated' do
user = create(:user)
@@ -1025,12 +1036,6 @@ RSpec.describe User do
let_it_be(:expiring_soon_not_notified) { create(:key, expires_at: 2.days.from_now, user: user2) }
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 2.days.from_now, user: user1, before_expiry_notification_delivered_at: Time.current) }
- describe '.with_ssh_key_expired_today' do
- it 'returns users whose key has expired today' do
- expect(described_class.with_ssh_key_expired_today).to contain_exactly(user1)
- end
- end
-
describe '.with_ssh_key_expiring_soon' do
it 'returns users whose keys will expire soon' do
expect(described_class.with_ssh_key_expiring_soon).to contain_exactly(user2)
@@ -4258,45 +4263,16 @@ RSpec.describe User do
end
describe '#invalidate_issue_cache_counts' do
- let_it_be(:user) { create(:user) }
-
- subject do
- user.invalidate_issue_cache_counts
- user.save!
- end
-
- shared_examples 'invalidates the cached value' do
- it 'invalidates cache for issue counter' do
- expect(Rails.cache).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
-
- subject
- end
- end
-
- it_behaves_like 'invalidates the cached value'
-
- context 'if feature flag assigned_open_issues_cache is enabled' do
- it 'calls the recalculate worker' do
- expect(Users::UpdateOpenIssueCountWorker).to receive(:perform_async).with(user.id)
-
- subject
- end
-
- it_behaves_like 'invalidates the cached value'
- end
+ let(:user) { build_stubbed(:user) }
- context 'if feature flag assigned_open_issues_cache is disabled' do
- before do
- stub_feature_flags(assigned_open_issues_cache: false)
- end
+ it 'invalidates cache for issue counter' do
+ cache_mock = double
- it 'does not call the recalculate worker' do
- expect(Users::UpdateOpenIssueCountWorker).not_to receive(:perform_async).with(user.id)
+ expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
- subject
- end
+ allow(Rails).to receive(:cache).and_return(cache_mock)
- it_behaves_like 'invalidates the cached value'
+ user.invalidate_issue_cache_counts
end
end
@@ -5272,9 +5248,10 @@ RSpec.describe User do
let_it_be(:user3) { create(:user, :ghost) }
let_it_be(:user4) { create(:user, user_type: :support_bot) }
let_it_be(:user5) { create(:user, state: 'blocked', user_type: :support_bot) }
+ let_it_be(:user6) { create(:user, user_type: :automation_bot) }
it 'returns all active users including active bots but ghost users' do
- expect(described_class.active_without_ghosts).to match_array([user1, user4])
+ expect(described_class.active_without_ghosts).to match_array([user1, user4, user6])
end
end
@@ -5409,7 +5386,8 @@ RSpec.describe User do
{ user_type: :ghost },
{ user_type: :alert_bot },
{ user_type: :support_bot },
- { user_type: :security_bot }
+ { user_type: :security_bot },
+ { user_type: :automation_bot }
]
end
@@ -5465,6 +5443,7 @@ RSpec.describe User do
'alert_bot' | false
'support_bot' | false
'security_bot' | false
+ 'automation_bot' | false
end
with_them do
@@ -5612,10 +5591,12 @@ RSpec.describe User do
it_behaves_like 'bot users', :migration_bot
it_behaves_like 'bot users', :security_bot
it_behaves_like 'bot users', :ghost
+ it_behaves_like 'bot users', :automation_bot
it_behaves_like 'bot user avatars', :alert_bot, 'alert-bot.png'
it_behaves_like 'bot user avatars', :support_bot, 'support-bot.png'
it_behaves_like 'bot user avatars', :security_bot, 'security-bot.png'
+ it_behaves_like 'bot user avatars', :automation_bot, 'support-bot.png'
context 'when bot is the support_bot' do
subject { described_class.support_bot }
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index bbbc5d08c07..9e995366c17 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -565,4 +565,34 @@ RSpec.describe GlobalPolicy do
it { is_expected.not_to be_allowed(:log_in) }
end
end
+
+ describe 'update_runners_registration_token' do
+ context 'when anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.not_to be_allowed(:update_runners_registration_token) }
+ end
+
+ context 'regular user' do
+ it { is_expected.not_to be_allowed(:update_runners_registration_token) }
+ end
+
+ context 'when external' do
+ let(:current_user) { build(:user, :external) }
+
+ it { is_expected.not_to be_allowed(:update_runners_registration_token) }
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:update_runners_registration_token) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
+ end
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index f5e389ff338..9fac5521aa6 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -924,53 +924,53 @@ RSpec.describe GroupPolicy do
end
end
- context 'timelogs' do
- context 'with admin' do
+ describe 'update_runners_registration_token' do
+ context 'admin' do
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ it { is_expected.to be_allowed(:update_runners_registration_token) }
end
context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
end
context 'with owner' do
let(:current_user) { owner }
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ it { is_expected.to be_allowed(:update_runners_registration_token) }
end
context 'with maintainer' do
let(:current_user) { maintainer }
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ it { is_expected.to be_allowed(:update_runners_registration_token) }
end
context 'with reporter' do
let(:current_user) { reporter }
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
context 'with guest' do
let(:current_user) { guest }
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
context 'with non member' do
let(:current_user) { create(:user) }
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
context 'with anonymous' do
let(:current_user) { nil }
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
end
end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 76788ae2cb7..bc09191f6ec 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe IssuePolicy do
let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
let(:issue_no_assignee) { create(:issue, project: project) }
+ let(:new_issue) { build(:issue, project: project, assignees: [assignee], author: author) }
before do
project.add_guest(guest)
@@ -34,42 +35,51 @@ RSpec.describe IssuePolicy do
end
it 'does not allow non-members to read issues' do
- expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(non_member, new_issue)).to be_disallowed(:create_issue, :set_issue_metadata)
end
it 'allows guests to read issues' do
expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata)
expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata)
+
+ expect(permissions(guest, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows reporters to read, update, and admin issues' do
- expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows reporters from group links to read, update, and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(reporter_from_group_link, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows issue authors to read and update their issues' do
expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(author, issue)).to be_disallowed(:admin_issue)
+ expect(permissions(author, issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata)
+
+ expect(permissions(author, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows issue assignees to read and update their issues' do
expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(assignee, issue)).to be_disallowed(:admin_issue)
+ expect(permissions(assignee, issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue)
+ expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :set_issue_metadata)
+
+ expect(permissions(assignee, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
context 'with confidential issues' do
@@ -78,48 +88,49 @@ RSpec.describe IssuePolicy do
it 'does not allow non-members to read confidential issues' do
expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'does not allow guests to read confidential issues' do
expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'allows reporters to read, update, and admin confidential issues' do
- expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'allows reporters from group links to read, update, and admin confidential issues' do
- expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'allows issue authors to read and update their confidential issues' do
expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue)
+ expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:admin_issue, :set_issue_metadata)
end
it 'does not allow issue author to read or update confidential issue moved to an private project' do
confidential_issue.project = create(:project, :private)
- expect(permissions(author, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(author, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata)
end
it 'allows issue assignees to read and update their confidential issues' do
expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue)
+ expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
- expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'does not allow issue assignees to read or update confidential issue moved to an private project' do
confidential_issue.project = create(:project, :private)
- expect(permissions(assignee, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue)
+ expect(permissions(assignee, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :set_issue_metadata)
end
end
end
@@ -129,6 +140,7 @@ RSpec.describe IssuePolicy do
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
let(:issue_no_assignee) { create(:issue, project: project) }
let(:issue_locked) { create(:issue, :locked, project: project, author: author, assignees: [assignee]) }
+ let(:new_issue) { build(:issue, project: project) }
before do
project.add_guest(guest)
@@ -139,56 +151,65 @@ RSpec.describe IssuePolicy do
create(:project_group_link, group: group, project: project)
end
- it 'does not allow guest to create todos' do
+ it 'does not allow anonymous user to create todos' do
expect(permissions(nil, issue)).to be_allowed(:read_issue)
- expect(permissions(nil, issue)).to be_disallowed(:create_todo)
+ expect(permissions(nil, issue)).to be_disallowed(:create_todo, :update_subscription, :set_issue_metadata)
+ expect(permissions(nil, new_issue)).to be_disallowed(:create_issue, :set_issue_metadata)
end
it 'allows guests to read issues' do
- expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid, :create_todo)
- expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid, :create_todo, :update_subscription)
+ expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
expect(permissions(guest, issue_locked)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(guest, issue_locked)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(guest, issue_locked)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
+
+ expect(permissions(guest, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows reporters to read, update, reopen, and admin issues' do
- expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
- expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
- expect(permissions(reporter, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
+ expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
+ expect(permissions(reporter, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
expect(permissions(reporter, issue_locked)).to be_disallowed(:reopen_issue)
+ expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows reporters from group links to read, update, reopen and admin issues' do
- expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
- expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue)
- expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
+ expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue)
+ expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows issue authors to read, reopen and update their issues' do
expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
- expect(permissions(author, issue)).to be_disallowed(:admin_issue)
+ expect(permissions(author, issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
expect(permissions(author, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(author, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue)
+ expect(permissions(author, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue, :set_issue_metadata)
+
+ expect(permissions(author, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
it 'allows issue assignees to read, reopen and update their issues' do
expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :reopen_issue)
- expect(permissions(assignee, issue)).to be_disallowed(:admin_issue)
+ expect(permissions(assignee, issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid)
- expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue)
+ expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue, :reopen_issue, :set_issue_metadata)
expect(permissions(assignee, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(assignee, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue)
+ expect(permissions(assignee, issue_locked)).to be_disallowed(:admin_issue, :reopen_issue, :set_issue_metadata)
+
+ expect(permissions(author, new_issue)).to be_allowed(:create_issue, :set_issue_metadata)
end
context 'when issues are private' do
@@ -205,12 +226,18 @@ RSpec.describe IssuePolicy do
it 'forbids visitors from commenting' do
expect(permissions(visitor, issue)).to be_disallowed(:create_note)
end
+ it 'forbids visitors from subscribing' do
+ expect(permissions(visitor, issue)).to be_disallowed(:update_subscription)
+ end
it 'allows guests to view' do
expect(permissions(guest, issue)).to be_allowed(:read_issue)
end
it 'allows guests to comment' do
expect(permissions(guest, issue)).to be_allowed(:create_note)
end
+ it 'allows guests to subscribe' do
+ expect(permissions(guest, issue)).to be_allowed(:update_subscription)
+ end
context 'when admin mode is enabled', :enable_admin_mode do
it 'allows admins to view' do
@@ -239,31 +266,31 @@ RSpec.describe IssuePolicy do
it 'does not allow guests to read confidential issues' do
expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'allows reporters to read, update, and admin confidential issues' do
expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'allows reporter from group links to read, update, and admin confidential issues' do
expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
- expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'allows issue authors to read and update their confidential issues' do
expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue)
+ expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
- expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
it 'allows issue assignees to read and update their confidential issues' do
expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
- expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue)
+ expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue, :set_issue_metadata)
- expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
+ expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata)
end
end
end
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index 744822f58d1..b94df4d4374 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -26,7 +26,8 @@ RSpec.describe MergeRequestPolicy do
read_merge_request
create_todo
approve_merge_request
- create_note].freeze
+ create_note
+ update_subscription].freeze
shared_examples_for 'a denied user' do
let(:perms) { permissions(subject, merge_request) }
@@ -55,7 +56,7 @@ RSpec.describe MergeRequestPolicy do
subject { permissions(nil, merge_request) }
it do
- is_expected.to be_disallowed(:create_todo)
+ is_expected.to be_disallowed(:create_todo, :update_subscription)
end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 46da42a4787..d0fe0cca8a1 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1386,53 +1386,249 @@ RSpec.describe ProjectPolicy do
end
end
- context 'timelogs' do
- context 'with admin' do
- let(:current_user) { admin }
+ describe 'when user is authenticated via CI_JOB_TOKEN', :request_store do
+ let(:current_user) { developer }
+ let(:job) { build_stubbed(:ci_build, project: scope_project, user: current_user) }
- context 'when admin mode enabled', :enable_admin_mode do
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ before do
+ current_user.set_ci_job_token_scope!(job)
+ end
+
+ context 'when accessing a private project' do
+ let(:project) { private_project }
+
+ context 'when the job token comes from the same project' do
+ let(:scope_project) { project }
+
+ it { is_expected.to be_allowed(:developer_access) }
end
- context 'when admin mode disabled' do
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ context 'when the job token comes from another project' do
+ let(:scope_project) { create(:project, :private) }
+
+ before do
+ scope_project.add_developer(current_user)
+ end
+
+ it { is_expected.to be_disallowed(:guest_access) }
end
end
- context 'with owner' do
- let(:current_user) { owner }
+ context 'when accessing a public project' do
+ let(:project) { public_project }
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ context 'when the job token comes from the same project' do
+ let(:scope_project) { project }
+
+ it { is_expected.to be_allowed(:developer_access) }
+ end
+
+ context 'when the job token comes from another project' do
+ let(:scope_project) { create(:project, :private) }
+
+ before do
+ scope_project.add_developer(current_user)
+ end
+
+ it { is_expected.to be_disallowed(:public_access) }
+ end
end
+ end
- context 'with maintainer' do
- let(:current_user) { maintainer }
+ describe 'container_image policies' do
+ using RSpec::Parameterized::TableSyntax
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ let(:guest_operations_permissions) { [:read_container_image] }
+
+ let(:developer_operations_permissions) do
+ guest_operations_permissions + [
+ :create_container_image, :update_container_image, :destroy_container_image
+ ]
end
- context 'with reporter' do
- let(:current_user) { reporter }
+ let(:maintainer_operations_permissions) do
+ developer_operations_permissions + [
+ :admin_container_image
+ ]
+ end
- it { is_expected.to be_allowed(:read_group_timelogs) }
+ where(:project_visibility, :access_level, :role, :allowed) do
+ :public | ProjectFeature::ENABLED | :maintainer | true
+ :public | ProjectFeature::ENABLED | :developer | true
+ :public | ProjectFeature::ENABLED | :reporter | true
+ :public | ProjectFeature::ENABLED | :guest | true
+ :public | ProjectFeature::ENABLED | :anonymous | true
+ :public | ProjectFeature::PRIVATE | :maintainer | true
+ :public | ProjectFeature::PRIVATE | :developer | true
+ :public | ProjectFeature::PRIVATE | :reporter | true
+ :public | ProjectFeature::PRIVATE | :guest | false
+ :public | ProjectFeature::PRIVATE | :anonymous | false
+ :public | ProjectFeature::DISABLED | :maintainer | false
+ :public | ProjectFeature::DISABLED | :developer | false
+ :public | ProjectFeature::DISABLED | :reporter | false
+ :public | ProjectFeature::DISABLED | :guest | false
+ :public | ProjectFeature::DISABLED | :anonymous | false
+ :internal | ProjectFeature::ENABLED | :maintainer | true
+ :internal | ProjectFeature::ENABLED | :developer | true
+ :internal | ProjectFeature::ENABLED | :reporter | true
+ :internal | ProjectFeature::ENABLED | :guest | true
+ :internal | ProjectFeature::ENABLED | :anonymous | false
+ :internal | ProjectFeature::PRIVATE | :maintainer | true
+ :internal | ProjectFeature::PRIVATE | :developer | true
+ :internal | ProjectFeature::PRIVATE | :reporter | true
+ :internal | ProjectFeature::PRIVATE | :guest | false
+ :internal | ProjectFeature::PRIVATE | :anonymous | false
+ :internal | ProjectFeature::DISABLED | :maintainer | false
+ :internal | ProjectFeature::DISABLED | :developer | false
+ :internal | ProjectFeature::DISABLED | :reporter | false
+ :internal | ProjectFeature::DISABLED | :guest | false
+ :internal | ProjectFeature::DISABLED | :anonymous | false
+ :private | ProjectFeature::ENABLED | :maintainer | true
+ :private | ProjectFeature::ENABLED | :developer | true
+ :private | ProjectFeature::ENABLED | :reporter | true
+ :private | ProjectFeature::ENABLED | :guest | false
+ :private | ProjectFeature::ENABLED | :anonymous | false
+ :private | ProjectFeature::PRIVATE | :maintainer | true
+ :private | ProjectFeature::PRIVATE | :developer | true
+ :private | ProjectFeature::PRIVATE | :reporter | true
+ :private | ProjectFeature::PRIVATE | :guest | false
+ :private | ProjectFeature::PRIVATE | :anonymous | false
+ :private | ProjectFeature::DISABLED | :maintainer | false
+ :private | ProjectFeature::DISABLED | :developer | false
+ :private | ProjectFeature::DISABLED | :reporter | false
+ :private | ProjectFeature::DISABLED | :guest | false
+ :private | ProjectFeature::DISABLED | :anonymous | false
end
- context 'with guest' do
- let(:current_user) { guest }
+ with_them do
+ let(:current_user) { send(role) }
+ let(:project) { send("#{project_visibility}_project") }
+
+ it 'allows/disallows the abilities based on the container_registry feature access level' do
+ project.project_feature.update!(container_registry_access_level: access_level)
+
+ if allowed
+ expect_allowed(*permissions_abilities(role))
+ else
+ expect_disallowed(*permissions_abilities(role))
+ end
+ end
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ def permissions_abilities(role)
+ case role
+ when :maintainer
+ maintainer_operations_permissions
+ when :developer
+ developer_operations_permissions
+ when :reporter, :guest, :anonymous
+ guest_operations_permissions
+ else
+ raise "Unknown role #{role}"
+ end
+ end
end
- context 'with non member' do
- let(:current_user) { non_member }
+ context 'with read_container_registry_access_level disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ where(:project_visibility, :container_registry_enabled, :role, :allowed) do
+ :public | true | :maintainer | true
+ :public | true | :developer | true
+ :public | true | :reporter | true
+ :public | true | :guest | true
+ :public | true | :anonymous | true
+ :public | false | :maintainer | false
+ :public | false | :developer | false
+ :public | false | :reporter | false
+ :public | false | :guest | false
+ :public | false | :anonymous | false
+ :internal | true | :maintainer | true
+ :internal | true | :developer | true
+ :internal | true | :reporter | true
+ :internal | true | :guest | true
+ :internal | true | :anonymous | false
+ :internal | false | :maintainer | false
+ :internal | false | :developer | false
+ :internal | false | :reporter | false
+ :internal | false | :guest | false
+ :internal | false | :anonymous | false
+ :private | true | :maintainer | true
+ :private | true | :developer | true
+ :private | true | :reporter | true
+ :private | true | :guest | false
+ :private | true | :anonymous | false
+ :private | false | :maintainer | false
+ :private | false | :developer | false
+ :private | false | :reporter | false
+ :private | false | :guest | false
+ :private | false | :anonymous | false
+ end
+
+ with_them do
+ let(:current_user) { send(role) }
+ let(:project) { send("#{project_visibility}_project") }
+
+ it 'allows/disallows the abilities based on container_registry_enabled' do
+ project.update_column(:container_registry_enabled, container_registry_enabled)
+
+ if allowed
+ expect_allowed(*permissions_abilities(role))
+ else
+ expect_disallowed(*permissions_abilities(role))
+ end
+ end
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ def permissions_abilities(role)
+ case role
+ when :maintainer
+ maintainer_operations_permissions
+ when :developer
+ developer_operations_permissions
+ when :reporter, :guest, :anonymous
+ guest_operations_permissions
+ else
+ raise "Unknown role #{role}"
+ end
+ end
+ end
end
+ end
- context 'with anonymous' do
+ describe 'update_runners_registration_token' do
+ context 'when anonymous' do
let(:current_user) { anonymous }
- it { is_expected.to be_disallowed(:read_group_timelogs) }
+ it { is_expected.not_to be_allowed(:update_runners_registration_token) }
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:update_runners_registration_token) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
+ end
+ end
+
+ %w(guest reporter developer).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it { is_expected.to be_disallowed(:update_runners_registration_token) }
+ end
+ end
+
+ %w(maintainer owner).each do |role|
+ context role do
+ let(:current_user) { send(role) }
+
+ it { is_expected.to be_allowed(:update_runners_registration_token) }
+ end
end
end
end
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 2e8364b2987..c8da8a54f16 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -304,9 +304,9 @@ RSpec.describe Clusters::ClusterPresenter do
expect(presenter.gitlab_managed_apps_logs_path).to eq k8s_project_logs_path(project, cluster_id: cluster.id, format: :json)
end
- context 'cluster has elastic stack application installed' do
+ context 'cluster has elastic stack integration enabled' do
before do
- create(:clusters_applications_elastic_stack, :installed, cluster: cluster)
+ create(:clusters_integrations_elastic_stack, cluster: cluster)
end
it 'returns path to logs' do
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 76b77ee0de2..c64f9e8465f 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe MergeRequestPresenter do
context 'when no head pipeline' do
it 'return status using CiService' do
- ci_service = double(MockCiService)
+ ci_service = double(Integrations::MockCi)
ci_status = double
allow(resource.source_project)
diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb
index d8f1c98e762..3009f2bd56d 100644
--- a/spec/presenters/packages/detail/package_presenter_spec.rb
+++ b/spec/presenters/packages/detail/package_presenter_spec.rb
@@ -19,7 +19,8 @@ RSpec.describe ::Packages::Detail::PackagePresenter do
size: file.size,
file_md5: file.file_md5,
file_sha1: file.file_sha1,
- file_sha256: file.file_sha256
+ file_sha256: file.file_sha256,
+ id: file.id
}
end
end
diff --git a/spec/presenters/packages/pypi/package_presenter_spec.rb b/spec/presenters/packages/pypi/package_presenter_spec.rb
index e4d234a4688..25aa5c31034 100644
--- a/spec/presenters/packages/pypi/package_presenter_spec.rb
+++ b/spec/presenters/packages/pypi/package_presenter_spec.rb
@@ -5,45 +5,52 @@ require 'spec_helper'
RSpec.describe ::Packages::Pypi::PackagePresenter do
using RSpec::Parameterized::TableSyntax
- let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
let_it_be(:package_name) { 'sample-project' }
let_it_be(:package1) { create(:pypi_package, project: project, name: package_name, version: '1.0.0') }
let_it_be(:package2) { create(:pypi_package, project: project, name: package_name, version: '2.0.0') }
let(:packages) { [package1, package2] }
- let(:presenter) { described_class.new(packages, project) }
- describe '#body' do
- subject { presenter.body}
+ let(:file) { package.package_files.first }
+ let(:filename) { file.file_name }
- shared_examples_for "pypi package presenter" do
- let(:file) { package.package_files.first }
- let(:filename) { file.file_name }
- let(:expected_file) { "<a href=\"http://localhost/api/v4/projects/#{project.id}/packages/pypi/files/#{file.file_sha256}/#{filename}#sha256=#{file.file_sha256}\" data-requires-python=\"#{expected_python_version}\">#{filename}</a><br>" }
+ subject(:presenter) { described_class.new(packages, project_or_group).body}
- before do
- package.pypi_metadatum.required_python = python_version
+ describe '#body' do
+ shared_examples_for "pypi package presenter" do
+ where(:version, :expected_version, :with_package1) do
+ '>=2.7' | '&gt;=2.7' | true
+ '"><script>alert(1)</script>' | '&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;' | true
+ '>=2.7, !=3.0' | '&gt;=2.7, !=3.0' | false
end
- it { is_expected.to include expected_file }
- end
+ with_them do
+ let(:python_version) { version }
+ let(:expected_python_version) { expected_version }
+ let(:package) { with_package1 ? package1 : package2 }
- it_behaves_like "pypi package presenter" do
- let(:python_version) { '>=2.7' }
- let(:expected_python_version) { '&gt;=2.7' }
- let(:package) { package1 }
+ before do
+ package.pypi_metadatum.required_python = python_version
+ end
+
+ it { is_expected.to include expected_file }
+ end
end
- it_behaves_like "pypi package presenter" do
- let(:python_version) { '"><script>alert(1)</script>' }
- let(:expected_python_version) { '&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;' }
- let(:package) { package1 }
+ context 'for project' do
+ let(:project_or_group) { project }
+ let(:expected_file) { "<a href=\"http://localhost/api/v4/projects/#{project.id}/packages/pypi/files/#{file.file_sha256}/#{filename}#sha256=#{file.file_sha256}\" data-requires-python=\"#{expected_python_version}\">#{filename}</a><br>" }
+
+ it_behaves_like 'pypi package presenter'
end
- it_behaves_like "pypi package presenter" do
- let(:python_version) { '>=2.7, !=3.0' }
- let(:expected_python_version) { '&gt;=2.7, !=3.0' }
- let(:package) { package2 }
+ context 'for group' do
+ let(:project_or_group) { group }
+ let(:expected_file) { "<a href=\"http://localhost/api/v4/groups/#{group.id}/-/packages/pypi/files/#{file.file_sha256}/#{filename}#sha256=#{file.file_sha256}\" data-requires-python=\"#{expected_python_version}\">#{filename}</a><br>" }
+
+ it_behaves_like 'pypi package presenter'
end
end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index c834e183e5e..fd75c8411d5 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -585,7 +585,7 @@ RSpec.describe ProjectPresenter do
"can_push_code" => "true",
"original_branch" => "master",
"path" => "/#{project.full_path}/-/create/master",
- "project_path" => project.path,
+ "project_path" => project.full_path,
"target_branch" => "master"
}
)
diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb
index 97c05a1b7a2..b2e7b684644 100644
--- a/spec/presenters/release_presenter_spec.rb
+++ b/spec/presenters/release_presenter_spec.rb
@@ -77,14 +77,6 @@ RSpec.describe ReleasePresenter do
it 'returns merge requests url with state=open' do
is_expected.to eq(project_merge_requests_url(project, opened_url_params))
end
-
- context 'when release_mr_issue_urls feature flag is disabled' do
- before do
- stub_feature_flags(release_mr_issue_urls: false)
- end
-
- it { is_expected.to be_nil }
- end
end
describe '#merged_merge_requests_url' do
@@ -93,14 +85,6 @@ RSpec.describe ReleasePresenter do
it 'returns merge requests url with state=merged' do
is_expected.to eq(project_merge_requests_url(project, merged_url_params))
end
-
- context 'when release_mr_issue_urls feature flag is disabled' do
- before do
- stub_feature_flags(release_mr_issue_urls: false)
- end
-
- it { is_expected.to be_nil }
- end
end
describe '#closed_merge_requests_url' do
@@ -109,14 +93,6 @@ RSpec.describe ReleasePresenter do
it 'returns merge requests url with state=closed' do
is_expected.to eq(project_merge_requests_url(project, closed_url_params))
end
-
- context 'when release_mr_issue_urls feature flag is disabled' do
- before do
- stub_feature_flags(release_mr_issue_urls: false)
- end
-
- it { is_expected.to be_nil }
- end
end
describe '#opened_issues_url' do
@@ -125,14 +101,6 @@ RSpec.describe ReleasePresenter do
it 'returns issues url with state=open' do
is_expected.to eq(project_issues_url(project, opened_url_params))
end
-
- context 'when release_mr_issue_urls feature flag is disabled' do
- before do
- stub_feature_flags(release_mr_issue_urls: false)
- end
-
- it { is_expected.to be_nil }
- end
end
describe '#closed_issues_url' do
@@ -141,14 +109,6 @@ RSpec.describe ReleasePresenter do
it 'returns issues url with state=closed' do
is_expected.to eq(project_issues_url(project, closed_url_params))
end
-
- context 'when release_mr_issue_urls feature flag is disabled' do
- before do
- stub_feature_flags(release_mr_issue_urls: false)
- end
-
- it { is_expected.to be_nil }
- end
end
describe '#edit_url' do
diff --git a/spec/presenters/service_hook_presenter_spec.rb b/spec/presenters/service_hook_presenter_spec.rb
index bc6e505d9e1..7d7b71f324a 100644
--- a/spec/presenters/service_hook_presenter_spec.rb
+++ b/spec/presenters/service_hook_presenter_spec.rb
@@ -4,15 +4,15 @@ require 'spec_helper'
RSpec.describe ServiceHookPresenter do
let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
- let(:service_hook) { create(:service_hook, integration: service) }
- let(:service) { create(:drone_ci_service, project: project) }
+ let(:service_hook) { create(:service_hook, integration: integration) }
+ let(:integration) { create(:drone_ci_integration, project: project) }
let(:project) { create(:project) }
describe '#logs_details_path' do
subject { service_hook.present.logs_details_path(web_hook_log) }
let(:expected_path) do
- "/#{project.namespace.path}/#{project.name}/-/services/#{service.to_param}/hook_logs/#{web_hook_log.id}"
+ "/#{project.namespace.path}/#{project.name}/-/services/#{integration.to_param}/hook_logs/#{web_hook_log.id}"
end
it { is_expected.to eq(expected_path) }
@@ -22,7 +22,7 @@ RSpec.describe ServiceHookPresenter do
subject { service_hook.present.logs_retry_path(web_hook_log) }
let(:expected_path) do
- "/#{project.namespace.path}/#{project.name}/-/services/#{service.to_param}/hook_logs/#{web_hook_log.id}/retry"
+ "/#{project.namespace.path}/#{project.name}/-/services/#{integration.to_param}/hook_logs/#{web_hook_log.id}/retry"
end
it { is_expected.to eq(expected_path) }
diff --git a/spec/presenters/web_hook_log_presenter_spec.rb b/spec/presenters/web_hook_log_presenter_spec.rb
index ec930be266d..aa9d1d8f545 100644
--- a/spec/presenters/web_hook_log_presenter_spec.rb
+++ b/spec/presenters/web_hook_log_presenter_spec.rb
@@ -18,10 +18,10 @@ RSpec.describe WebHookLogPresenter do
end
context 'service hook' do
- let(:web_hook) { create(:service_hook, integration: service) }
- let(:service) { create(:drone_ci_service, project: project) }
+ let(:web_hook) { create(:service_hook, integration: integration) }
+ let(:integration) { create(:drone_ci_integration, project: project) }
- it { is_expected.to eq(project_service_hook_log_path(project, service, web_hook_log)) }
+ it { is_expected.to eq(project_service_hook_log_path(project, integration, web_hook_log)) }
end
end
@@ -38,10 +38,10 @@ RSpec.describe WebHookLogPresenter do
end
context 'service hook' do
- let(:web_hook) { create(:service_hook, integration: service) }
- let(:service) { create(:drone_ci_service, project: project) }
+ let(:web_hook) { create(:service_hook, integration: integration) }
+ let(:integration) { create(:drone_ci_integration, project: project) }
- it { is_expected.to eq(retry_project_service_hook_log_path(project, service, web_hook_log)) }
+ it { is_expected.to eq(retry_project_service_hook_log_path(project, integration, web_hook_log)) }
end
end
end
diff --git a/spec/rack_servers/unicorn_spec.rb b/spec/rack_servers/unicorn_spec.rb
deleted file mode 100644
index 52d44b6e7e0..00000000000
--- a/spec/rack_servers/unicorn_spec.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-require 'fileutils'
-
-require 'excon'
-
-require 'spec_helper'
-
-RSpec.describe 'Unicorn' do
- before(:all) do
- project_root = File.expand_path('../..', __dir__)
-
- config_lines = File.read('config/unicorn.rb.example')
- .gsub('/home/git/gitlab', project_root)
- .gsub('/home/git', project_root)
- .split("\n")
-
- # Remove these because they make setup harder.
- config_lines = config_lines.reject do |line|
- %w[
- worker_processes
- listen
- pid
- stderr_path
- stdout_path
- ].any? { |prefix| line.start_with?(prefix) }
- end
-
- config_lines << "working_directory '#{Rails.root}'"
-
- # We want to have exactly 1 worker process because that makes it
- # predictable which process will handle our requests.
- config_lines << 'worker_processes 1'
-
- @socket_path = File.join(project_root, 'tmp/tests/unicorn.socket')
- config_lines << "listen '#{@socket_path}'"
-
- ready_file = File.join(project_root, 'tmp/tests/unicorn-worker-ready')
- FileUtils.rm_f(ready_file)
- after_fork_index = config_lines.index { |l| l.start_with?('after_fork') }
- config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)")
-
- config_path = File.join(project_root, 'tmp/tests/unicorn.rb')
- File.write(config_path, config_lines.join("\n") + "\n")
-
- cmd = %W[unicorn -E test -c #{config_path} spec/rack_servers/configs/config.ru]
- @unicorn_master_pid = spawn(*cmd)
- wait_unicorn_boot!(@unicorn_master_pid, ready_file)
- WebMock.allow_net_connect!
- end
-
- %w[SIGQUIT SIGTERM SIGKILL].each do |signal|
- it "has a worker that self-terminates on signal #{signal}" do
- response = Excon.get('unix://', socket: @socket_path)
- expect(response.status).to eq(200)
-
- worker_pid = response.body.to_i
- expect(worker_pid).to be > 0
-
- begin
- Excon.post("unix://?#{signal}", socket: @socket_path)
- rescue Excon::Error::Socket
- # The connection may be closed abruptly
- end
-
- expect(pid_gone?(worker_pid)).to eq(true)
- end
- end
-
- after(:all) do
- webmock_enable!
- Process.kill('TERM', @unicorn_master_pid)
- end
-
- def wait_unicorn_boot!(master_pid, ready_file)
- # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes.
- timeout = 5 * 60
- timeout.times do
- return if File.exist?(ready_file)
-
- pid = Process.waitpid(master_pid, Process::WNOHANG)
- raise "unicorn failed to boot: #{$?}" unless pid.nil?
-
- sleep 1
- end
-
- raise "unicorn boot timed out after #{timeout} seconds"
- end
-
- def pid_gone?(pid)
- # Worker termination should take less than a second. That makes 10
- # seconds a generous timeout.
- 10.times do
- begin
- Process.kill(0, pid)
- rescue Errno::ESRCH
- return true
- end
-
- sleep 1
- end
-
- false
- end
-end
diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb
index b47240658e3..7df1cf7444f 100644
--- a/spec/rake_helper.rb
+++ b/spec/rake_helper.rb
@@ -6,18 +6,12 @@ require 'rake'
RSpec.configure do |config|
config.include RakeHelpers
- # Redirect stdout so specs don't have so much noise
config.before(:all) do
- $stdout = StringIO.new
-
Rake.application.rake_require 'tasks/gitlab/helpers'
Rake::Task.define_task :environment
end
- # Reset stdout
config.after(:all) do
- $stdout = STDOUT
-
delete_from_all_tables!
end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index a38ba782c44..36fbe86ac76 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe API::Branches do
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:branch_name) { 'feature' }
let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
- let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
- let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
+ let(:branch_with_dot) { 'ends-with.json' }
+ let(:branch_with_slash) { 'improve/awesome' }
let(:project_id) { project.id }
let(:current_user) { nil }
@@ -105,7 +105,7 @@ RSpec.describe API::Branches do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/branches')
- expect(response.headers).not_to include('Link', 'Links')
+ expect(response.headers).not_to include('Link')
branch_names = json_response.map { |x| x['name'] }
expect(branch_names).to match_array(project.repository.branch_names)
end
@@ -116,7 +116,7 @@ RSpec.describe API::Branches do
get api(route, current_user), params: base_params.merge(per_page: 2)
expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers).to include('Link', 'Links')
+ expect(response.headers).to include('Link')
expect(json_response.count).to eq 2
check_merge_status(json_response)
@@ -285,6 +285,13 @@ RSpec.describe API::Branches do
let(:request) { get api(route, current_user) }
end
end
+
+ context 'when repository does not exist' do
+ it_behaves_like '404 response' do
+ let(:project) { create(:project, creator: user) }
+ let(:request) { get api(route, current_user) }
+ end
+ end
end
context 'when unauthenticated', 'and project is public' do
@@ -320,19 +327,19 @@ RSpec.describe API::Branches do
end
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository branch'
end
context 'when branch contains dot txt' do
- let(:branch_name) { project.repository.find_branch('ends-with.txt').name }
+ let(:branch_name) { 'ends-with.txt' }
it_behaves_like 'repository branch'
end
context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash.name }
+ let(:branch_name) { branch_with_slash }
it_behaves_like '404 response' do
let(:request) { get api(route, current_user) }
@@ -340,7 +347,7 @@ RSpec.describe API::Branches do
end
context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash.name) }
+ let(:branch_name) { CGI.escape(branch_with_slash) }
it_behaves_like 'repository branch'
end
@@ -351,7 +358,7 @@ RSpec.describe API::Branches do
it_behaves_like 'repository branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository branch'
end
@@ -475,13 +482,13 @@ RSpec.describe API::Branches do
it_behaves_like 'repository new protected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository new protected branch'
end
context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash.name }
+ let(:branch_name) { branch_with_slash }
it_behaves_like '404 response' do
let(:request) { put api(route, current_user) }
@@ -489,7 +496,7 @@ RSpec.describe API::Branches do
end
context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash.name) }
+ let(:branch_name) { CGI.escape(branch_with_slash) }
it_behaves_like 'repository new protected branch'
end
@@ -500,7 +507,7 @@ RSpec.describe API::Branches do
it_behaves_like 'repository new protected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository new protected branch'
end
@@ -609,13 +616,13 @@ RSpec.describe API::Branches do
it_behaves_like 'repository unprotected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository unprotected branch'
end
context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash.name }
+ let(:branch_name) { branch_with_slash }
it_behaves_like '404 response' do
let(:request) { put api(route, current_user) }
@@ -623,7 +630,7 @@ RSpec.describe API::Branches do
end
context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash.name) }
+ let(:branch_name) { CGI.escape(branch_with_slash) }
it_behaves_like 'repository unprotected branch'
end
@@ -634,7 +641,7 @@ RSpec.describe API::Branches do
it_behaves_like 'repository unprotected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository unprotected branch'
end
@@ -732,7 +739,7 @@ RSpec.describe API::Branches do
end
it 'removes a branch with dots in the branch name' do
- delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user)
+ delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot}", user)
expect(response).to have_gitlab_http_status(:no_content)
end
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index 3d5021fba08..8c95748aa5f 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -17,18 +17,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
describe '/api/v4/jobs' do
- let(:group) { create(:group, :nested) }
- let(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
- let(:runner) { create(:ci_runner, :project, projects: [project]) }
- let(:user) { create(:user) }
- let(:job) do
- create(:ci_build, :artifacts, :extended_options,
- pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
- end
+ let_it_be(:group) { create(:group, :nested) }
+ 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(:user) { create(:user) }
describe 'PUT /api/v4/jobs/:id' do
- let(:job) do
+ let_it_be_with_reload(:job) do
create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, runner_id: runner.id)
end
@@ -204,53 +200,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
- context 'when trace is given' do
- it 'creates a trace artifact' do
- allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do
- ArchiveTraceWorker.new.perform(job.id)
- end
-
- update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
-
- job.reload
- expect(response).to have_gitlab_http_status(:ok)
- expect(job.trace.raw).to eq 'BUILD TRACE UPDATED'
- expect(job.job_artifacts_trace.open.read).to eq 'BUILD TRACE UPDATED'
- end
-
- context 'when concurrent update of trace is happening' do
- before do
- job.trace.write('wb') do
- update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
- end
- end
-
- it 'returns that operation conflicts' do
- expect(response).to have_gitlab_http_status(:conflict)
- end
- end
- end
-
- context 'when no trace is given' do
- it 'does not override trace information' do
- update_job
-
- expect(job.reload.trace.raw).to eq 'BUILD TRACE'
- end
-
- context 'when running state is sent' do
- it 'updates update_at value' do
- expect { update_job_after_time }.to change { job.reload.updated_at }
- end
- end
-
- context 'when other state is sent' do
- it "doesn't update update_at value" do
- expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at }
- end
- end
- end
-
context 'when job has been erased' do
let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
@@ -267,20 +216,19 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
job.drop!(:script_failure)
end
- it 'does not update job status and job trace' do
- update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
+ it 'does not update job status' do
+ update_job(state: 'success')
job.reload
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.header['Job-Status']).to eq 'failed'
- expect(job.trace.raw).to eq 'Job failed'
expect(job).to be_failed
end
end
context 'when job does not exist anymore' do
it 'returns 403 Forbidden' do
- update_job(non_existing_record_id, state: 'success', trace: 'BUILD TRACE UPDATED')
+ update_job(non_existing_record_id, state: 'success')
expect(response).to have_gitlab_http_status(:forbidden)
end
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 63da3340a45..8896bd44077 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:user) { create(:user) }
let(:job) do
- create(:ci_build, :artifacts, :extended_options,
+ create(:ci_build, :pending, :queued, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0)
end
@@ -129,7 +129,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when other projects have pending jobs' do
before do
job.success
- create(:ci_build, :pending)
+ create(:ci_build, :pending, :queued)
end
it_behaves_like 'no jobs available'
@@ -239,7 +239,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when job is made for tag' do
- let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
it 'sets branch as ref_type' do
request_job
@@ -297,7 +297,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when job filtered by job_age' do
- let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
+ let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
context 'job is queued less than job_age parameter' do
let(:job_age) { 120 }
@@ -359,7 +359,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when job is for a release' do
- let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) }
+ let!(:job) { create(:ci_build, :pending, :queued, :release_options, pipeline: pipeline) }
context 'when `multi_build_steps` is passed by the runner' do
it 'exposes release info' do
@@ -398,7 +398,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when job is made for merge request' do
let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
- let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
let(:merge_request) { create(:merge_request) }
it 'sets branch as ref_type' do
@@ -439,6 +439,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
+ it "sets the runner's config" do
+ request_job info: { 'config' => { 'gpus' => 'all', 'ignored' => 'hello' } }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(runner.reload.config).to eq( { 'gpus' => 'all' } )
+ end
+
it "sets the runner's ip_address" do
post api('/jobs/request'),
params: { token: runner.token },
@@ -472,9 +479,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when project and pipeline have multiple jobs' do
- let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
- let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
+ let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
job.success
@@ -524,8 +531,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when pipeline have jobs with artifacts' do
- let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
+ let!(:job) { create(:ci_build, :pending, :queued, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
job.success
@@ -544,10 +551,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when explicit dependencies are defined' do
- let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
- create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
+ create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [job2.name] })
end
@@ -568,10 +575,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when dependencies is an empty array' do
- let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
- create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
+ create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1,
options: { script: ['bash'], dependencies: [] })
end
@@ -732,7 +739,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
describe 'port support' do
- let(:job) { create(:ci_build, pipeline: pipeline, options: options) }
+ let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
context 'when job image has ports' do
let(:options) do
@@ -784,7 +791,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
describe 'a job with excluded artifacts' do
context 'when excluded paths are defined' do
let(:job) do
- create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test',
+ create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'test',
stage: 'deploy', stage_idx: 1,
options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
end
@@ -832,7 +839,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
subject { request_job }
context 'when triggered by a user' do
- let(:job) { create(:ci_build, user: user, project: project) }
+ let(:job) { create(:ci_build, :pending, :queued, user: user, project: project) }
subject { request_job(id: job.id) }
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index b38630183f4..1696fe63d5d 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -94,7 +94,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when it exceeds the application limits' do
before do
- create(:ci_runner, runner_type: :project_type, projects: [project])
+ create(:ci_runner, runner_type: :project_type, projects: [project], contacted_at: 1.second.ago)
create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
end
@@ -106,6 +106,22 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(project.runners.reload.size).to eq(1)
end
end
+
+ context 'when abandoned runners cause application limits to not be exceeded' do
+ before do
+ create(:ci_runner, runner_type: :project_type, projects: [project], created_at: 14.months.ago, contacted_at: 13.months.ago)
+ create(:plan_limits, :default_plan, ci_registered_project_runners: 1)
+ end
+
+ it 'creates runner' do
+ request
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['message']).to be_nil
+ expect(project.runners.reload.size).to eq(2)
+ expect(project.runners.recent.size).to eq(1)
+ end
+ end
end
context 'when group token is used' do
@@ -135,7 +151,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when it exceeds the application limits' do
before do
- create(:ci_runner, runner_type: :group_type, groups: [group])
+ create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 1.month.ago)
create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
end
@@ -147,6 +163,23 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(group.runners.reload.size).to eq(1)
end
end
+
+ context 'when abandoned runners cause application limits to not be exceeded' do
+ before do
+ create(:ci_runner, runner_type: :group_type, groups: [group], created_at: 4.months.ago, contacted_at: 3.months.ago)
+ create(:ci_runner, runner_type: :group_type, groups: [group], contacted_at: nil, created_at: 4.months.ago)
+ create(:plan_limits, :default_plan, ci_registered_group_runners: 1)
+ end
+
+ it 'creates runner' do
+ request
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['message']).to be_nil
+ expect(group.runners.reload.size).to eq(3)
+ expect(group.runners.recent.size).to eq(1)
+ end
+ end
end
end
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 1727bc830fc..82fb4440429 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -137,11 +137,11 @@ RSpec.describe API::Ci::Runners do
get api('/runners/all', admin)
expect(json_response).to match_array [
- a_hash_including('description' => 'Project runner'),
- a_hash_including('description' => 'Two projects runner'),
- a_hash_including('description' => 'Group runner A'),
- a_hash_including('description' => 'Group runner B'),
- a_hash_including('description' => 'Shared runner')
+ a_hash_including('description' => 'Project runner', 'is_shared' => false, 'runner_type' => 'project_type'),
+ a_hash_including('description' => 'Two projects runner', 'is_shared' => false, 'runner_type' => 'project_type'),
+ a_hash_including('description' => 'Group runner A', 'is_shared' => false, 'runner_type' => 'group_type'),
+ a_hash_including('description' => 'Group runner B', 'is_shared' => false, 'runner_type' => 'group_type'),
+ a_hash_including('description' => 'Shared runner', 'is_shared' => true, 'runner_type' => 'instance_type')
]
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index ac125e81acd..ccc9f8c50c4 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe API::CommitStatuses do
- let!(:project) { create(:project, :repository) }
- let(:commit) { project.repository.commit }
- let(:guest) { create_user(:guest) }
- let(:reporter) { create_user(:reporter) }
- let(:developer) { create_user(:developer) }
- let(:sha) { commit.id }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:commit) { project.repository.commit }
+ let_it_be(:guest) { create_user(:guest) }
+ let_it_be(:reporter) { create_user(:reporter) }
+ let_it_be(:developer) { create_user(:developer) }
+ let_it_be(:sha) { commit.id }
describe "GET /projects/:id/repository/commits/:sha/statuses" do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
@@ -233,27 +233,44 @@ RSpec.describe API::CommitStatuses do
end
end
- context 'when updatig a commit status' do
+ context 'when updating a commit status' do
+ let(:parameters) do
+ {
+ state: 'success',
+ name: 'coverage',
+ ref: 'master'
+ }
+ end
+
+ let(:updatable_optional_attributes) do
+ {
+ description: 'new description',
+ coverage: 90.0
+ }
+ end
+
+ # creating the initial commit status
before do
post api(post_url, developer), params: {
state: 'running',
context: 'coverage',
ref: 'master',
description: 'coverage test',
- coverage: 0.0,
+ coverage: 10.0,
target_url: 'http://gitlab.com/status'
}
+ end
+ subject(:send_request) do
post api(post_url, developer), params: {
- state: 'success',
- name: 'coverage',
- ref: 'master',
- description: 'new description',
- coverage: 90.0
+ **parameters,
+ **updatable_optional_attributes
}
end
it 'updates a commit status' do
+ send_request
+
expect(response).to have_gitlab_http_status(:created)
expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success')
@@ -265,7 +282,28 @@ RSpec.describe API::CommitStatuses do
end
it 'does not create a new commit status' do
- expect(CommitStatus.count).to eq 1
+ expect { send_request }.not_to change { CommitStatus.count }
+ end
+
+ context 'when the `state` parameter is sent the same' do
+ let(:parameters) do
+ {
+ state: 'running',
+ name: 'coverage',
+ ref: 'master'
+ }
+ end
+
+ it 'does not update the commit status' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+
+ commit_status = project.commit_statuses.find_by!(name: 'coverage')
+
+ expect(commit_status.description).to eq('coverage test')
+ expect(commit_status.coverage).to eq(10.0)
+ end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index ac3aa808f37..1162ae76d15 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -284,6 +284,18 @@ RSpec.describe API::Commits do
end
end
end
+
+ context 'with the optional trailers parameter' do
+ it 'includes the Git trailers' do
+ get api("/projects/#{project_id}/repository/commits?ref_name=6d394385cf567f80a8fd85055db1ab4c5295806f&trailers=true", current_user)
+
+ commit = json_response[0]
+
+ expect(commit['trailers']).to eq(
+ 'Signed-off-by' => 'Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>'
+ )
+ end
+ end
end
end
@@ -1503,6 +1515,13 @@ RSpec.describe API::Commits do
expect(json_response).to eq("dry_run" => "success")
expect(project.commit(branch)).to eq(head)
end
+
+ it 'supports the use of a custom commit message' do
+ post api(route, user), params: { branch: branch, message: 'foo' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response["message"]).to eq('foo')
+ end
end
context 'when repository is disabled' do
diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb
index 0ff88cb41a8..4120edabea3 100644
--- a/spec/requests/api/composer_packages_spec.rb
+++ b/spec/requests/api/composer_packages_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe API::ComposerPackages do
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:package_name) { 'package-name' }
let_it_be(:project, reload: true) { create(:project, :custom_repo, files: { 'composer.json' => { name: package_name }.to_json }, group: group) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
let(:headers) { {} }
using RSpec::Parameterized::TableSyntax
@@ -428,6 +429,7 @@ RSpec.describe API::ComposerPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb
index 817530f0bad..ff3b332c620 100644
--- a/spec/requests/api/conan_instance_packages_spec.rb
+++ b/spec/requests/api/conan_instance_packages_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::ConanInstancePackages do
+ let(:snowplow_standard_context_params) { { user: user, project: project, namespace: project.namespace } }
+
include_context 'conan api setup'
describe 'GET /api/v4/packages/conan/v1/ping' do
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 42c6c987872..c3abb06c5c1 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -7,33 +7,33 @@ RSpec.describe API::DebianGroupPackages do
include_context 'Debian repository shared context', :group, false do
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release.gpg" }
it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/Release" }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Release'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^TODO Release$/
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/InRelease" }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/InRelease" }
it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component}/binary-#{architecture}/Packages" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Packages'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^TODO Packages$/
end
describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
let(:url) { "/groups/#{container.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO File'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^TODO File$/
end
end
end
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index f400b6e928c..c11c4ecc12a 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -7,43 +7,55 @@ RSpec.describe API::DebianProjectPackages do
include_context 'Debian repository shared context', :project, true do
describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/Release.gpg" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" }
it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
describe 'GET projects/:id/packages/debian/dists/*distribution/Release' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/Release" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Release'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^TODO Release$/
end
describe 'GET projects/:id/packages/debian/dists/*distribution/InRelease' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/InRelease" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" }
it_behaves_like 'Debian repository read endpoint', 'GET request', :not_found
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component}/binary-#{architecture}/Packages" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO Packages'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^TODO Packages$/
end
describe 'GET projects/:id/packages/debian/pool/:component/:letter/:source_package/:file_name' do
let(:url) { "/projects/#{container.id}/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
- it_behaves_like 'Debian repository read endpoint', 'GET request', :success, 'TODO File'
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^TODO File$/
end
describe 'PUT projects/:id/packages/debian/:file_name' do
let(:method) { :put }
let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}" }
+ let(:snowplow_gitlab_standard_context) { { project: container, user: user, namespace: container.namespace } }
- it_behaves_like 'Debian repository write endpoint', 'upload request', :created
+ context 'with a deb' do
+ let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
+
+ it_behaves_like 'Debian repository write endpoint', 'upload request', :created
+ end
+
+ context 'with a changes file' do
+ let(:file_name) { 'sample_1.2.3~alpha2_amd64.changes' }
+
+ it_behaves_like 'Debian repository write endpoint', 'upload request', :created
+ end
end
describe 'PUT projects/:id/packages/debian/:file_name/authorize' do
+ let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
let(:method) { :put }
let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" }
diff --git a/spec/requests/api/feature_flag_scopes_spec.rb b/spec/requests/api/feature_flag_scopes_spec.rb
deleted file mode 100644
index da5b2cbb7ae..00000000000
--- a/spec/requests/api/feature_flag_scopes_spec.rb
+++ /dev/null
@@ -1,319 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe API::FeatureFlagScopes do
- include FeatureFlagHelpers
-
- let(:project) { create(:project, :repository) }
- let(:developer) { create(:user) }
- let(:reporter) { create(:user) }
- let(:user) { developer }
-
- before do
- project.add_developer(developer)
- project.add_reporter(reporter)
- end
-
- shared_examples_for 'check user permission' do
- context 'when user is reporter' do
- let(:user) { reporter }
-
- it 'forbids the request' do
- subject
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- shared_examples_for 'not found' do
- it 'returns Not Found' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- describe 'GET /projects/:id/feature_flag_scopes' do
- subject do
- get api("/projects/#{project.id}/feature_flag_scopes", user),
- params: params
- end
-
- let(:feature_flag_1) { create_flag(project, 'flag_1', true) }
- let(:feature_flag_2) { create_flag(project, 'flag_2', true) }
-
- before do
- create_scope(feature_flag_1, 'staging', false)
- create_scope(feature_flag_1, 'production', true)
- create_scope(feature_flag_2, 'review/*', false)
- end
-
- context 'when environment is production' do
- let(:params) { { environment: 'production' } }
-
- it_behaves_like 'check user permission'
-
- it 'returns all effective feature flags under the environment' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag_detailed_scopes')
- expect(json_response.second).to include({ 'name' => 'flag_1', 'active' => true })
- expect(json_response.first).to include({ 'name' => 'flag_2', 'active' => true })
- end
- end
-
- context 'when environment is staging' do
- let(:params) { { environment: 'staging' } }
-
- it 'returns all effective feature flags under the environment' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.second).to include({ 'name' => 'flag_1', 'active' => false })
- expect(json_response.first).to include({ 'name' => 'flag_2', 'active' => true })
- end
- end
-
- context 'when environment is review/feature X' do
- let(:params) { { environment: 'review/feature X' } }
-
- it 'returns all effective feature flags under the environment' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.second).to include({ 'name' => 'flag_1', 'active' => true })
- expect(json_response.first).to include({ 'name' => 'flag_2', 'active' => false })
- end
- end
- end
-
- describe 'GET /projects/:id/feature_flags/:name/scopes' do
- subject do
- get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes", user)
- end
-
- context 'when there are two scopes' do
- let(:feature_flag) { create_flag(project, 'test') }
- let!(:additional_scope) { create_scope(feature_flag, 'production', false) }
-
- it_behaves_like 'check user permission'
-
- it 'returns scopes of the feature flag' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag_scopes')
- expect(json_response.count).to eq(2)
- expect(json_response.first['environment_scope']).to eq(feature_flag.scopes[0].environment_scope)
- expect(json_response.second['environment_scope']).to eq(feature_flag.scopes[1].environment_scope)
- end
- end
-
- context 'when there are no feature flags' do
- let(:feature_flag) { double(:feature_flag, name: 'test') }
-
- it_behaves_like 'not found'
- end
- end
-
- describe 'POST /projects/:id/feature_flags/:name/scopes' do
- subject do
- post api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes", user),
- params: params
- end
-
- let(:params) do
- {
- environment_scope: 'staging',
- active: true,
- strategies: [{ name: 'userWithId', parameters: { 'userIds': 'a,b,c' } }].to_json
- }
- end
-
- context 'when there is a corresponding feature flag' do
- let!(:feature_flag) { create(:operations_feature_flag, project: project) }
-
- it_behaves_like 'check user permission'
-
- it 'creates a new scope' do
- subject
-
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/feature_flag_scope')
- expect(json_response['environment_scope']).to eq(params[:environment_scope])
- expect(json_response['active']).to eq(params[:active])
- expect(json_response['strategies']).to eq(Gitlab::Json.parse(params[:strategies]))
- end
-
- context 'when the scope already exists' do
- before do
- create_scope(feature_flag, params[:environment_scope])
- end
-
- it 'returns error' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include('Scopes environment scope (staging) has already been taken')
- end
- end
- end
-
- context 'when feature flag is not found' do
- let(:feature_flag) { double(:feature_flag, name: 'test') }
-
- it_behaves_like 'not found'
- end
- end
-
- describe 'GET /projects/:id/feature_flags/:name/scopes/:environment_scope' do
- subject do
- get api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes/#{environment_scope}",
- user)
- end
-
- let(:environment_scope) { scope.environment_scope }
-
- shared_examples_for 'successful response' do
- it 'returns a scope' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag_scope')
- expect(json_response['id']).to eq(scope.id)
- expect(json_response['active']).to eq(scope.active)
- expect(json_response['environment_scope']).to eq(scope.environment_scope)
- end
- end
-
- context 'when there is a feature flag' do
- let!(:feature_flag) { create(:operations_feature_flag, project: project) }
- let(:scope) { feature_flag.default_scope }
-
- it_behaves_like 'check user permission'
- it_behaves_like 'successful response'
-
- context 'when environment scope includes slash' do
- let!(:scope) { create_scope(feature_flag, 'review/*', false) }
-
- it_behaves_like 'not found'
-
- context 'when URL-encoding the environment scope parameter' do
- let(:environment_scope) { CGI.escape(scope.environment_scope) }
-
- it_behaves_like 'successful response'
- end
- end
- end
-
- context 'when there are no feature flags' do
- let(:feature_flag) { double(:feature_flag, name: 'test') }
- let(:scope) { double(:feature_flag_scope, environment_scope: 'prd') }
-
- it_behaves_like 'not found'
- end
- end
-
- describe 'PUT /projects/:id/feature_flags/:name/scopes/:environment_scope' do
- subject do
- put api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes/#{environment_scope}",
- user), params: params
- end
-
- let(:environment_scope) { scope.environment_scope }
-
- let(:params) do
- {
- active: true,
- strategies: [{ name: 'userWithId', parameters: { 'userIds': 'a,b,c' } }].to_json
- }
- end
-
- context 'when there is a corresponding feature flag' do
- let!(:feature_flag) { create(:operations_feature_flag, project: project) }
- let(:scope) { create_scope(feature_flag, 'staging', false, [{ name: "default", parameters: {} }]) }
-
- it_behaves_like 'check user permission'
-
- it 'returns the updated scope' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag_scope')
- expect(json_response['id']).to eq(scope.id)
- expect(json_response['active']).to eq(params[:active])
- expect(json_response['strategies']).to eq(Gitlab::Json.parse(params[:strategies]))
- end
-
- context 'when there are no corresponding feature flag scopes' do
- let(:scope) { double(:feature_flag_scope, environment_scope: 'prd') }
-
- it_behaves_like 'not found'
- end
- end
-
- context 'when there are no corresponding feature flags' do
- let(:feature_flag) { double(:feature_flag, name: 'test') }
- let(:scope) { double(:feature_flag_scope, environment_scope: 'prd') }
-
- it_behaves_like 'not found'
- end
- end
-
- describe 'DELETE /projects/:id/feature_flags/:name/scopes/:environment_scope' do
- subject do
- delete api("/projects/#{project.id}/feature_flags/#{feature_flag.name}/scopes/#{environment_scope}",
- user)
- end
-
- let(:environment_scope) { scope.environment_scope }
-
- shared_examples_for 'successful response' do
- it 'destroys the scope' do
- expect { subject }
- .to change { Operations::FeatureFlagScope.exists?(environment_scope: scope.environment_scope) }
- .from(true).to(false)
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
-
- context 'when there is a feature flag' do
- let!(:feature_flag) { create(:operations_feature_flag, project: project) }
-
- context 'when there is a targeted scope' do
- let!(:scope) { create_scope(feature_flag, 'production', false) }
-
- it_behaves_like 'check user permission'
- it_behaves_like 'successful response'
-
- context 'when environment scope includes slash' do
- let!(:scope) { create_scope(feature_flag, 'review/*', false) }
-
- it_behaves_like 'not found'
-
- context 'when URL-encoding the environment scope parameter' do
- let(:environment_scope) { CGI.escape(scope.environment_scope) }
-
- it_behaves_like 'successful response'
- end
- end
- end
-
- context 'when there are no targeted scopes' do
- let!(:scope) { double(:feature_flag_scope, environment_scope: 'production') }
-
- it_behaves_like 'not found'
- end
- end
-
- context 'when there are no feature flags' do
- let(:feature_flag) { double(:feature_flag, name: 'test') }
- let(:scope) { double(:feature_flag_scope, environment_scope: 'prd') }
-
- it_behaves_like 'not found'
- end
- end
-end
diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb
index dd12648f4dd..2cd52c0a5e5 100644
--- a/spec/requests/api/feature_flags_spec.rb
+++ b/spec/requests/api/feature_flags_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe API::FeatureFlags do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/feature_flags')
- expect(json_response.map { |f| f['version'] }).to eq(%w[legacy_flag legacy_flag])
+ expect(json_response.map { |f| f['version'] }).to eq(%w[new_version_flag new_version_flag])
end
it 'does not have N+1 problem' do
@@ -145,7 +145,7 @@ RSpec.describe API::FeatureFlags do
expect(response).to match_response_schema('public_api/v4/feature_flag')
expect(json_response['name']).to eq(feature_flag.name)
expect(json_response['description']).to eq(feature_flag.description)
- expect(json_response['version']).to eq('legacy_flag')
+ expect(json_response['version']).to eq('new_version_flag')
end
it_behaves_like 'check user permission'
@@ -453,210 +453,6 @@ RSpec.describe API::FeatureFlags do
end
end
- describe 'POST /projects/:id/feature_flags/:name/enable' do
- subject do
- post api("/projects/#{project.id}/feature_flags/#{params[:name]}/enable", user),
- params: params
- end
-
- let(:params) do
- {
- name: 'awesome-feature',
- environment_scope: 'production',
- strategy: { name: 'userWithId', parameters: { userIds: 'Project:1' } }.to_json
- }
- end
-
- context 'when feature flag does not exist yet' do
- it 'creates a new feature flag with the specified scope and strategy' do
- subject
-
- feature_flag = project.operations_feature_flags.last
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag')
- expect(feature_flag.name).to eq(params[:name])
- expect(scope.strategies).to eq([Gitlab::Json.parse(params[:strategy])])
- expect(feature_flag.version).to eq('legacy_flag')
- end
-
- it 'returns the flag version and strategies in the json response' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag')
- expect(json_response.slice('version', 'strategies')).to eq({
- 'version' => 'legacy_flag',
- 'strategies' => []
- })
- end
-
- it_behaves_like 'check user permission'
- end
-
- context 'when feature flag exists already' do
- let!(:feature_flag) { create_flag(project, params[:name]) }
-
- context 'when feature flag scope does not exist yet' do
- it 'creates a new scope with the specified strategy' do
- subject
-
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- expect(response).to have_gitlab_http_status(:ok)
- expect(scope.strategies).to eq([Gitlab::Json.parse(params[:strategy])])
- end
-
- it_behaves_like 'check user permission'
- end
-
- context 'when feature flag scope exists already' do
- let(:defined_strategy) { { name: 'userWithId', parameters: { userIds: 'Project:2' } } }
-
- before do
- create_scope(feature_flag, params[:environment_scope], true, [defined_strategy])
- end
-
- it 'adds an additional strategy to the scope' do
- subject
-
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- expect(response).to have_gitlab_http_status(:ok)
- expect(scope.strategies).to eq([defined_strategy.deep_stringify_keys, Gitlab::Json.parse(params[:strategy])])
- end
-
- context 'when the specified strategy exists already' do
- let(:defined_strategy) { Gitlab::Json.parse(params[:strategy]) }
-
- it 'does not add a duplicate strategy' do
- subject
-
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- strategy_count = scope.strategies.count { |strategy| strategy['name'] == 'userWithId' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(strategy_count).to eq(1)
- end
- end
- end
- end
-
- context 'with a version 2 flag' do
- let!(:feature_flag) { create(:operations_feature_flag, :new_version_flag, project: project, name: params[:name]) }
-
- it 'does not change the flag and returns an unprocessable_entity response' do
- subject
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response).to eq({ 'message' => 'Version 2 flags not supported' })
- feature_flag.reload
- expect(feature_flag.scopes).to eq([])
- expect(feature_flag.strategies).to eq([])
- end
- end
- end
-
- describe 'POST /projects/:id/feature_flags/:name/disable' do
- subject do
- post api("/projects/#{project.id}/feature_flags/#{params[:name]}/disable", user),
- params: params
- end
-
- let(:params) do
- {
- name: 'awesome-feature',
- environment_scope: 'production',
- strategy: { name: 'userWithId', parameters: { userIds: 'Project:1' } }.to_json
- }
- end
-
- context 'when feature flag does not exist yet' do
- it_behaves_like 'not found'
- end
-
- context 'when feature flag exists already' do
- let!(:feature_flag) { create_flag(project, params[:name]) }
-
- context 'when feature flag scope does not exist yet' do
- it_behaves_like 'not found'
- end
-
- context 'when feature flag scope exists already and has the specified strategy' do
- let(:defined_strategies) do
- [
- { name: 'userWithId', parameters: { userIds: 'Project:1' } },
- { name: 'userWithId', parameters: { userIds: 'Project:2' } }
- ]
- end
-
- before do
- create_scope(feature_flag, params[:environment_scope], true, defined_strategies)
- end
-
- it 'removes the strategy from the scope' do
- subject
-
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag')
- expect(scope.strategies)
- .to eq([{ name: 'userWithId', parameters: { userIds: 'Project:2' } }.deep_stringify_keys])
- end
-
- it 'returns the flag version and strategies in the json response' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/feature_flag')
- expect(json_response.slice('version', 'strategies')).to eq({
- 'version' => 'legacy_flag',
- 'strategies' => []
- })
- end
-
- it_behaves_like 'check user permission'
-
- context 'when strategies become empty array after the removal' do
- let(:defined_strategies) do
- [{ name: 'userWithId', parameters: { userIds: 'Project:1' } }]
- end
-
- it 'destroys the scope' do
- subject
-
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- expect(response).to have_gitlab_http_status(:ok)
- expect(scope).to be_nil
- end
-
- it_behaves_like 'check user permission'
- end
- end
-
- context 'when scope exists already but cannot find the corresponding strategy' do
- let(:defined_strategy) { { name: 'userWithId', parameters: { userIds: 'Project:2' } } }
-
- before do
- create_scope(feature_flag, params[:environment_scope], true, [defined_strategy])
- end
-
- it_behaves_like 'not found'
- end
- end
-
- context 'with a version 2 feature flag' do
- let!(:feature_flag) { create(:operations_feature_flag, :new_version_flag, project: project, name: params[:name]) }
-
- it 'does not change the flag and returns an unprocessable_entity response' do
- subject
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response).to eq({ 'message' => 'Version 2 flags not supported' })
- feature_flag.reload
- expect(feature_flag.scopes).to eq([])
- expect(feature_flag.strategies).to eq([])
- end
- end
- end
-
describe 'PUT /projects/:id/feature_flags/:name' do
context 'with a legacy feature flag' do
let!(:feature_flag) do
@@ -664,13 +460,13 @@ RSpec.describe API::FeatureFlags do
name: 'feature1', description: 'old description')
end
- it 'returns a 422' do
+ it 'returns a 404' do
params = { description: 'new description' }
put api("/projects/#{project.id}/feature_flags/feature1", user), params: params
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response).to eq({ 'message' => 'PUT operations are not supported for legacy feature flags' })
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response).to eq({ 'message' => '404 Not Found' })
expect(feature_flag.reload.description).to eq('old description')
end
end
@@ -984,7 +780,7 @@ RSpec.describe API::FeatureFlags do
params: params
end
- let!(:feature_flag) { create(:operations_feature_flag, project: project) }
+ let!(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project) }
let(:params) { {} }
it 'destroys the feature flag' do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 71a4a1a2784..869df06b60c 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -558,8 +558,7 @@ RSpec.describe API::Files do
get api(url, current_user), params: params
- expect(response.headers["Cache-Control"]).to include("no-store")
- expect(response.headers["Cache-Control"]).to include("no-cache")
+ expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
expect(response.headers["Pragma"]).to eq("no-cache")
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
end
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index a5e40eec919..378ee2f3e7c 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -18,7 +18,8 @@ RSpec.describe API::GenericPackages do
let_it_be(:project_deploy_token_wo) { create(:project_deploy_token, deploy_token: deploy_token_wo, project: project) }
let(:user) { personal_access_token.user }
- let(:ci_build) { create(:ci_build, :running, user: user) }
+ let(:ci_build) { create(:ci_build, :running, user: user, project: project) }
+ let(:snowplow_standard_context_params) { { user: user, project: project, namespace: project.namespace } }
def auth_header
return {} if user_role == :anonymous
diff --git a/spec/requests/api/go_proxy_spec.rb b/spec/requests/api/go_proxy_spec.rb
index e678b6cf1c8..0143340de11 100644
--- a/spec/requests/api/go_proxy_spec.rb
+++ b/spec/requests/api/go_proxy_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe API::GoProxy do
let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" }
let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user }
- let_it_be(:job) { create :ci_build, user: user, status: :running }
+ let_it_be(:job) { create :ci_build, user: user, status: :running, project: project }
let_it_be(:pa_token) { create :personal_access_token, user: user }
let_it_be(:modules) do
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index 99647d0fa3a..578a71a7272 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -47,6 +47,7 @@ RSpec.describe 'Getting Ci Cd Setting' do
expect(settings_data['mergePipelinesEnabled']).to eql project.ci_cd_settings.merge_pipelines_enabled?
expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled?
expect(settings_data['keepLatestArtifact']).to eql project.keep_latest_artifacts_available?
+ expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
end
end
end
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index 452610ab18f..31cb0393d7f 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -14,6 +14,23 @@ RSpec.describe 'getting group members information' do
[user_1, user_2].each { |user| parent_group.add_guest(user) }
end
+ context 'when a member is invited only via email' do
+ before do
+ create(:group_member, :invited, source: parent_group)
+ end
+
+ it 'returns null in the user field' do
+ fetch_members(group: parent_group, args: { relations: [:DIRECT] })
+
+ expect(graphql_errors).to be_nil
+ expect(graphql_data_at(:group, :group_members, :edges, :node)).to contain_exactly(
+ { 'user' => { 'id' => global_id_of(user_1) } },
+ { 'user' => { 'id' => global_id_of(user_2) } },
+ 'user' => nil
+ )
+ end
+ end
+
context 'when the request is correct' do
it_behaves_like 'a working graphql query' do
before do
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 601cab6aade..2b80b5239c8 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -40,6 +40,13 @@ RSpec.describe 'Milestones through GroupQuery' do
expect_array_response(milestone_2.to_global_id.to_s, milestone_3.to_global_id.to_s)
end
+
+ it 'fetches milestones between timeframe start and end arguments' do
+ today = Date.today
+ fetch_milestones(user, { timeframe: { start: today.to_s, end: (today + 2.days).to_s } })
+
+ expect_array_response(milestone_2.to_global_id.to_s, milestone_3.to_global_id.to_s)
+ end
end
context 'when filtering by state' do
diff --git a/spec/requests/api/graphql/group/timelogs_spec.rb b/spec/requests/api/graphql/group/timelogs_spec.rb
index 6e21a73afa9..05b6ee3ff89 100644
--- a/spec/requests/api/graphql/group/timelogs_spec.rb
+++ b/spec/requests/api/graphql/group/timelogs_spec.rb
@@ -17,8 +17,37 @@ RSpec.describe 'Timelogs through GroupQuery' do
let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
- before do
- group.add_developer(user)
+ context 'when the project is private' do
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:project2) { create(:project, :private, group: group2) }
+ let_it_be(:issue2) { create(:issue, project: project2) }
+ let_it_be(:timelog3) { create(:timelog, issue: issue2, spent_at: '2019-08-13 14:00:00') }
+
+ subject { post_graphql(query(full_path: group2.full_path), current_user: user) }
+
+ context 'when the user is not a member of the project' do
+ it 'returns no timelogs' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_errors).to be_nil
+ expect(timelog_array.size).to eq 0
+ end
+ end
+
+ context 'when the user is a member of the project' do
+ before do
+ project2.add_developer(user)
+ end
+
+ it 'returns timelogs' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_errors).to be_nil
+ expect(timelog_array.size).to eq 1
+ end
+ end
end
context 'when the request is correct' do
@@ -74,18 +103,6 @@ RSpec.describe 'Timelogs through GroupQuery' do
expect(timelogs_data).to be_empty
end
end
-
- context 'when user has no permission to read group timelogs' do
- it 'returns empty result' do
- guest = create(:user)
- group.add_guest(guest)
- post_graphql(query, current_user: guest)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(graphql_errors).to be_nil
- expect(timelogs_data).to be_empty
- end
- end
end
end
@@ -95,7 +112,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
end
end
- def query(timelog_params = params)
+ def query(timelog_params: params, full_path: group.full_path)
timelog_nodes = <<~NODE
nodes {
spentAt
@@ -114,7 +131,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
graphql_query_for(
:group,
- { full_path: group.full_path },
+ { full_path: full_path },
query_graphql_field(:timelogs, timelog_params, timelog_nodes)
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
index 0dcae28ac5d..0d7571d91ca 100644
--- a/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/ci_cd_settings_update_spec.rb
@@ -5,8 +5,16 @@ require 'spec_helper'
RSpec.describe 'CiCdSettingsUpdate' do
include GraphqlHelpers
- let_it_be(:project) { create(:project, keep_latest_artifact: true) }
- let(:variables) { { full_path: project.full_path, keep_latest_artifact: false } }
+ let_it_be(:project) { create(:project, keep_latest_artifact: true, ci_job_token_scope_enabled: true) }
+
+ let(:variables) do
+ {
+ full_path: project.full_path,
+ keep_latest_artifact: false,
+ job_token_scope_enabled: false
+ }
+ end
+
let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) }
context 'when unauthorized' do
@@ -45,6 +53,26 @@ RSpec.describe 'CiCdSettingsUpdate' do
expect(project.keep_latest_artifact).to eq(false)
end
+ it 'updates job_token_scope_enabled' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_job_token_scope_enabled).to eq(false)
+ end
+
+ it 'does not update job_token_scope_enabled if not specified' do
+ variables.except!(:job_token_scope_enabled)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_job_token_scope_enabled).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/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
new file mode 100644
index 00000000000..07b05ead651
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'RunnersRegistrationTokenReset' do
+ include GraphqlHelpers
+
+ let(:mutation) { graphql_mutation(:runners_registration_token_reset, input) }
+ let(:mutation_response) { graphql_mutation_response(:runners_registration_token_reset) }
+
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ shared_examples 'unauthorized' do
+ it 'returns an error' do
+ subject
+
+ expect(graphql_errors).not_to be_empty
+ 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"))
+ expect(mutation_response).to be_nil
+ end
+ end
+
+ shared_context 'when unauthorized' do |scope|
+ context 'when unauthorized' do
+ let_it_be(:user) { create(:user) }
+
+ context "when not a #{scope} member" do
+ it_behaves_like 'unauthorized'
+ end
+
+ context "with a non-admin #{scope} member" do
+ before do
+ target.add_developer(user)
+ end
+
+ it_behaves_like 'unauthorized'
+ end
+ end
+ end
+
+ shared_context 'when authorized' do |scope|
+ it 'resets runner registration token' do
+ expect { subject }.to change { get_token }
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response).not_to be_nil
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['token']).not_to be_empty
+ expect(mutation_response['token']).to eq(get_token)
+ end
+
+ context 'when malformed id is provided' do
+ let(:input) { { type: "#{scope.upcase}_TYPE", id: 'some string' } }
+
+ it 'returns errors' do
+ expect { subject }.not_to change { get_token }
+
+ expect(graphql_errors).not_to be_empty
+ expect(mutation_response).to be_nil
+ end
+ end
+ end
+
+ context 'applied to project' do
+ let_it_be(:project) { create_default(:project) }
+
+ let(:input) { { type: 'PROJECT_TYPE', id: project.to_global_id.to_s } }
+
+ include_context 'when unauthorized', 'project' do
+ let(:target) { project }
+ end
+
+ include_context 'when authorized', 'project' do
+ let_it_be(:user) { project.owner }
+
+ def get_token
+ project.reload.runners_token
+ end
+ end
+ end
+
+ context 'applied to group' do
+ let_it_be(:group) { create_default(:group) }
+
+ let(:input) { { type: 'GROUP_TYPE', id: group.to_global_id.to_s } }
+
+ include_context 'when unauthorized', 'group' do
+ let(:target) { group }
+ end
+
+ include_context 'when authorized', 'group' do
+ let_it_be(:user) { create_default(:group_member, :maintainer, user: create(:user), group: group ).user }
+
+ def get_token
+ group.reload.runners_token
+ end
+ end
+ end
+
+ context 'applied to instance' do
+ before do
+ ApplicationSetting.create_from_defaults
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
+ let(:input) { { type: 'INSTANCE_TYPE' } }
+
+ context 'when unauthorized' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'unauthorized'
+ end
+
+ include_context 'when authorized', 'instance' do
+ let_it_be(:user) { create(:user, :admin) }
+
+ def get_token
+ ApplicationSetting.current_without_cache.runners_registration_token
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/labels/create_spec.rb b/spec/requests/api/graphql/mutations/labels/create_spec.rb
index ca3ccc8e06c..28284408306 100644
--- a/spec/requests/api/graphql/mutations/labels/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/labels/create_spec.rb
@@ -11,8 +11,7 @@ RSpec.describe Mutations::Labels::Create do
{
'title' => 'foo',
'description' => 'some description',
- 'color' => '#FF0000',
- 'removeOnClose' => true
+ 'color' => '#FF0000'
}
end
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index d944c9e9e57..214c804c519 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -86,7 +86,7 @@ RSpec.describe 'Creating a Snippet' do
it 'passes disable_spam_action_service param to service' do
expect(::Snippets::CreateService)
.to receive(:new)
- .with(anything, anything, hash_including(disable_spam_action_service: true))
+ .with(project: anything, current_user: anything, params: hash_including(disable_spam_action_service: true))
.and_call_original
subject
@@ -190,7 +190,7 @@ RSpec.describe 'Creating a Snippet' do
it do
expect(::Snippets::CreateService).to receive(:new)
- .with(nil, user, hash_including(files: expected_value))
+ .with(project: nil, current_user: user, params: hash_including(files: expected_value))
.and_return(double(execute: creation_response))
subject
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 28ab593526a..77efb786dcb 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe 'Updating a Snippet' do
it 'passes disable_spam_action_service param to service' do
expect(::Snippets::UpdateService)
.to receive(:new)
- .with(anything, anything, hash_including(disable_spam_action_service: true))
+ .with(project: anything, current_user: anything, params: hash_including(disable_spam_action_service: true))
.and_call_original
subject
diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
index 705ef28ffd4..8f92105dc9c 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb
@@ -22,8 +22,8 @@ RSpec.describe 'Marking all todos done' do
graphql_mutation(:todos_mark_all_done, input,
<<-QL.strip_heredoc
clientMutationId
+ todos { id }
errors
- updatedIds
QL
)
end
@@ -40,7 +40,7 @@ RSpec.describe 'Marking all todos done' do
expect(todo3.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
- updated_todo_ids = mutation_response['updatedIds']
+ updated_todo_ids = mutation_response['todos'].map { |todo| todo['id'] }
expect(updated_todo_ids).to contain_exactly(global_id_of(todo1), global_id_of(todo3))
end
@@ -52,7 +52,7 @@ RSpec.describe 'Marking all todos done' do
expect(todo3.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('pending')
- updated_todo_ids = mutation_response['updatedIds']
+ updated_todo_ids = mutation_response['todos']
expect(updated_todo_ids).to be_empty
end
diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
index 3e96d5c5058..e71a232ff7c 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb
@@ -22,7 +22,6 @@ RSpec.describe 'Restoring many Todos' do
<<-QL.strip_heredoc
clientMutationId
errors
- updatedIds
todos {
id
state
@@ -44,7 +43,6 @@ RSpec.describe 'Restoring many Todos' do
expect(mutation_response).to include(
'errors' => be_empty,
- 'updatedIds' => match_array(input_ids),
'todos' => contain_exactly(
{ 'id' => global_id_of(todo1), 'state' => 'pending' },
{ 'id' => global_id_of(todo2), 'state' => 'pending' }
diff --git a/spec/requests/api/graphql/packages/composer_spec.rb b/spec/requests/api/graphql/packages/composer_spec.rb
index 34137a07c34..9830623ede8 100644
--- a/spec/requests/api/graphql/packages/composer_spec.rb
+++ b/spec/requests/api/graphql/packages/composer_spec.rb
@@ -3,62 +3,35 @@ require 'spec_helper'
RSpec.describe 'package details' do
include GraphqlHelpers
+ include_context 'package details setup'
- let_it_be(:project) { create(:project) }
- let_it_be(:composer_package) { create(:composer_package, project: project) }
+ let_it_be(:package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
# and avoid an error where gitaly can't find the repository
- create(:composer_metadatum, package: composer_package, target_sha: 'foo_sha', composer_json: composer_json)
+ create(:composer_metadatum, package: package, target_sha: 'foo_sha', composer_json: composer_json)
end
- let(:depth) { 3 }
- let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:metadata) { query_graphql_fragment('ComposerMetadata') }
- let(:package_files) { all_graphql_fields_for('PackageFile') }
- let(:user) { project.owner }
- let(:package_global_id) { global_id_of(composer_package) }
- let(:package_details) { graphql_data_at(:package) }
- let(:metadata_response) { graphql_data_at(:package, :metadata) }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
- let(:query) do
- graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
- #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
- metadata {
- #{metadata}
- }
- packageFiles {
- nodes {
- #{package_files}
- }
- }
- FIELDS
- end
-
subject { post_graphql(query, current_user: user) }
before do
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_behaves_like 'a package detail'
- describe 'Composer' do
- it 'has the correct metadata' do
- expect(metadata_response).to include(
- 'targetSha' => 'foo_sha',
- 'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
- )
- end
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
+ 'targetSha' => 'foo_sha',
+ 'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
+ )
+ end
- it 'does not have files' do
- expect(package_files_response).to be_empty
- end
+ it 'does not have files' do
+ expect(package_files_response).to be_empty
end
end
diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb
index dc64c5057d5..84c5af33e5d 100644
--- a/spec/requests/api/graphql/packages/conan_spec.rb
+++ b/spec/requests/api/graphql/packages/conan_spec.rb
@@ -3,26 +3,13 @@ require 'spec_helper'
RSpec.describe 'conan package details' do
include GraphqlHelpers
+ include_context 'package details setup'
- let_it_be(:project) { create(:project) }
- let_it_be(:conan_package) { create(:conan_package, project: project) }
+ let_it_be(:package) { create(:conan_package, project: project) }
- let(:package_global_id) { global_id_of(conan_package) }
let(:metadata) { query_graphql_fragment('ConanMetadata') }
- let(:first_file) { conan_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
-
- let(:depth) { 3 }
- let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
- let(:package_files) { all_graphql_fields_for('PackageFile') }
let(:package_files_metadata) {query_graphql_fragment('ConanFileMetadata')}
- let(:user) { project.owner }
- let(:package_details) { graphql_data_at(:package) }
- let(:metadata_response) { graphql_data_at(:package, :metadata) }
- let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
- let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
- let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
-
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
#{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
@@ -46,35 +33,16 @@ RSpec.describe 'conan package details' do
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_behaves_like 'a package detail'
+ it_behaves_like 'a package with files'
it 'has the correct metadata' do
expect(metadata_response).to include(
- 'id' => global_id_of(conan_package.conan_metadatum),
- 'recipe' => conan_package.conan_metadatum.recipe,
- 'packageChannel' => conan_package.conan_metadatum.package_channel,
- 'packageUsername' => conan_package.conan_metadatum.package_username,
- 'recipePath' => conan_package.conan_metadatum.recipe_path
- )
- end
-
- it 'has the right amount of files' do
- expect(package_files_response.length).to be(conan_package.package_files.length)
- end
-
- it 'has the basic package files data' do
- expect(first_file_response).to include(
- 'id' => global_id_of(first_file),
- 'fileName' => first_file.file_name,
- 'size' => first_file.size.to_s,
- 'downloadPath' => first_file.download_path,
- 'fileSha1' => first_file.file_sha1,
- 'fileMd5' => first_file.file_md5,
- 'fileSha256' => first_file.file_sha256
+ 'id' => global_id_of(package.conan_metadatum),
+ 'recipe' => package.conan_metadatum.recipe,
+ 'packageChannel' => package.conan_metadatum.package_channel,
+ 'packageUsername' => package.conan_metadatum.package_username,
+ 'recipePath' => package.conan_metadatum.recipe_path
)
end
diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb
index 8b6b5ea0986..d28d32b0df5 100644
--- a/spec/requests/api/graphql/packages/maven_spec.rb
+++ b/spec/requests/api/graphql/packages/maven_spec.rb
@@ -3,89 +3,51 @@ require 'spec_helper'
RSpec.describe 'maven package details' do
include GraphqlHelpers
+ include_context 'package details setup'
- let_it_be(:project) { create(:project) }
- let_it_be(:maven_package) { create(:maven_package, project: project) }
+ let_it_be(:package) { create(:maven_package, project: project) }
- let(:package_global_id) { global_id_of(maven_package) }
let(:metadata) { query_graphql_fragment('MavenMetadata') }
- let(:first_file) { maven_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
-
- let(:depth) { 3 }
- let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
- let(:package_files) { all_graphql_fields_for('PackageFile') }
-
- let(:user) { project.owner }
- let(:package_details) { graphql_data_at(:package) }
- let(:metadata_response) { graphql_data_at(:package, :metadata) }
- let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
- let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
-
- let(:query) do
- graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
- #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
- metadata {
- #{metadata}
- }
- packageFiles {
- nodes {
- #{package_files}
- }
- }
- FIELDS
- end
-
- subject { post_graphql(query, current_user: user) }
-
- shared_examples 'a working maven package' do
- before do
- 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
+ shared_examples 'correct maven metadata' do
it 'has the correct metadata' do
expect(metadata_response).to include(
- 'id' => global_id_of(maven_package.maven_metadatum),
- 'path' => maven_package.maven_metadatum.path,
- 'appGroup' => maven_package.maven_metadatum.app_group,
- 'appVersion' => maven_package.maven_metadatum.app_version,
- 'appName' => maven_package.maven_metadatum.app_name
+ 'id' => global_id_of(package.maven_metadatum),
+ 'path' => package.maven_metadatum.path,
+ 'appGroup' => package.maven_metadatum.app_group,
+ 'appVersion' => package.maven_metadatum.app_version,
+ 'appName' => package.maven_metadatum.app_name
)
end
+ end
- it 'has the right amount of files' do
- expect(package_files_response.length).to be(maven_package.package_files.length)
- end
+ context 'a maven package with version' do
+ subject { post_graphql(query, current_user: user) }
- it 'has the basic package files data' do
- expect(first_file_response).to include(
- 'id' => global_id_of(first_file),
- 'fileName' => first_file.file_name,
- 'size' => first_file.size.to_s,
- 'downloadPath' => first_file.download_path,
- 'fileSha1' => first_file.file_sha1,
- 'fileMd5' => first_file.file_md5,
- 'fileSha256' => first_file.file_sha256
- )
+ before do
+ subject
end
- end
- context 'a maven package with version' do
- it_behaves_like "a working maven package"
+ it_behaves_like 'a package detail'
+ it_behaves_like 'correct maven metadata'
+ it_behaves_like 'a package with files'
end
context 'a versionless maven package' do
let_it_be(:maven_metadatum) { create(:maven_metadatum, app_version: nil) }
- let_it_be(:maven_package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
+ let_it_be(:package) { create(:maven_package, project: project, version: nil, maven_metadatum: maven_metadatum) }
+
+ subject { post_graphql(query, current_user: user) }
+
+ before do
+ subject
+ end
- it_behaves_like "a working maven package"
+ it_behaves_like 'a package detail'
+ it_behaves_like 'correct maven metadata'
+ it_behaves_like 'a package with files'
- it "has an empty version" do
+ it 'has an empty version' do
subject
expect(metadata_response['appVersion']).to eq(nil)
diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb
index fa9d8a0e37e..1de16009684 100644
--- a/spec/requests/api/graphql/packages/nuget_spec.rb
+++ b/spec/requests/api/graphql/packages/nuget_spec.rb
@@ -3,37 +3,11 @@ require 'spec_helper'
RSpec.describe 'nuget package details' do
include GraphqlHelpers
+ include_context 'package details setup'
- let_it_be(:project) { create(:project) }
- let_it_be(:nuget_package) { create(:nuget_package, :with_metadatum, project: project) }
+ let_it_be(:package) { create(:nuget_package, :with_metadatum, project: project) }
- let(:package_global_id) { global_id_of(nuget_package) }
let(:metadata) { query_graphql_fragment('NugetMetadata') }
- let(:first_file) { nuget_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
-
- let(:depth) { 3 }
- let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
- let(:package_files) { all_graphql_fields_for('PackageFile') }
-
- let(:user) { project.owner }
- let(:package_details) { graphql_data_at(:package) }
- let(:metadata_response) { graphql_data_at(:package, :metadata) }
- let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
- let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
-
- let(:query) do
- graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
- #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
- metadata {
- #{metadata}
- }
- packageFiles {
- nodes {
- #{package_files}
- }
- }
- FIELDS
- end
subject { post_graphql(query, current_user: user) }
@@ -41,34 +15,15 @@ RSpec.describe 'nuget package details' do
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_behaves_like 'a package detail'
+ it_behaves_like 'a package with files'
it 'has the correct metadata' do
expect(metadata_response).to include(
- 'id' => global_id_of(nuget_package.nuget_metadatum),
- 'licenseUrl' => nuget_package.nuget_metadatum.license_url,
- 'projectUrl' => nuget_package.nuget_metadatum.project_url,
- 'iconUrl' => nuget_package.nuget_metadatum.icon_url
- )
- end
-
- it 'has the right amount of files' do
- expect(package_files_response.length).to be(nuget_package.package_files.length)
- end
-
- it 'has the basic package files data' do
- expect(first_file_response).to include(
- 'id' => global_id_of(first_file),
- 'fileName' => first_file.file_name,
- 'size' => first_file.size.to_s,
- 'downloadPath' => first_file.download_path,
- 'fileSha1' => first_file.file_sha1,
- 'fileMd5' => first_file.file_md5,
- 'fileSha256' => first_file.file_sha256
+ 'id' => global_id_of(package.nuget_metadatum),
+ 'licenseUrl' => package.nuget_metadatum.license_url,
+ 'projectUrl' => package.nuget_metadatum.project_url,
+ 'iconUrl' => package.nuget_metadatum.icon_url
)
end
end
diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb
new file mode 100644
index 00000000000..64fe7d29a7a
--- /dev/null
+++ b/spec/requests/api/graphql/packages/pypi_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'pypi package details' do
+ include GraphqlHelpers
+ include_context 'package details setup'
+
+ let_it_be(:package) { create(:pypi_package, project: project) }
+
+ let(:metadata) { query_graphql_fragment('PypiMetadata') }
+
+ subject { post_graphql(query, current_user: user) }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a package detail'
+ it_behaves_like 'a package with files'
+
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
+ 'id' => global_id_of(package.pypi_metadatum),
+ 'requiredPython' => package.pypi_metadatum.required_python
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb
index 4dfc242da80..af462c4a639 100644
--- a/spec/requests/api/graphql/project/base_service_spec.rb
+++ b/spec/requests/api/graphql/project/base_service_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'query Jira service' do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:jira_service) { create(:jira_service, project: project) }
- let_it_be(:bugzilla_service) { create(:bugzilla_service, project: project) }
+ let_it_be(:bugzilla_integration) { create(:bugzilla_integration, project: project) }
let_it_be(:redmine_service) { create(:redmine_service, project: project) }
let(:query) do
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
index c08bb8dc0a0..466464f600c 100644
--- a/spec/requests/api/graphql/project/project_members_spec.rb
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -50,6 +50,20 @@ RSpec.describe 'getting project members information' do
invited_group.add_guest(invited_user)
end
+ context 'when a member is invited only via email and current_user is a maintainer' do
+ before do
+ parent_project.add_maintainer(user)
+ create(:project_member, :invited, source: parent_project)
+ end
+
+ it 'returns null in the user field' do
+ fetch_members(project: parent_project, args: { relations: [:DIRECT] })
+
+ expect(graphql_errors).to be_nil
+ expect(graphql_data_at(:project, :project_members, :edges, :node)).to contain_exactly({ 'user' => { 'id' => global_id_of(user) } }, 'user' => nil)
+ end
+ end
+
it 'returns direct members' do
fetch_members(project: child_project, args: { relations: [:DIRECT] })
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index 43732c2ed18..8ccdb955ed9 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -295,75 +295,69 @@ RSpec.describe 'Query.project(fullPath).releases()' do
end
end
- describe 'sorting behavior' do
- let_it_be(:today) { Time.now }
- let_it_be(:yesterday) { today - 1.day }
- let_it_be(:tomorrow) { today + 1.day }
+ describe 'sorting and pagination' do
+ let_it_be(:sort_project) { create(:project, :public) }
- let_it_be(:project) { create(:project, :repository, :public) }
+ let(:data_path) { [:project, :releases] }
+ let(:current_user) { developer }
- let_it_be(:release_v1) { create(:release, project: project, tag: 'v1', released_at: yesterday, created_at: tomorrow) }
- let_it_be(:release_v2) { create(:release, project: project, tag: 'v2', released_at: today, created_at: yesterday) }
- let_it_be(:release_v3) { create(:release, project: project, tag: 'v3', released_at: tomorrow, created_at: today) }
-
- let(:current_user) { developer }
-
- let(:params) { nil }
-
- let(:sorted_tags) do
- graphql_data.dig('project', 'releases', 'nodes').map { |release| release['tagName'] }
- end
-
- let(:query) do
- graphql_query_for(:project, { fullPath: project.full_path },
- %{
- releases#{params ? "(#{params})" : ""} {
- nodes {
- tagName
- }
- }
- })
- end
-
- before do
- post_query
+ def pagination_query(params)
+ graphql_query_for(
+ :project,
+ { full_path: sort_project.full_path },
+ query_graphql_field(:releases, params, "#{page_info} nodes { tagName }")
+ )
end
- context 'when no sort: parameter is provided' do
- it 'returns the results with the default sort applied (sort: RELEASED_AT_DESC)' do
- expect(sorted_tags).to eq(%w(v3 v2 v1))
- end
+ def pagination_results_data(nodes)
+ nodes.map { |release| release['tagName'] }
end
- context 'with sort: RELEASED_AT_DESC' do
- let(:params) { 'sort: RELEASED_AT_DESC' }
-
- it 'returns the releases ordered by released_at in descending order' do
- expect(sorted_tags).to eq(%w(v3 v2 v1))
+ context 'when sorting by released_at' do
+ let_it_be(:release5) { create(:release, project: sort_project, tag: 'v5.5.0', released_at: 3.days.from_now) }
+ let_it_be(:release1) { create(:release, project: sort_project, tag: 'v5.1.0', released_at: 3.days.ago) }
+ let_it_be(:release4) { create(:release, project: sort_project, tag: 'v5.4.0', released_at: 2.days.from_now) }
+ let_it_be(:release2) { create(:release, project: sort_project, tag: 'v5.2.0', released_at: 2.days.ago) }
+ let_it_be(:release3) { create(:release, project: sort_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(:expected_results) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] }
+ end
end
- end
- context 'with sort: RELEASED_AT_ASC' do
- let(:params) { 'sort: RELEASED_AT_ASC' }
-
- it 'returns the releases ordered by released_at in ascending order' do
- expect(sorted_tags).to eq(%w(v1 v2 v3))
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :RELEASED_AT_DESC }
+ let(:first_param) { 2 }
+ let(:expected_results) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] }
+ end
end
end
- context 'with sort: CREATED_DESC' do
- let(:params) { 'sort: CREATED_DESC' }
-
- it 'returns the releases ordered by created_at in descending order' do
- expect(sorted_tags).to eq(%w(v1 v3 v2))
+ context 'when sorting by created_at' do
+ let_it_be(:release5) { create(:release, project: sort_project, tag: 'v5.5.0', created_at: 3.days.from_now) }
+ let_it_be(:release1) { create(:release, project: sort_project, tag: 'v5.1.0', created_at: 3.days.ago) }
+ let_it_be(:release4) { create(:release, project: sort_project, tag: 'v5.4.0', created_at: 2.days.from_now) }
+ let_it_be(:release2) { create(:release, project: sort_project, tag: 'v5.2.0', created_at: 2.days.ago) }
+ let_it_be(:release3) { create(:release, project: sort_project, tag: 'v5.3.0', created_at: 1.day.ago) }
+
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :CREATED_ASC }
+ let(:first_param) { 2 }
+ let(:expected_results) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] }
+ end
end
- end
-
- context 'with sort: CREATED_ASC' do
- let(:params) { 'sort: CREATED_ASC' }
- it 'returns the releases ordered by created_at in ascending order' do
- expect(sorted_tags).to eq(%w(v2 v3 v1))
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :CREATED_DESC }
+ let(:first_param) { 2 }
+ let(:expected_results) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] }
+ end
end
end
end
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index b367bbaaf43..54375d4de1d 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe 'getting project information' do
end
it 'includes topics array' do
- project.update!(tag_list: 'topic1, topic2, topic3')
+ project.update!(topic_list: 'topic1, topic2, topic3')
post_graphql(query, current_user: current_user)
@@ -119,6 +119,29 @@ RSpec.describe 'getting project information' do
end
end
+ context 'when the user has reporter access to the project' do
+ let(:statistics_query) do
+ <<~GRAPHQL
+ {
+ project(fullPath: "#{project.full_path}") {
+ statistics { wikiSize }
+ }
+ }
+ GRAPHQL
+ end
+
+ before do
+ project.add_reporter(current_user)
+ create(:project_statistics, project: project, wiki_size: 100)
+ end
+
+ it 'allows fetching project statistics' do
+ post_graphql(statistics_query, current_user: current_user)
+
+ expect(graphql_data.dig('project', 'statistics')).to include('wikiSize' => 100.0)
+ end
+ end
+
context 'when the user does not have access to the project' do
it 'returns an empty field' do
post_graphql(query, current_user: current_user)
diff --git a/spec/requests/api/group_avatar_spec.rb b/spec/requests/api/group_avatar_spec.rb
new file mode 100644
index 00000000000..be5cfbc234c
--- /dev/null
+++ b/spec/requests/api/group_avatar_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::GroupAvatar do
+ def avatar_path(group)
+ "/groups/#{group.id}/avatar"
+ end
+
+ describe 'GET /groups/:id/avatar' do
+ context 'when the group is public' do
+ it 'retrieves the avatar successfully' do
+ group = create(:group, :public, :with_avatar)
+
+ get api(avatar_path(group))
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when the group does not have avatar' do
+ it 'returns :not_found' do
+ group = create(:group, :public)
+
+ get api(avatar_path(group))
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when the group is private' do
+ let(:group) { create(:group, :private, :with_avatar) }
+
+ context 'when the user is not authenticated' do
+ it 'returns :not_found' do
+ get api(avatar_path(group))
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the the group user is authenticated' do
+ context 'and have access to the group' do
+ it 'retrieves the avatar successfully' do
+ owner = create(:user)
+ group.add_owner(owner)
+
+ get api(avatar_path(group), owner)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'and does not have access to the group' do
+ it 'returns :not_found' do
+ get api(avatar_path(group), create(:user))
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/group_container_repositories_spec.rb b/spec/requests/api/group_container_repositories_spec.rb
index 4584ef37bd0..fdbf910e4bc 100644
--- a/spec/requests/api/group_container_repositories_spec.rb
+++ b/spec/requests/api/group_container_repositories_spec.rb
@@ -33,6 +33,7 @@ RSpec.describe API::GroupContainerRepositories do
describe 'GET /groups/:id/registry/repositories' do
let(:url) { "/groups/#{group.id}/registry/repositories" }
+ let(:snowplow_gitlab_standard_context) { { user: api_user, namespace: group } }
subject { get api(url, api_user) }
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
index 8309e2ba7c1..31eef21654a 100644
--- a/spec/requests/api/group_export_spec.rb
+++ b/spec/requests/api/group_export_spec.rb
@@ -64,6 +64,23 @@ RSpec.describe API::GroupExport do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when object is not present' do
+ let(:other_group) { create(:group, :with_export) }
+ let(:other_download_path) { "/groups/#{other_group.id}/export/download" }
+
+ before do
+ other_group.add_owner(user)
+ other_group.export_file.file.delete
+ end
+
+ it 'returns 404' do
+ get api(other_download_path, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('The group export file is not available yet')
+ end
+ end
end
context 'when export file does not exist' do
@@ -215,7 +232,7 @@ RSpec.describe API::GroupExport do
context 'when export file exists' do
it 'downloads exported group archive' do
- upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/labels.ndjson.gz'))
+ upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz'))
get api(download_path, user)
diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb
index 900ffe6dfc7..c677e68b285 100644
--- a/spec/requests/api/group_labels_spec.rb
+++ b/spec/requests/api/group_labels_spec.rb
@@ -290,7 +290,7 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels", user), params: { name: group_label1.name }
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('new_name, color, description, remove_on_close are missing, '\
+ expect(json_response['error']).to eq('new_name, color, description are missing, '\
'at least one parameter must be provided')
end
end
@@ -337,7 +337,7 @@ RSpec.describe API::GroupLabels do
put api("/groups/#{group.id}/labels/#{valid_group_label_title_1_esc}", user)
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('new_name, color, description, remove_on_close are missing, '\
+ expect(json_response['error']).to eq('new_name, color, description are missing, '\
'at least one parameter must be provided')
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 1c359b6e50f..0a47b93773b 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -506,7 +506,7 @@ RSpec.describe API::Groups do
end
it "does not return a non existing group" do
- get api("/groups/1328", user1)
+ get api("/groups/#{non_existing_record_id}", user1)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -586,7 +586,7 @@ RSpec.describe API::Groups do
end
it "does not return a non existing group" do
- get api("/groups/1328", admin)
+ get api("/groups/#{non_existing_record_id}", admin)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -631,30 +631,11 @@ RSpec.describe API::Groups do
end
end
- context 'when limiting feature is enabled' do
- before do
- stub_feature_flags(limit_projects_in_groups_api: true)
- end
-
- it 'limits projects and shared_projects' do
- get api("/groups/#{group1.id}")
-
- expect(json_response['projects'].count).to eq(limit)
- expect(json_response['shared_projects'].count).to eq(limit)
- end
- end
-
- context 'when limiting feature is not enabled' do
- before do
- stub_feature_flags(limit_projects_in_groups_api: false)
- end
-
- it 'does not limit projects and shared_projects' do
- get api("/groups/#{group1.id}")
+ it 'limits projects and shared_projects' do
+ get api("/groups/#{group1.id}")
- expect(json_response['projects'].count).to eq(3)
- expect(json_response['shared_projects'].count).to eq(3)
- end
+ expect(json_response['projects'].count).to eq(limit)
+ expect(json_response['shared_projects'].count).to eq(limit)
end
end
end
@@ -748,7 +729,7 @@ RSpec.describe API::Groups do
end
it 'returns 404 for a non existing group' do
- put api('/groups/1328', user1), params: { name: new_group_name }
+ put api("/groups/#{non_existing_record_id}", user1), params: { name: new_group_name }
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -973,7 +954,7 @@ RSpec.describe API::Groups do
end
it "does not return a non existing group" do
- get api("/groups/1328/projects", user1)
+ get api("/groups/#{non_existing_record_id}/projects", user1)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -1027,7 +1008,7 @@ RSpec.describe API::Groups do
end
it "does not return a non existing group" do
- get api("/groups/1328/projects", admin)
+ get api("/groups/#{non_existing_record_id}/projects", admin)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -1686,7 +1667,7 @@ RSpec.describe API::Groups do
end
it "does not remove a non existing group" do
- delete api("/groups/1328", user1)
+ delete api("/groups/#{non_existing_record_id}", user1)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -1706,7 +1687,7 @@ RSpec.describe API::Groups do
end
it "does not remove a non existing group" do
- delete api("/groups/1328", admin)
+ delete api("/groups/#{non_existing_record_id}", admin)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb
new file mode 100644
index 00000000000..5871c0a5d5b
--- /dev/null
+++ b/spec/requests/api/helm_packages_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::HelmPackages do
+ include_context 'helm api setup'
+
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:project) { create(:project, :public) }
+ let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+ let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
+
+ describe 'GET /api/v4/projects/:id/packages/helm/:channel/charts/:file_name.tgz' do
+ let_it_be(:package) { create(:helm_package, project: project) }
+
+ let(:channel) { package.package_files.first.helm_channel }
+
+ let(:url) { "/projects/#{project.id}/packages/helm/#{channel}/charts/#{package.name}-#{package.version}.tgz" }
+
+ subject { get api(url) }
+
+ context 'with valid project' do
+ where(:visibility, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | true | 'process helm download content request' | :success
+ :public | :guest | true | true | 'process helm download content request' | :success
+ :public | :developer | true | false | 'rejects helm packages access' | :unauthorized
+ :public | :guest | true | false | 'rejects helm packages access' | :unauthorized
+ :public | :developer | false | true | 'process helm download content request' | :success
+ :public | :guest | false | true | 'process helm download content request' | :success
+ :public | :developer | false | false | 'rejects helm packages access' | :unauthorized
+ :public | :guest | false | false | 'rejects helm packages access' | :unauthorized
+ :public | :anonymous | false | true | 'process helm download content request' | :success
+ :private | :developer | true | true | 'process helm download content request' | :success
+ :private | :guest | true | true | 'rejects helm packages access' | :forbidden
+ :private | :developer | true | false | 'rejects helm packages access' | :unauthorized
+ :private | :guest | true | false | 'rejects helm packages access' | :unauthorized
+ :private | :developer | false | true | 'rejects helm packages access' | :not_found
+ :private | :guest | false | true | 'rejects helm packages access' | :not_found
+ :private | :developer | false | false | 'rejects helm packages access' | :unauthorized
+ :private | :guest | false | false | 'rejects helm packages access' | :unauthorized
+ :private | :anonymous | false | true | 'rejects helm packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility: visibility.to_s)
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ it_behaves_like 'deploy token for package GET requests'
+
+ it_behaves_like 'rejects helm access with unknown project id'
+ end
+end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 6bedd43e5c4..631698554f9 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -341,9 +341,9 @@ RSpec.describe API::Internal::Base do
end
describe "GET /internal/authorized_keys" do
- context "using an existing key's fingerprint" do
+ context "using an existing key" do
it "finds the key" do
- get(api('/internal/authorized_keys'), params: { fingerprint: key.fingerprint, secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(key.id)
@@ -351,58 +351,23 @@ RSpec.describe API::Internal::Base do
end
it 'exposes the comment of the key as a simple identifier of username + hostname' do
- get(api('/internal/authorized_keys'), params: { fingerprint: key.fingerprint, secret_token: secret_token })
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['key']).to include("#{key.user_name} (#{Gitlab.config.gitlab.host})")
end
end
- context "non existing key's fingerprint" do
- it "returns 404" do
- get(api('/internal/authorized_keys'), params: { fingerprint: "no:t-:va:li:d0", secret_token: secret_token })
+ it "returns 404 with a partial key" do
+ get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3], secret_token: secret_token })
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
- context "using a partial fingerprint" do
- it "returns 404" do
- get(api('/internal/authorized_keys'), params: { fingerprint: "#{key.fingerprint[0..5]}%", secret_token: secret_token })
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context "sending the key" do
- context "using an existing key" do
- it "finds the key" do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1], secret_token: secret_token })
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['id']).to eq(key.id)
- expect(json_response['key'].split[1]).to eq(key.key.split[1])
- end
-
- it 'exposes the comment of the key as a simple identifier of username + hostname' do
- get(api('/internal/authorized_keys'), params: { fingerprint: key.fingerprint, secret_token: secret_token })
+ it "returns 404 with an not valid base64 string" do
+ get(api('/internal/authorized_keys'), params: { key: "whatever!", secret_token: secret_token })
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['key']).to include("#{key.user_name} (#{Gitlab.config.gitlab.host})")
- end
- end
-
- it "returns 404 with a partial key" do
- get(api('/internal/authorized_keys'), params: { key: key.key.split[1][0...-3], secret_token: secret_token })
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- it "returns 404 with an not valid base64 string" do
- get(api('/internal/authorized_keys'), params: { key: "whatever!", secret_token: secret_token })
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index b0e54055854..f9f03c9e55c 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe API::Invitations do
context 'and new member is already a requester' do
it 'does not transform the requester into a proper member' do
expect do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: access_requester.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
@@ -71,7 +71,7 @@ RSpec.describe API::Invitations do
it 'invites a new member' do
expect do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: email, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
@@ -82,7 +82,7 @@ RSpec.describe API::Invitations do
expect do
email_list = [email, email2].join(',')
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: email_list, access_level: Member::DEVELOPER }
expect(response).to have_gitlab_http_status(:created)
@@ -98,7 +98,7 @@ RSpec.describe API::Invitations do
project.update!(group: group)
parent.add_developer(stranger)
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: stranger.email, access_level: Member::REPORTER }
expect(response).to have_gitlab_http_status(:created)
@@ -113,7 +113,7 @@ RSpec.describe API::Invitations do
project.update!(group: group)
parent.add_developer(stranger)
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: stranger.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
@@ -122,7 +122,7 @@ RSpec.describe API::Invitations do
context 'access expiry date' do
subject do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: email, access_level: Member::DEVELOPER, expires_at: expires_at }
end
@@ -152,8 +152,36 @@ RSpec.describe API::Invitations do
end
end
+ context 'with invite_source considerations', :snowplow do
+ let(:params) { { email: email, access_level: Member::DEVELOPER } }
+
+ it 'tracks the invite source as api' do
+ post invitations_url(source, maintainer), params: params
+
+ expect_snowplow_event(
+ category: 'Members::InviteService',
+ action: 'create_member',
+ label: 'invitations-api',
+ property: 'net_new_user',
+ user: maintainer
+ )
+ end
+
+ it 'tracks the invite source from params' do
+ post invitations_url(source, maintainer), params: params.merge(invite_source: '_invite_source_')
+
+ expect_snowplow_event(
+ category: 'Members::InviteService',
+ action: 'create_member',
+ label: '_invite_source_',
+ property: 'net_new_user',
+ user: maintainer
+ )
+ end
+ end
+
it "returns a message if member already exists" do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: developer.email, access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
@@ -161,7 +189,7 @@ RSpec.describe API::Invitations do
end
it 'returns 404 when the email is not valid' do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: '', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:created)
@@ -169,7 +197,7 @@ RSpec.describe API::Invitations do
end
it 'returns 404 when the email list is not a valid format' do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -177,14 +205,14 @@ RSpec.describe API::Invitations do
end
it 'returns 400 when email is not given' do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { access_level: Member::MAINTAINER }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 when access_level is not given' do
- post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
+ post invitations_url(source, maintainer),
params: { email: email }
expect(response).to have_gitlab_http_status(:bad_request)
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index 38c080059c4..dac721cbea0 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -402,17 +402,6 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['state']).to eq 'opened'
end
-
- it 'removes labels marked to be removed on issue closed' do
- removable_label = create(:label, project: project, remove_on_close: true)
- create(:label_link, target: issue, label: removable_label)
-
- put api_for_user, params: { state_event: 'close' }
-
- expect(issue.reload.label_ids).not_to include(removable_label.id)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['state']).to eq 'closed'
- end
end
describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index f2ceedf6dbd..26377c40b73 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -57,7 +57,7 @@ RSpec.describe API::Labels do
put_labels_api(route_type, user, spec_params)
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('new_name, color, description, priority, remove_on_close are missing, '\
+ expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\
'at least one parameter must be provided')
end
@@ -112,14 +112,6 @@ RSpec.describe API::Labels do
expect(json_response['id']).to eq(expected_response_label_id)
expect(json_response['priority']).to eq(10)
end
-
- it "returns 200 if remove_on_close is changed (#{route_type} route)" do
- put_labels_api(route_type, user, spec_params, remove_on_close: true)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['id']).to eq(expected_response_label_id)
- expect(json_response['remove_on_close']).to eq(true)
- end
end
it 'returns 200 if a priority is removed (deprecated route)' do
@@ -309,8 +301,7 @@ RSpec.describe API::Labels do
name: valid_label_title_2,
color: '#FFAABB',
description: 'test',
- priority: 2,
- remove_on_close: true
+ priority: 2
}
expect(response).to have_gitlab_http_status(:created)
@@ -318,7 +309,6 @@ RSpec.describe API::Labels do
expect(json_response['color']).to eq('#FFAABB')
expect(json_response['description']).to eq('test')
expect(json_response['priority']).to eq(2)
- expect(json_response['remove_on_close']).to eq(true)
end
it 'returns created label when only required params' do
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 4fc5fcf8282..d9f11b19e6e 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe API::MavenPackages do
include_context 'workhorse headers'
let_it_be_with_refind(:package_settings) { create(:namespace_package_setting, :group) }
- let_it_be(:group) { package_settings.namespace }
+ let_it_be_with_refind(:group) { package_settings.namespace }
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
let_it_be(:package, reload: true) { create(:maven_package, project: project, name: project.full_path) }
@@ -15,12 +15,13 @@ RSpec.describe API::MavenPackages do
let_it_be(:package_file) { package.package_files.with_file_name_like('%.xml').first }
let_it_be(:jar_file) { package.package_files.with_file_name_like('%.jar').first }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
- let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
+ let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
let(:package_name) { 'com/example/my-app' }
let(:headers) { workhorse_headers }
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
@@ -39,22 +40,73 @@ RSpec.describe API::MavenPackages do
project.add_developer(user)
end
+ shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: %i[public]|
+ context 'within a group' do
+ visibilities.each do |visibility|
+ context "that is #{visibility}" do
+ before do
+ group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
+ end
+
+ it_behaves_like shared_example_name
+ end
+ end
+ end
+
+ context 'within a subgroup' do
+ let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
+
+ before do
+ move_project_to_namespace(subgroup)
+ end
+
+ visibilities.each do |visibility|
+ context "that is #{visibility}" do
+ before do
+ subgroup.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
+ group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
+ end
+
+ it_behaves_like shared_example_name
+ end
+ end
+ end
+ end
+
+ shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: %i[public]|
+ it_behaves_like 'handling groups and subgroups for', shared_example_name, visibilities: visibilities
+
+ context 'within a user namespace' do
+ before do
+ move_project_to_namespace(user.namespace)
+ end
+
+ visibilities.each do |visibility|
+ context "that is #{visibility}" do
+ before do
+ user.namespace.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
+ end
+
+ it_behaves_like shared_example_name
+ end
+ end
+ end
+ end
+
shared_examples 'tracking the file download event' do
context 'with jar file' do
let_it_be(:package_file) { jar_file }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
end
end
shared_examples 'rejecting the request for non existing maven path' do |expected_status: :not_found|
- before do
- if Feature.enabled?(:check_maven_path_first, default_enabled: :yaml)
- expect(::Packages::Maven::PackageFinder).not_to receive(:new)
- end
- end
-
it 'rejects the request' do
+ expect(::Packages::Maven::PackageFinder).not_to receive(:new)
+
subject
expect(response).to have_gitlab_http_status(expected_status)
@@ -166,10 +218,10 @@ RSpec.describe API::MavenPackages do
end
describe 'GET /api/v4/packages/maven/*path/:file_name' do
- shared_examples 'handling all conditions' do
- context 'a public project' do
- subject { download_file(file_name: package_file.file_name) }
+ context 'a public project' do
+ subject { download_file(file_name: package_file.file_name) }
+ shared_examples 'getting a file' do
it_behaves_like 'tracking the file download event'
it 'returns the file' do
@@ -194,14 +246,18 @@ RSpec.describe API::MavenPackages do
end
end
- context 'internal project' do
- before do
- project.team.truncate
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file'
+ end
- subject { download_file_with_token(file_name: package_file.file_name) }
+ context 'internal project' do
+ before do
+ project.team.truncate
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+ subject { download_file_with_token(file_name: package_file.file_name) }
+
+ shared_examples 'getting a file' do
it_behaves_like 'tracking the file download event'
it 'returns the file' do
@@ -228,13 +284,17 @@ RSpec.describe API::MavenPackages do
end
end
- context 'private project' do
- subject { download_file_with_token(file_name: package_file.file_name) }
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal]
+ end
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'private project' do
+ subject { download_file_with_token(file_name: package_file.file_name) }
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ shared_examples 'getting a file' do
it_behaves_like 'tracking the file download event'
it 'returns the file' do
@@ -245,11 +305,13 @@ RSpec.describe API::MavenPackages do
end
it 'denies download when not enough permissions' do
- project.add_guest(user)
+ unless project.root_namespace == user.namespace
+ project.add_guest(user)
- subject
+ subject
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
it 'denies download when no private token' do
@@ -286,33 +348,19 @@ RSpec.describe API::MavenPackages do
end
end
- context 'project name is different from a package name' do
- before do
- maven_metadatum.update!(path: "wrong_name/#{package.version}")
- end
-
- it 'rejects request' do
- download_file(file_name: package_file.file_name)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: %i[public internal private]
end
- context 'with check_maven_path_first enabled' do
+ context 'project name is different from a package name' do
before do
- stub_feature_flags(check_maven_path_first: true)
+ maven_metadatum.update!(path: "wrong_name/#{package.version}")
end
- it_behaves_like 'handling all conditions'
- end
+ it 'rejects request' do
+ download_file(file_name: package_file.file_name)
- context 'with check_maven_path_first disabled' do
- before do
- stub_feature_flags(check_maven_path_first: false)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
-
- it_behaves_like 'handling all conditions'
end
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
@@ -328,14 +376,16 @@ RSpec.describe API::MavenPackages do
let(:path) { package.maven_metadatum.path }
let(:url) { "/packages/maven/#{path}/#{package_file.file_name}" }
- it_behaves_like 'processing HEAD requests', instance_level: true
+ shared_examples 'heading a file' do
+ it_behaves_like 'processing HEAD requests', instance_level: true
+ end
context 'with check_maven_path_first enabled' do
before do
stub_feature_flags(check_maven_path_first: true)
end
- it_behaves_like 'processing HEAD requests', instance_level: true
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'heading a file'
end
context 'with check_maven_path_first disabled' do
@@ -343,7 +393,7 @@ RSpec.describe API::MavenPackages do
stub_feature_flags(check_maven_path_first: false)
end
- it_behaves_like 'processing HEAD requests', instance_level: true
+ it_behaves_like 'handling groups, subgroups and user namespaces for', 'heading a file'
end
end
@@ -353,10 +403,10 @@ RSpec.describe API::MavenPackages do
group.add_developer(user)
end
- shared_examples 'handling all conditions' do
- context 'a public project' do
- subject { download_file(file_name: package_file.file_name) }
+ context 'a public project' do
+ subject { download_file(file_name: package_file.file_name) }
+ shared_examples 'getting a file for a group' do
it_behaves_like 'tracking the file download event'
it 'returns the file' do
@@ -381,14 +431,18 @@ RSpec.describe API::MavenPackages do
end
end
- context 'internal project' do
- before do
- group.group_member(user).destroy!
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group'
+ end
+
+ context 'internal project' do
+ before do
+ group.group_member(user).destroy!
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
- subject { download_file_with_token(file_name: package_file.file_name) }
+ subject { download_file_with_token(file_name: package_file.file_name) }
+ shared_examples 'getting a file for a group' do
it_behaves_like 'tracking the file download event'
it 'returns the file' do
@@ -415,13 +469,17 @@ RSpec.describe API::MavenPackages do
end
end
- context 'private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[internal public]
+ end
- subject { download_file_with_token(file_name: package_file.file_name) }
+ context 'private project' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ subject { download_file_with_token(file_name: package_file.file_name) }
+ shared_examples 'getting a file for a group' do
it_behaves_like 'tracking the file download event'
it 'returns the file' do
@@ -480,101 +538,87 @@ RSpec.describe API::MavenPackages do
it_behaves_like 'rejecting the request for non existing maven path'
end
end
+ end
- context 'with a reporter from a subgroup accessing the root group' do
- let_it_be(:root_group) { create(:group, :private) }
- let_it_be(:group) { create(:group, :private, parent: root_group) }
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: %i[private internal public]
- subject { download_file_with_token(file_name: package_file.file_name, request_headers: headers_with_token, group_id: root_group.id) }
+ context 'with a reporter from a subgroup accessing the root group' do
+ let_it_be(:root_group) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: root_group) }
- before do
- project.update!(namespace: group)
- group.add_reporter(user)
- end
+ subject { download_file_with_token(file_name: package_file.file_name, request_headers: headers_with_token, group_id: root_group.id) }
- it 'returns the file' do
- subject
+ before do
+ project.update!(namespace: group)
+ group.add_reporter(user)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
+ it 'returns the file' do
+ subject
- context 'with a non existing maven path' do
- subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
- it_behaves_like 'rejecting the request for non existing maven path'
- end
+ context 'with a non existing maven path' do
+ subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) }
+
+ it_behaves_like 'rejecting the request for non existing maven path'
end
end
+ end
- context 'maven metadata file' do
- let_it_be(:sub_group1) { create(:group, parent: group) }
- let_it_be(:sub_group2) { create(:group, parent: group) }
- let_it_be(:project1) { create(:project, :private, group: sub_group1) }
- let_it_be(:project2) { create(:project, :private, group: sub_group2) }
- let_it_be(:project3) { create(:project, :private, group: sub_group1) }
- let_it_be(:package_name) { 'foo' }
- let_it_be(:package1) { create(:maven_package, project: project1, name: package_name, version: nil) }
- let_it_be(:package_file1) { create(:package_file, :xml, package: package1, file_name: 'maven-metadata.xml') }
- let_it_be(:package2) { create(:maven_package, project: project2, name: package_name, version: nil) }
- let_it_be(:package_file2) { create(:package_file, :xml, package: package2, file_name: 'maven-metadata.xml') }
- let_it_be(:package3) { create(:maven_package, project: project3, name: package_name, version: nil) }
- let_it_be(:package_file3) { create(:package_file, :xml, package: package3, file_name: 'maven-metadata.xml') }
+ context 'maven metadata file' do
+ let_it_be(:sub_group1) { create(:group, parent: group) }
+ let_it_be(:sub_group2) { create(:group, parent: group) }
+ let_it_be(:project1) { create(:project, :private, group: sub_group1) }
+ let_it_be(:project2) { create(:project, :private, group: sub_group2) }
+ let_it_be(:project3) { create(:project, :private, group: sub_group1) }
+ let_it_be(:package_name) { 'foo' }
+ let_it_be(:package1) { create(:maven_package, project: project1, name: package_name, version: nil) }
+ let_it_be(:package_file1) { create(:package_file, :xml, package: package1, file_name: 'maven-metadata.xml') }
+ let_it_be(:package2) { create(:maven_package, project: project2, name: package_name, version: nil) }
+ let_it_be(:package_file2) { create(:package_file, :xml, package: package2, file_name: 'maven-metadata.xml') }
+ let_it_be(:package3) { create(:maven_package, project: project3, name: package_name, version: nil) }
+ let_it_be(:package_file3) { create(:package_file, :xml, package: package3, file_name: 'maven-metadata.xml') }
- let(:maven_metadatum) { package3.maven_metadatum }
+ let(:maven_metadatum) { package3.maven_metadatum }
- subject { download_file_with_token(file_name: package_file3.file_name) }
+ subject { download_file_with_token(file_name: package_file3.file_name) }
- before do
- sub_group1.add_developer(user)
- sub_group2.add_developer(user)
- # the package with the most recently published file should be returned
- create(:package_file, :xml, package: package2)
- end
+ before do
+ sub_group1.add_developer(user)
+ sub_group2.add_developer(user)
+ # the package with the most recently published file should be returned
+ create(:package_file, :xml, package: package2)
+ end
- context 'in multiple versionless packages' do
- it 'downloads the file' do
- expect(::Packages::PackageFileFinder)
- .to receive(:new).with(package2, 'maven-metadata.xml').and_call_original
+ context 'in multiple versionless packages' do
+ it 'downloads the file' do
+ expect(::Packages::PackageFileFinder)
+ .to receive(:new).with(package2, 'maven-metadata.xml').and_call_original
- subject
- end
+ subject
end
+ end
- context 'in multiple snapshot packages' do
- before do
- version = '1.0.0-SNAPSHOT'
- [package1, package2, package3].each do |pkg|
- pkg.update!(version: version)
-
- pkg.maven_metadatum.update!(path: "#{pkg.name}/#{pkg.version}")
- end
- end
-
- it 'downloads the file' do
- expect(::Packages::PackageFileFinder)
- .to receive(:new).with(package3, 'maven-metadata.xml').and_call_original
+ context 'in multiple snapshot packages' do
+ before do
+ version = '1.0.0-SNAPSHOT'
+ [package1, package2, package3].each do |pkg|
+ pkg.update!(version: version)
- subject
+ pkg.maven_metadatum.update!(path: "#{pkg.name}/#{pkg.version}")
end
end
- end
- end
- context 'with check_maven_path_first enabled' do
- before do
- stub_feature_flags(check_maven_path_first: true)
- end
-
- it_behaves_like 'handling all conditions'
- end
+ it 'downloads the file' do
+ expect(::Packages::PackageFileFinder)
+ .to receive(:new).with(package3, 'maven-metadata.xml').and_call_original
- context 'with check_maven_path_first disabled' do
- before do
- stub_feature_flags(check_maven_path_first: false)
+ subject
+ end
end
-
- it_behaves_like 'handling all conditions'
end
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path, group_id: group.id)
@@ -595,7 +639,7 @@ RSpec.describe API::MavenPackages do
stub_feature_flags(check_maven_path_first: true)
end
- it_behaves_like 'processing HEAD requests'
+ it_behaves_like 'handling groups and subgroups for', 'processing HEAD requests'
end
context 'with check_maven_path_first disabled' do
@@ -603,95 +647,77 @@ RSpec.describe API::MavenPackages do
stub_feature_flags(check_maven_path_first: false)
end
- it_behaves_like 'processing HEAD requests'
+ it_behaves_like 'handling groups and subgroups for', 'processing HEAD requests'
end
end
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
- shared_examples 'handling all conditions' do
- context 'a public project' do
- subject { download_file(file_name: package_file.file_name) }
-
- it_behaves_like 'tracking the file download event'
-
- it 'returns the file' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
+ context 'a public project' do
+ subject { download_file(file_name: package_file.file_name) }
- it 'returns sha1 of the file' do
- download_file(file_name: package_file.file_name + '.sha1')
+ it_behaves_like 'tracking the file download event'
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('text/plain')
- expect(response.body).to eq(package_file.file_sha1)
- end
-
- context 'with a non existing maven path' do
- subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
+ it 'returns the file' do
+ subject
- it_behaves_like 'rejecting the request for non existing maven path'
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
end
- context 'private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ it 'returns sha1 of the file' do
+ download_file(file_name: package_file.file_name + '.sha1')
- subject { download_file_with_token(file_name: package_file.file_name) }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('text/plain')
+ expect(response.body).to eq(package_file.file_sha1)
+ end
- it_behaves_like 'tracking the file download event'
+ context 'with a non existing maven path' do
+ subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it 'returns the file' do
- subject
+ it_behaves_like 'rejecting the request for non existing maven path'
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
- end
+ context 'private project' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
- it 'denies download when not enough permissions' do
- project.add_guest(user)
+ subject { download_file_with_token(file_name: package_file.file_name) }
- subject
+ it_behaves_like 'tracking the file download event'
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it 'returns the file' do
+ subject
- it 'denies download when no private token' do
- download_file(file_name: package_file.file_name)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it 'denies download when not enough permissions' do
+ project.add_guest(user)
- it_behaves_like 'downloads with a job token'
+ subject
- it_behaves_like 'downloads with a deploy token'
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
- context 'with a non existing maven path' do
- subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
+ it 'denies download when no private token' do
+ download_file(file_name: package_file.file_name)
- it_behaves_like 'rejecting the request for non existing maven path'
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
- end
- context 'with check_maven_path_first enabled' do
- before do
- stub_feature_flags(check_maven_path_first: true)
- end
+ it_behaves_like 'downloads with a job token'
- it_behaves_like 'handling all conditions'
- end
+ it_behaves_like 'downloads with a deploy token'
- context 'with check_maven_path_first disabled' do
- before do
- stub_feature_flags(check_maven_path_first: false)
- end
+ context 'with a non existing maven path' do
+ subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
- it_behaves_like 'handling all conditions'
+ it_behaves_like 'rejecting the request for non existing maven path'
+ end
end
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
@@ -1020,4 +1046,10 @@ RSpec.describe API::MavenPackages do
upload_file(params: params, request_headers: request_headers, file_extension: file_extension)
end
end
+
+ def move_project_to_namespace(namespace)
+ project.update!(namespace: namespace)
+ package.update!(name: project.full_path)
+ maven_metadatum.update!(path: "#{package.name}/#{package.version}")
+ end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index d488aee0c10..cac1b95e854 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -255,11 +255,41 @@ RSpec.describe API::Members do
expect(json_response['access_level']).to eq(Member::DEVELOPER)
end
- describe 'executes the Members::CreateService for multiple user_ids' do
+ context 'with invite_source considerations', :snowplow do
+ let(:params) { { user_id: stranger.id, access_level: Member::DEVELOPER } }
+
+ it 'tracks the invite source as api' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: params
+
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: 'members-api',
+ property: 'existing_user',
+ user: maintainer
+ )
+ end
+
+ it 'tracks the invite source from params' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: params.merge(invite_source: '_invite_source_')
+
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: '_invite_source_',
+ property: 'existing_user',
+ user: maintainer
+ )
+ end
+ end
+
+ context 'when executing the Members::CreateService for multiple user_ids' do
+ let(:user_ids) { [stranger.id, access_requester.id].join(',') }
+
it 'returns success when it successfully create all members' do
expect do
- user_ids = [stranger.id, access_requester.id].join(',')
-
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: user_ids, access_level: Member::DEVELOPER }
@@ -270,8 +300,6 @@ RSpec.describe API::Members do
it 'returns the error message if there was an error adding members to group' do
error_message = 'Unable to find User ID'
- user_ids = [stranger.id, access_requester.id].join(',')
-
allow_next_instance_of(::Members::CreateService) do |service|
expect(service).to receive(:execute).and_return({ status: :error, message: error_message })
end
@@ -283,6 +311,36 @@ RSpec.describe API::Members do
expect(json_response['status']).to eq('error')
expect(json_response['message']).to eq(error_message)
end
+
+ context 'with invite_source considerations', :snowplow do
+ let(:params) { { user_id: user_ids, access_level: Member::DEVELOPER } }
+
+ it 'tracks the invite source as api' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: params
+
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: 'members-api',
+ property: 'existing_user',
+ user: maintainer
+ )
+ end
+
+ it 'tracks the invite source from params' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: params.merge(invite_source: '_invite_source_')
+
+ expect_snowplow_event(
+ category: 'Members::CreateService',
+ action: 'create_member',
+ label: '_invite_source_',
+ property: 'existing_user',
+ user: maintainer
+ )
+ end
+ end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index a13db1bb414..038c3bc552a 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1186,7 +1186,8 @@ RSpec.describe API::MergeRequests do
expect(json_response['downvotes']).to eq(1)
expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
- expect(json_response['work_in_progress']).to be_falsy
+ expect(json_response['draft']).to be false
+ expect(json_response['work_in_progress']).to be false
expect(json_response['merge_when_pipeline_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged')
expect(json_response['should_close_merge_request']).to be_falsy
@@ -1329,29 +1330,30 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:not_found)
end
- context 'Work in Progress' do
- let!(:merge_request_wip) do
+ context 'Draft' do
+ let!(:merge_request_draft) do
create(:merge_request,
author: user,
assignees: [user],
source_project: project,
target_project: project,
- title: "WIP: Test",
+ title: "Draft: Test",
created_at: base_time + 1.second
)
end
it "returns merge request" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request_draft.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['draft']).to eq(true)
expect(json_response['work_in_progress']).to eq(true)
end
end
context 'when a merge request has more than the changes limit' do
it "returns a string indicating that more changes were made" do
- allow(Commit).to receive(:diff_hard_limit_files).and_return(5)
+ allow(Commit).to receive(:diff_max_files).and_return(5)
merge_request_overflow = create(:merge_request, :simple,
author: user,
@@ -2174,6 +2176,12 @@ RSpec.describe API::MergeRequests do
a_hash_including('name' => user2.name)
)
end
+
+ it 'creates appropriate system notes', :sidekiq_inline do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(merge_request.notes.system.last.note).to include("assigned to #{user2.to_reference}")
+ end
end
context 'when assignee_id=user2.id' do
@@ -2193,6 +2201,27 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'when assignee_id=0' do
+ let(:params) do
+ {
+ assignee_id: 0
+ }
+ end
+
+ it 'clears the assignees' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['assignees']).to be_empty
+ end
+
+ it 'creates appropriate system notes', :sidekiq_inline do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+
+ expect(merge_request.notes.system.last.note).to include("unassigned #{user.to_reference}")
+ end
+ end
+
context 'when only assignee_ids are provided, and the list is empty' do
let(:params) do
{
@@ -2495,8 +2524,8 @@ RSpec.describe API::MergeRequests do
expect(json_response['message']).to eq('405 Method Not Allowed')
end
- it "returns 405 if merge_request is a work in progress" do
- merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
+ it "returns 405 if merge_request is a draft" do
+ merge_request.update_attribute(:title, "Draft: #{merge_request.title}")
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
expect(response).to have_gitlab_http_status(:method_not_allowed)
expect(json_response['message']).to eq('405 Method Not Allowed')
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 10271719a15..ab74da4bda4 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -71,12 +71,14 @@ RSpec.describe API::NpmProjectPackages do
end
context 'a public project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
context 'with a job token for a different user' do
let_it_be(:other_user) { create(:user) }
- let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) }
+ let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user, project: project) }
let(:headers) { build_token_auth_header(other_job.token) }
@@ -161,6 +163,7 @@ RSpec.describe API::NpmProjectPackages do
context 'valid package record' do
let(:params) { upload_params(package_name: package_name) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
shared_examples 'handling upload with different authentications' do
context 'with access token' do
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index aefbc89dc3b..1b71f0f9de1 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -46,6 +46,7 @@ RSpec.describe API::NugetGroupPackages do
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: subgroup) }
let(:target) { subgroup }
+ let(:snowplow_gitlab_standard_context) { { namespace: subgroup } }
it_behaves_like 'handling all endpoints'
@@ -57,6 +58,7 @@ RSpec.describe API::NugetGroupPackages do
context 'a group' do
let(:target) { group }
+ let(:snowplow_gitlab_standard_context) { { namespace: group } }
it_behaves_like 'handling all endpoints'
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
index 54fe0b985df..572736cfc86 100644
--- a/spec/requests/api/nuget_project_packages_spec.rb
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe API::NugetProjectPackages do
describe 'GET /api/v4/projects/:id/packages/nuget' do
it_behaves_like 'handling nuget service requests' do
let(:url) { "/projects/#{target.id}/packages/nuget/index.json" }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
end
end
@@ -34,6 +35,7 @@ RSpec.describe API::NugetProjectPackages do
describe 'GET /api/v4/projects/:id/packages/nuget/query' do
it_behaves_like 'handling nuget search requests' do
let(:url) { "/projects/#{target.id}/packages/nuget/query?#{query_parameters.to_query}" }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
end
end
@@ -121,6 +123,7 @@ RSpec.describe API::NugetProjectPackages do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
subject { get api(url), headers: headers }
@@ -189,7 +192,7 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'deploy token for package uploads'
it_behaves_like 'job token for package uploads', authorize_endpoint: true do
- let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
end
it_behaves_like 'rejects nuget access with unknown target id'
@@ -244,6 +247,7 @@ RSpec.describe API::NugetProjectPackages do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
@@ -256,7 +260,7 @@ RSpec.describe API::NugetProjectPackages do
it_behaves_like 'deploy token for package uploads'
it_behaves_like 'job token for package uploads' do
- let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
end
it_behaves_like 'rejects nuget access with unknown target id'
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index d28442bd692..2932447f663 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -69,6 +69,7 @@ itself: # project
- shared_with_groups
- ssh_url_to_repo
- tag_list
+ - topics
- web_url
build_auto_devops: # auto_devops
@@ -86,13 +87,13 @@ ci_cd_settings:
- id
- project_id
- group_runners_enabled
- - keep_latest_artifact
- merge_pipelines_enabled
- merge_trains_enabled
- auto_rollback_enabled
remapped_attributes:
default_git_depth: ci_default_git_depth
forward_deployment_enabled: ci_forward_deployment_enabled
+ job_token_scope_enabled: ci_job_token_scope_enabled
build_import_state: # import_state
unexposed_attributes:
@@ -139,7 +140,6 @@ project_setting:
- project_id
- push_rule_id
- show_default_award_emojis
- - squash_option
- updated_at
- cve_id_request_enabled
- mr_default_target_self
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index f3da99573fe..695d2c3fe2c 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -32,6 +32,8 @@ RSpec.describe API::ProjectContainerRepositories do
let(:method) { :get }
let(:params) { {} }
+ let(:snowplow_gitlab_standard_context) { { user: api_user, project: project, namespace: project.namespace } }
+
before_all do
project.add_maintainer(maintainer)
project.add_developer(developer)
@@ -405,7 +407,7 @@ RSpec.describe API::ProjectContainerRepositories do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag', project: project, user: api_user, namespace: project.namespace)
end
end
@@ -421,7 +423,7 @@ RSpec.describe API::ProjectContainerRepositories do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect_snowplow_event(category: described_class.name, action: 'delete_tag')
+ expect_snowplow_event(category: described_class.name, action: 'delete_tag', project: project, user: api_user, namespace: project.namespace)
end
end
end
diff --git a/spec/requests/api/project_debian_distributions_spec.rb b/spec/requests/api/project_debian_distributions_spec.rb
new file mode 100644
index 00000000000..de7362758f7
--- /dev/null
+++ b/spec/requests/api/project_debian_distributions_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::ProjectDebianDistributions do
+ include HttpBasicAuthHelpers
+ include WorkhorseHelpers
+
+ include_context 'Debian repository shared context', :project, true do
+ describe 'POST projects/:id/debian_distributions' do
+ let(:method) { :post }
+ let(:url) { "/projects/#{container.id}/debian_distributions" }
+ let(:api_params) { { 'codename': 'my-codename' } }
+
+ it_behaves_like 'Debian repository write endpoint', 'POST distribution request', :created, /^{.*"codename":"my-codename",.*"components":\["main"\],.*"architectures":\["all","amd64"\]/, authenticate_non_public: false
+
+ context 'with invalid parameters' do
+ let(:api_params) { { codename: distribution.codename } }
+
+ it_behaves_like 'Debian repository write endpoint', 'GET request', :bad_request, /^{"message":{"codename":\["has already been taken"\]}}$/, authenticate_non_public: false
+ end
+ end
+
+ describe 'GET projects/:id/debian_distributions' do
+ let(:url) { "/projects/#{container.id}/debian_distributions" }
+
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^\[{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false
+ end
+
+ describe 'GET projects/:id/debian_distributions/:codename' do
+ let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
+
+ it_behaves_like 'Debian repository read endpoint', 'GET request', :success, /^{.*"codename":"existing-codename\",.*"components":\["existing-component"\],.*"architectures":\["all","existing-arch"\]/, authenticate_non_public: false
+ end
+
+ describe 'PUT projects/:id/debian_distributions/:codename' do
+ let(:method) { :put }
+ let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
+ let(:api_params) { { suite: 'my-suite' } }
+
+ it_behaves_like 'Debian repository write endpoint', 'PUT distribution request', :success, /^{.*"codename":"existing-codename",.*"suite":"my-suite",/, authenticate_non_public: false
+
+ context 'with invalid parameters' do
+ let(:api_params) { { suite: distribution.codename } }
+
+ it_behaves_like 'Debian repository write endpoint', 'GET request', :bad_request, /^{"message":{"suite":\["has already been taken as Codename"\]}}$/, authenticate_non_public: false
+ end
+ end
+
+ describe 'DELETE projects/:id/debian_distributions/:codename' do
+ let(:method) { :delete }
+ let(:url) { "/projects/#{container.id}/debian_distributions/#{distribution.codename}" }
+
+ it_behaves_like 'Debian repository maintainer write endpoint', 'DELETE distribution request', :success, /^{\"message\":\"202 Accepted\"}$/, authenticate_non_public: false
+
+ context 'when destroy fails' do
+ before do
+ allow_next_found_instance_of(::Packages::Debian::ProjectDistribution) do |distribution|
+ expect(distribution).to receive(:destroy).and_return(false)
+ end
+ end
+
+ it_behaves_like 'Debian repository maintainer write endpoint', 'GET request', :bad_request, /^{"message":"Failed to delete distribution"}$/, authenticate_non_public: false
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index ac24aeee52c..06f4475ef79 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -196,6 +196,19 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
end
end
+ context 'when export object is not present' do
+ before do
+ project_after_export.export_file.file.delete
+ end
+
+ it 'returns 404' do
+ get api(download_path_export_action, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('The project export file is not available yet')
+ end
+ end
+
context 'when upload complete' do
before do
project_after_export.remove_exports
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index fb1aa65c08d..5886f293f41 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -40,10 +40,36 @@ RSpec.describe API::ProjectPackages do
context 'with terraform module package' do
let_it_be(:terraform_module_package) { create(:terraform_module_package, project: project) }
- it 'filters out terraform module packages when no package_type filter is set' do
- subject
+ context 'when no package_type filter is set' do
+ let(:params) { {} }
+
+ it 'filters out terraform module packages' do
+ subject
+
+ expect(json_response).not_to include(a_hash_including('package_type' => 'terraform_module'))
+ end
+
+ it 'returns packages with the package registry web_path' do
+ subject
+
+ expect(json_response).to include(a_hash_including('_links' => a_hash_including('web_path' => include('packages'))))
+ end
+ end
+
+ context 'when package_type filter is set to terraform_module' do
+ let(:params) { { package_type: :terraform_module } }
- expect(json_response).not_to include(a_hash_including('package_type' => 'terraform_module'))
+ it 'returns the terraform module package' do
+ subject
+
+ expect(json_response).to include(a_hash_including('package_type' => 'terraform_module'))
+ end
+
+ it 'returns the terraform module package with the infrastructure registry web_path' do
+ subject
+
+ expect(json_response).to include(a_hash_including('_links' => a_hash_including('web_path' => include('infrastructure_registry'))))
+ end
end
end
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
index b40645ba2de..5b272121233 100644
--- a/spec/requests/api/project_repository_storage_moves_spec.rb
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::ProjectRepositoryStorageMoves do
it_behaves_like 'repository_storage_moves API', 'projects' do
- let_it_be(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
+ let_it_be(:container) { create(:project, :repository) }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :project_repository_storage_move }
let(:bulk_worker_klass) { Projects::ScheduleBulkRepositoryShardMovesWorker }
diff --git a/spec/requests/api/project_statistics_spec.rb b/spec/requests/api/project_statistics_spec.rb
index 5f0cac403aa..d314af0746a 100644
--- a/spec/requests/api/project_statistics_spec.rb
+++ b/spec/requests/api/project_statistics_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe API::ProjectStatistics do
- let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
let_it_be(:public_project) { create(:project, :public) }
before do
- public_project.add_developer(developer)
+ public_project.add_reporter(reporter)
end
describe 'GET /projects/:id/statistics' do
@@ -19,7 +19,7 @@ RSpec.describe API::ProjectStatistics do
let_it_be(:fetch_statistics_other_project) { create(:project_daily_statistic, project: create(:project), fetch_count: 29, date: 29.days.ago) }
it 'returns the fetch statistics of the last 30 days' do
- get api("/projects/#{public_project.id}/statistics", developer)
+ get api("/projects/#{public_project.id}/statistics", reporter)
expect(response).to have_gitlab_http_status(:ok)
fetches = json_response['fetches']
@@ -32,7 +32,7 @@ RSpec.describe API::ProjectStatistics do
it 'excludes the fetch statistics older than 30 days' do
create(:project_daily_statistic, fetch_count: 31, project: public_project, date: 30.days.ago)
- get api("/projects/#{public_project.id}/statistics", developer)
+ get api("/projects/#{public_project.id}/statistics", reporter)
expect(response).to have_gitlab_http_status(:ok)
fetches = json_response['fetches']
@@ -41,7 +41,7 @@ RSpec.describe API::ProjectStatistics do
expect(fetches['days'].last).to eq({ 'count' => fetch_statistics1.fetch_count, 'date' => fetch_statistics1.date.to_s })
end
- it 'responds with 403 when the user is not a developer of the repository' do
+ it 'responds with 403 when the user is not a reporter of the repository' do
guest = create(:user)
public_project.add_guest(guest)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 7f804186bc7..e7e26c34a83 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe API::Projects do
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:admin) { create(:admin) }
- let_it_be(:project, reload: true) { create(:project, :repository, namespace: user.namespace) }
+ let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace) }
let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) }
let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let_it_be(:user4) { create(:user, username: 'user.with.dot') }
@@ -109,6 +109,43 @@ RSpec.describe API::Projects do
end
end
+ shared_examples_for 'create project with default branch parameter' do
+ let(:params) { { name: 'Foo Project', initialize_with_readme: true, default_branch: default_branch } }
+ let(:default_branch) { 'main' }
+
+ it 'creates project with provided default branch name' do
+ expect { request }.to change { Project.count }.by(1)
+ expect(response).to have_gitlab_http_status(:created)
+
+ project = Project.find(json_response['id'])
+ expect(project.default_branch).to eq(default_branch)
+ end
+
+ context 'when branch name is empty' do
+ let(:default_branch) { '' }
+
+ it 'creates project with a default project branch name' do
+ expect { request }.to change { Project.count }.by(1)
+ expect(response).to have_gitlab_http_status(:created)
+
+ project = Project.find(json_response['id'])
+ expect(project.default_branch).to eq('master')
+ end
+ end
+
+ context 'when initialize with readme is not set' do
+ let(:params) { super().merge(initialize_with_readme: nil) }
+
+ it 'creates project with a default project branch name' do
+ expect { request }.to change { Project.count }.by(1)
+ expect(response).to have_gitlab_http_status(:created)
+
+ project = Project.find(json_response['id'])
+ expect(project.default_branch).to be_nil
+ end
+ end
+ end
+
describe 'GET /projects' do
shared_examples_for 'projects response' do
it 'returns an array of projects' do
@@ -184,13 +221,40 @@ RSpec.describe API::Projects do
end
end
- it 'includes the project labels as the tag_list' do
+ it 'includes correct value of container_registry_enabled', :aggregate_failures do
+ project.update_column(:container_registry_enabled, true)
+ project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
+
+ get api('/projects', user)
+ project_response = json_response.find { |p| p['id'] == project.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(project_response['container_registry_enabled']).to eq(false)
+ end
+
+ it 'reads projects.container_registry_enabled when read_container_registry_access_level is disabled' do
+ stub_feature_flags(read_container_registry_access_level: false)
+
+ project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
+ project.update_column(:container_registry_enabled, true)
+
+ get api('/projects', user)
+ project_response = json_response.find { |p| p['id'] == project.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(project_response['container_registry_enabled']).to eq(true)
+ end
+
+ it 'includes project topics' do
get api('/projects', user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.first.keys).to include('tag_list')
+ expect(json_response.first.keys).to include('tag_list') # deprecated in favor of 'topics'
+ expect(json_response.first.keys).to include('topics')
end
it 'includes open_issues_count' do
@@ -223,9 +287,9 @@ RSpec.describe API::Projects do
expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
end
- context 'filter by topic (column tag_list)' do
+ context 'filter by topic (column topic_list)' do
before do
- project.update!(tag_list: %w(ruby javascript))
+ project.update!(topic_list: %w(ruby javascript))
end
it 'returns no projects' do
@@ -742,10 +806,6 @@ RSpec.describe API::Projects do
it 'includes a pagination header with link to the next page' do
get api('/projects', current_user), params: params
- expect(response.header).to include('Links')
- expect(response.header['Links']).to include('pagination=keyset')
- expect(response.header['Links']).to include("id_after=#{first_project_id}")
-
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
expect(response.header['Link']).to include("id_after=#{first_project_id}")
@@ -762,10 +822,6 @@ RSpec.describe API::Projects do
it 'still includes a link if the end has reached and there is no more data after this page' do
get api('/projects', current_user), params: params.merge(id_after: project2.id)
- expect(response.header).to include('Links')
- expect(response.header['Links']).to include('pagination=keyset')
- expect(response.header['Links']).to include("id_after=#{project3.id}")
-
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
expect(response.header['Link']).to include("id_after=#{project3.id}")
@@ -774,7 +830,6 @@ RSpec.describe API::Projects do
it 'does not include a next link when the page does not have any records' do
get api('/projects', current_user), params: params.merge(id_after: Project.maximum(:id))
- expect(response.header).not_to include('Links')
expect(response.header).not_to include('Link')
end
@@ -798,10 +853,6 @@ RSpec.describe API::Projects do
it 'includes a pagination header with link to the next page' do
get api('/projects', current_user), params: params
- expect(response.header).to include('Links')
- expect(response.header['Links']).to include('pagination=keyset')
- expect(response.header['Links']).to include("id_before=#{last_project_id}")
-
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
expect(response.header['Link']).to include("id_before=#{last_project_id}")
@@ -828,11 +879,6 @@ RSpec.describe API::Projects do
requests += 1
get api(url, current_user), params: params
- links = response.header['Links']
- url = links&.match(/<[^>]+(\/projects\?[^>]+)>; rel="next"/) do |match|
- match[1]
- end
-
link = response.header['Link']
url = link&.match(/<[^>]+(\/projects\?[^>]+)>; rel="next"/) do |match|
match[1]
@@ -938,6 +984,10 @@ RSpec.describe API::Projects do
expect(project.path).to eq('path-project-Foo')
end
+ it_behaves_like 'create project with default branch parameter' do
+ let(:request) { post api('/projects', user), params: params }
+ end
+
it 'creates last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
post api('/projects', user2), params: { name: 'foo' }
@@ -1050,12 +1100,20 @@ RSpec.describe API::Projects do
expect(json_response['readme_url']).to eql("#{Gitlab.config.gitlab.url}/#{json_response['namespace']['full_path']}/somewhere/-/blob/master/README.md")
end
- it 'sets tag list to a project' do
+ it 'sets tag list to a project (deprecated)' do
project = attributes_for(:project, tag_list: %w[tagFirst tagSecond])
post api('/projects', user), params: project
- expect(json_response['tag_list']).to eq(%w[tagFirst tagSecond])
+ expect(json_response['topics']).to eq(%w[tagFirst tagSecond])
+ end
+
+ it 'sets topics to a project' do
+ project = attributes_for(:project, topics: %w[topic1 topics2])
+
+ post api('/projects', user), params: project
+
+ expect(json_response['topics']).to eq(%w[topic1 topics2])
end
it 'uploads avatar for project a project' do
@@ -1410,6 +1468,10 @@ RSpec.describe API::Projects do
expect(project.path).to eq('path-project-Foo')
end
+ it_behaves_like 'create project with default branch parameter' do
+ let(:request) { post api("/projects/user/#{user.id}", admin), params: params }
+ end
+
it 'responds with 400 on failure and not project' do
expect { post api("/projects/user/#{user.id}", admin) }
.not_to change { Project.count }
@@ -1910,7 +1972,8 @@ RSpec.describe API::Projects do
expect(json_response['id']).to eq(project.id)
expect(json_response['description']).to eq(project.description)
expect(json_response['default_branch']).to eq(project.default_branch)
- expect(json_response['tag_list']).to be_an Array
+ expect(json_response['tag_list']).to be_an Array # deprecated in favor of 'topics'
+ expect(json_response['topics']).to be_an Array
expect(json_response['archived']).to be_falsey
expect(json_response['visibility']).to be_present
expect(json_response['ssh_url_to_repo']).to be_present
@@ -1987,7 +2050,8 @@ RSpec.describe API::Projects do
expect(json_response['id']).to eq(project.id)
expect(json_response['description']).to eq(project.description)
expect(json_response['default_branch']).to eq(project.default_branch)
- expect(json_response['tag_list']).to be_an Array
+ expect(json_response['tag_list']).to be_an Array # deprecated in favor of 'topics'
+ expect(json_response['topics']).to be_an Array
expect(json_response['archived']).to be_falsey
expect(json_response['visibility']).to be_present
expect(json_response['ssh_url_to_repo']).to be_present
@@ -2043,8 +2107,10 @@ RSpec.describe API::Projects do
expect(json_response['ci_default_git_depth']).to eq(project.ci_default_git_depth)
expect(json_response['ci_forward_deployment_enabled']).to eq(project.ci_forward_deployment_enabled)
expect(json_response['merge_method']).to eq(project.merge_method.to_s)
+ expect(json_response['squash_option']).to eq(project.squash_option.to_s)
expect(json_response['readme_url']).to eq(project.readme_url)
expect(json_response).to have_key 'packages_enabled'
+ expect(json_response['keep_latest_artifact']).to be_present
end
it 'returns a group link with expiration date' do
@@ -2755,7 +2821,7 @@ RSpec.describe API::Projects do
expect(project.project_group_links).to be_empty
end
- it 'updates project authorization' do
+ it 'updates project authorization', :sidekiq_inline do
expect do
delete api("/projects/#{project.id}/share/#{group.id}", user)
end.to(
@@ -2902,6 +2968,18 @@ RSpec.describe API::Projects do
end
end
+ it 'updates default_branch' do
+ project_param = { default_branch: 'something_else' }
+
+ put api("/projects/#{project.id}", user), params: project_param
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
it 'updates jobs_enabled' do
project_param = { jobs_enabled: true }
@@ -3027,6 +3105,26 @@ RSpec.describe API::Projects do
expect(json_response['auto_devops_enabled']).to eq(false)
end
+
+ it 'updates topics using tag_list (deprecated)' do
+ project_param = { tag_list: 'topic1' }
+
+ put api("/projects/#{project3.id}", user), params: project_param
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response['topics']).to eq(%w[topic1])
+ end
+
+ it 'updates topics' do
+ project_param = { topics: 'topic2' }
+
+ put api("/projects/#{project3.id}", user), params: project_param
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response['topics']).to eq(%w[topic2])
+ end
end
context 'when authenticated as project maintainer' do
@@ -3203,6 +3301,24 @@ RSpec.describe API::Projects do
expect { subject }.to change { project.reload.service_desk_enabled }.to(true)
end
end
+
+ context 'when updating keep latest artifact' do
+ subject { put(api("/projects/#{project.id}", user), params: { keep_latest_artifact: true }) }
+
+ before do
+ project.update!(keep_latest_artifact: false)
+ end
+
+ it 'returns 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'enables keep_latest_artifact' do
+ expect { subject }.to change { project.reload.keep_latest_artifact }.to(true)
+ end
+ end
end
describe 'POST /projects/:id/archive' do
@@ -3882,6 +3998,48 @@ RSpec.describe API::Projects do
end
end
+ describe 'GET /projects/:id/storage' do
+ context 'when unauthenticated' do
+ it 'does not return project storage data' do
+ get api("/projects/#{project.id}/storage")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ it 'returns project storage data when user is admin' do
+ get api("/projects/#{project.id}/storage", create(:admin))
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['project_id']).to eq(project.id)
+ expect(json_response['disk_path']).to eq(project.repository.disk_path)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['repository_storage']).to eq(project.repository_storage)
+ end
+
+ it 'does not return project storage data when user is not admin' do
+ get api("/projects/#{project.id}/storage", user3)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'responds with a 401 for unauthenticated users trying to access a non-existent project id' do
+ expect(Project.find_by(id: non_existing_record_id)).to be_nil
+
+ get api("/projects/#{non_existing_record_id}/storage")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with a 403 for non-admin users trying to access a non-existent project id' do
+ expect(Project.find_by(id: non_existing_record_id)).to be_nil
+
+ get api("/projects/#{non_existing_record_id}/storage", user3)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
it_behaves_like 'custom attributes endpoints', 'projects' do
let(:attributable) { project }
let(:other_attributable) { project2 }
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 718004a0087..86925e6a0ba 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -8,69 +8,59 @@ RSpec.describe API::PypiPackages do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
- let_it_be(:project, reload: true) { create(:project, :public) }
+ let_it_be_with_reload(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, :public, group: group) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
- let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
+ let(:headers) { {} }
- describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
+ context 'simple API endpoint' do
let_it_be(:package) { create(:pypi_package, project: project) }
- let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package.name}" }
- subject { get api(url) }
+ subject { get api(url), headers: headers }
- context 'with valid project' do
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'PyPI package versions' | :success
- 'PUBLIC' | :guest | true | true | 'PyPI package versions' | :success
- 'PUBLIC' | :developer | true | false | 'PyPI package versions' | :success
- 'PUBLIC' | :guest | true | false | 'PyPI package versions' | :success
- 'PUBLIC' | :developer | false | true | 'PyPI package versions' | :success
- 'PUBLIC' | :guest | false | true | 'PyPI package versions' | :success
- 'PUBLIC' | :developer | false | false | 'PyPI package versions' | :success
- 'PUBLIC' | :guest | false | false | 'PyPI package versions' | :success
- 'PUBLIC' | :anonymous | false | true | 'PyPI package versions' | :success
- 'PRIVATE' | :developer | true | true | 'PyPI package versions' | :success
- 'PRIVATE' | :guest | true | true | 'process PyPI api request' | :forbidden
- 'PRIVATE' | :developer | true | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'process PyPI api request' | :not_found
- 'PRIVATE' | :guest | false | true | 'process PyPI api request' | :not_found
- 'PRIVATE' | :developer | false | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'process PyPI api request' | :unauthorized
- end
+ describe 'GET /api/v4/groups/:id/-/packages/pypi/simple/:package_name' do
+ let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package.name}" }
+ let(:snowplow_gitlab_standard_context) { {} }
- with_them do
- let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ it_behaves_like 'pypi simple API endpoint'
+ it_behaves_like 'rejects PyPI access with unknown group id'
- subject { get api(url), headers: headers }
+ context 'deploy tokens' do
+ let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: group) }
before do
- project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
end
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ it_behaves_like 'deploy token for package GET requests'
end
- end
- context 'with a normalized package name' do
- let_it_be(:package) { create(:pypi_package, project: project, name: 'my.package') }
- let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" }
- let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
+ context 'job token' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ group.add_developer(user)
+ end
- subject { get api(url), headers: headers }
+ it_behaves_like 'job token for package GET requests'
+ end
- it_behaves_like 'PyPI package versions', :developer, :success
+ it_behaves_like 'a pypi user namespace endpoint'
end
- it_behaves_like 'deploy token for package GET requests'
-
- it_behaves_like 'job token for package GET requests'
+ describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
+ let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package.name}" }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
- it_behaves_like 'rejects PyPI access with unknown project id'
+ it_behaves_like 'pypi simple API endpoint'
+ it_behaves_like 'rejects PyPI access with unknown project id'
+ it_behaves_like 'deploy token for package GET requests'
+ it_behaves_like 'job token for package GET requests'
+ end
end
describe 'POST /api/v4/projects/:id/packages/pypi/authorize' do
@@ -82,25 +72,25 @@ RSpec.describe API::PypiPackages do
subject { post api(url), headers: headers }
context 'with valid project' do
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process PyPI api request' | :success
- 'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden
- 'PUBLIC' | :developer | true | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'process PyPI api request' | :forbidden
- 'PUBLIC' | :guest | false | true | 'process PyPI api request' | :forbidden
- 'PUBLIC' | :developer | false | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process PyPI api request' | :success
- 'PRIVATE' | :guest | true | true | 'process PyPI api request' | :forbidden
- 'PRIVATE' | :developer | true | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'process PyPI api request' | :not_found
- 'PRIVATE' | :guest | false | true | 'process PyPI api request' | :not_found
- 'PRIVATE' | :developer | false | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'process PyPI api request' | :unauthorized
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | true | 'process PyPI api request' | :success
+ :public | :guest | true | true | 'process PyPI api request' | :forbidden
+ :public | :developer | true | false | 'process PyPI api request' | :unauthorized
+ :public | :guest | true | false | 'process PyPI api request' | :unauthorized
+ :public | :developer | false | true | 'process PyPI api request' | :forbidden
+ :public | :guest | false | true | 'process PyPI api request' | :forbidden
+ :public | :developer | false | false | 'process PyPI api request' | :unauthorized
+ :public | :guest | false | false | 'process PyPI api request' | :unauthorized
+ :public | :anonymous | false | true | 'process PyPI api request' | :unauthorized
+ :private | :developer | true | true | 'process PyPI api request' | :success
+ :private | :guest | true | true | 'process PyPI api request' | :forbidden
+ :private | :developer | true | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | true | false | 'process PyPI api request' | :unauthorized
+ :private | :developer | false | true | 'process PyPI api request' | :not_found
+ :private | :guest | false | true | 'process PyPI api request' | :not_found
+ :private | :developer | false | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | false | false | 'process PyPI api request' | :unauthorized
+ :private | :anonymous | false | true | 'process PyPI api request' | :unauthorized
end
with_them do
@@ -109,7 +99,7 @@ RSpec.describe API::PypiPackages do
let(:headers) { user_headers.merge(workhorse_headers) }
before do
- project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
@@ -133,6 +123,7 @@ RSpec.describe API::PypiPackages do
let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '123' } }
let(:params) { base_params.merge(content: temp_file(file_name)) }
let(:send_rewritten_field) { true }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
subject do
workhorse_finalize(
@@ -146,25 +137,25 @@ RSpec.describe API::PypiPackages do
end
context 'with valid project' do
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'PyPI package creation' | :created
- 'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden
- 'PUBLIC' | :developer | true | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'process PyPI api request' | :forbidden
- 'PUBLIC' | :guest | false | true | 'process PyPI api request' | :forbidden
- 'PUBLIC' | :developer | false | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'process PyPI api request' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process PyPI api request' | :created
- 'PRIVATE' | :guest | true | true | 'process PyPI api request' | :forbidden
- 'PRIVATE' | :developer | true | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'process PyPI api request' | :not_found
- 'PRIVATE' | :guest | false | true | 'process PyPI api request' | :not_found
- 'PRIVATE' | :developer | false | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'process PyPI api request' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'process PyPI api request' | :unauthorized
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | true | 'PyPI package creation' | :created
+ :public | :guest | true | true | 'process PyPI api request' | :forbidden
+ :public | :developer | true | false | 'process PyPI api request' | :unauthorized
+ :public | :guest | true | false | 'process PyPI api request' | :unauthorized
+ :public | :developer | false | true | 'process PyPI api request' | :forbidden
+ :public | :guest | false | true | 'process PyPI api request' | :forbidden
+ :public | :developer | false | false | 'process PyPI api request' | :unauthorized
+ :public | :guest | false | false | 'process PyPI api request' | :unauthorized
+ :public | :anonymous | false | true | 'process PyPI api request' | :unauthorized
+ :private | :developer | true | true | 'process PyPI api request' | :created
+ :private | :guest | true | true | 'process PyPI api request' | :forbidden
+ :private | :developer | true | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | true | false | 'process PyPI api request' | :unauthorized
+ :private | :developer | false | true | 'process PyPI api request' | :not_found
+ :private | :guest | false | true | 'process PyPI api request' | :not_found
+ :private | :developer | false | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | false | false | 'process PyPI api request' | :unauthorized
+ :private | :anonymous | false | true | 'process PyPI api request' | :unauthorized
end
with_them do
@@ -173,7 +164,7 @@ RSpec.describe API::PypiPackages do
let(:headers) { user_headers.merge(workhorse_headers) }
before do
- project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
@@ -187,7 +178,7 @@ RSpec.describe API::PypiPackages do
let(:headers) { user_headers.merge(workhorse_headers) }
before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
end
it_behaves_like 'process PyPI api request', :developer, :bad_request, true
@@ -225,84 +216,27 @@ RSpec.describe API::PypiPackages do
end
end
- describe 'GET /api/v4/projects/:id/packages/pypi/files/:sha256/*file_identifier' do
+ context 'file download endpoint' do
let_it_be(:package_name) { 'Dummy-Package' }
let_it_be(:package) { create(:pypi_package, project: project, name: package_name, version: '1.0.0') }
- let(:url) { "/projects/#{project.id}/packages/pypi/files/#{package.package_files.first.file_sha256}/#{package_name}-1.0.0.tar.gz" }
-
- subject { get api(url) }
-
- context 'with valid project' do
- where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'PyPI package download' | :success
- 'PUBLIC' | :guest | true | true | 'PyPI package download' | :success
- 'PUBLIC' | :developer | true | false | 'PyPI package download' | :success
- 'PUBLIC' | :guest | true | false | 'PyPI package download' | :success
- 'PUBLIC' | :developer | false | true | 'PyPI package download' | :success
- 'PUBLIC' | :guest | false | true | 'PyPI package download' | :success
- 'PUBLIC' | :developer | false | false | 'PyPI package download' | :success
- 'PUBLIC' | :guest | false | false | 'PyPI package download' | :success
- 'PUBLIC' | :anonymous | false | true | 'PyPI package download' | :success
- 'PRIVATE' | :developer | true | true | 'PyPI package download' | :success
- 'PRIVATE' | :guest | true | true | 'PyPI package download' | :success
- 'PRIVATE' | :developer | true | false | 'PyPI package download' | :success
- 'PRIVATE' | :guest | true | false | 'PyPI package download' | :success
- 'PRIVATE' | :developer | false | true | 'PyPI package download' | :success
- 'PRIVATE' | :guest | false | true | 'PyPI package download' | :success
- 'PRIVATE' | :developer | false | false | 'PyPI package download' | :success
- 'PRIVATE' | :guest | false | false | 'PyPI package download' | :success
- 'PRIVATE' | :anonymous | false | true | 'PyPI package download' | :success
- end
-
- with_them do
- let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
-
- subject { get api(url), headers: headers }
+ subject { get api(url), headers: headers }
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
- end
-
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
- end
- end
-
- context 'with deploy token headers' do
- let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) }
-
- context 'valid token' do
- it_behaves_like 'returning response status', :success
- end
-
- context 'invalid token' do
- let(:headers) { basic_auth_header('foo', 'bar') }
+ describe 'GET /api/v4/groups/:id/-/packages/pypi/files/:sha256/*file_identifier' do
+ let(:url) { "/groups/#{group.id}/-/packages/pypi/files/#{package.package_files.first.file_sha256}/#{package_name}-1.0.0.tar.gz" }
+ let(:snowplow_gitlab_standard_context) { {} }
- it_behaves_like 'returning response status', :success
- end
+ it_behaves_like 'pypi file download endpoint'
+ it_behaves_like 'rejects PyPI access with unknown group id'
+ it_behaves_like 'a pypi user namespace endpoint'
end
- context 'with job token headers' do
- let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
-
- context 'valid token' do
- it_behaves_like 'returning response status', :success
- end
-
- context 'invalid token' do
- let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
-
- it_behaves_like 'returning response status', :success
- end
-
- context 'invalid user' do
- let(:headers) { basic_auth_header('foo', job.token) }
+ describe 'GET /api/v4/projects/:id/packages/pypi/files/:sha256/*file_identifier' do
+ let(:url) { "/projects/#{project.id}/packages/pypi/files/#{package.package_files.first.file_sha256}/#{package_name}-1.0.0.tar.gz" }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
- it_behaves_like 'returning response status', :success
- end
+ it_behaves_like 'pypi file download endpoint'
+ it_behaves_like 'rejects PyPI access with unknown project id'
end
-
- it_behaves_like 'rejects PyPI access with unknown project id'
end
end
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index dad3e34404b..81a4fcdbcac 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -775,7 +775,7 @@ RSpec.describe API::Releases do
end
context 'when using JOB-TOKEN auth' do
- let(:job) { create(:ci_build, user: maintainer) }
+ let(:job) { create(:ci_build, user: maintainer, project: project) }
let(:params) do
{
name: 'Another release',
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index a12b4dc9848..1b96efeca22 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -178,10 +178,12 @@ RSpec.describe API::Repositories do
expect(headers['Content-Disposition']).to eq 'inline'
end
- it_behaves_like 'uncached response' do
- before do
- get api(route, current_user)
- end
+ it 'defines an uncached header response' do
+ get api(route, current_user)
+
+ expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
+ expect(response.headers["Pragma"]).to eq("no-cache")
+ expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
end
context 'when sha does not exist' do
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index d6ad8186063..7d863b55bbe 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -10,10 +10,11 @@ RSpec.describe API::RubygemPackages do
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user }
- let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:headers) { {} }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
let(:tokens) do
{
@@ -162,6 +163,7 @@ RSpec.describe API::RubygemPackages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
@@ -304,6 +306,16 @@ RSpec.describe API::RubygemPackages do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:user_headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: snowplow_user } }
+ let(:snowplow_user) do
+ if token_type == :deploy_token
+ deploy_token
+ elsif token_type == :job_token
+ job.user
+ else
+ user
+ end
+ end
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 1f859622760..8701efcd65f 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe API::Services do
context 'project with services' do
let!(:active_service) { create(:emails_on_push_service, project: project, active: true) }
- let!(:service) { create(:custom_issue_tracker_service, project: project, active: false) }
+ let!(:service) { create(:custom_issue_tracker_integration, project: project, active: false) }
it "returns a list of all active services" do
get api("/projects/#{project.id}/services", user)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 66c0dcaa36c..4a4aeaea714 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -113,6 +113,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
terms: 'Hello world!',
performance_bar_allowed_group_path: group.full_path,
diff_max_patch_bytes: 300_000,
+ diff_max_files: 2000,
+ diff_max_lines: 50000,
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
allow_local_requests_from_web_hooks_and_services: true,
@@ -159,6 +161,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['terms']).to eq('Hello world!')
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
expect(json_response['diff_max_patch_bytes']).to eq(300_000)
+ expect(json_response['diff_max_files']).to eq(2000)
+ expect(json_response['diff_max_lines']).to eq(50000)
expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(json_response['local_markdown_version']).to eq(3)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 3c698cf577e..1aa1ad87be9 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -358,17 +358,6 @@ RSpec.describe API::Tags do
expect(json_response['message']).to eq('Target foo is invalid')
end
- context 'lightweight tags with release notes' do
- it 'creates a new tag' do
- post api(route, current_user), params: { tag_name: tag_name, ref: 'master', release_description: 'Wow' }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/tag')
- expect(json_response['name']).to eq(tag_name)
- expect(json_response['release']['description']).to eq('Wow')
- end
- end
-
context 'annotated tag' do
it 'creates a new annotated tag' do
# Identity must be set in .gitconfig to create annotated tag.
@@ -440,122 +429,4 @@ RSpec.describe API::Tags do
end
end
end
-
- describe 'POST /projects/:id/repository/tags/:tag_name/release' do
- let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}/release" }
- let(:description) { 'Awesome release!' }
-
- shared_examples_for 'repository new release' do
- it 'creates description for existing git tag' do
- post api(route, user), params: { description: description }
-
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/release/tag_release')
- expect(json_response['tag_name']).to eq(tag_name)
- expect(json_response['description']).to eq(description)
- end
-
- context 'when tag does not exist' do
- let(:tag_name) { 'unknown' }
-
- it_behaves_like '404 response' do
- let(:request) { post api(route, current_user), params: { description: description } }
- let(:message) { '404 Tag Not Found' }
- end
- end
-
- context 'when repository is disabled' do
- include_context 'disabled repository'
-
- it_behaves_like '403 response' do
- let(:request) { post api(route, current_user), params: { description: description } }
- end
- end
- end
-
- context 'when authenticated', 'as a maintainer' do
- let(:current_user) { user }
-
- it_behaves_like 'repository new release'
-
- context 'requesting with the escaped project full path' do
- let(:project_id) { CGI.escape(project.full_path) }
-
- it_behaves_like 'repository new release'
- end
-
- context 'on tag with existing release' do
- let!(:release) { create(:release, :legacy, project: project, tag: tag_name, description: description) }
-
- it 'returns 409 if there is already a release' do
- post api(route, user), params: { description: description }
-
- expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response['message']).to eq('Release already exists')
- end
- end
- end
- end
-
- describe 'PUT id/repository/tags/:tag_name/release' do
- let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}/release" }
- let(:description) { 'Awesome release!' }
- let(:new_description) { 'The best release!' }
-
- shared_examples_for 'repository update release' do
- context 'on tag with existing release' do
- let!(:release) do
- create(:release,
- :legacy,
- project: project,
- tag: tag_name,
- description: description)
- end
-
- it 'updates the release description' do
- put api(route, current_user), params: { description: new_description }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['tag_name']).to eq(tag_name)
- expect(json_response['description']).to eq(new_description)
- end
- end
-
- context 'when tag does not exist' do
- let(:tag_name) { 'unknown' }
-
- it_behaves_like '403 response' do
- let(:request) { put api(route, current_user), params: { description: new_description } }
- let(:message) { '403 Forbidden' }
- end
- end
-
- context 'when repository is disabled' do
- include_context 'disabled repository'
-
- it_behaves_like '403 response' do
- let(:request) { put api(route, current_user), params: { description: new_description } }
- end
- end
- end
-
- context 'when authenticated', 'as a maintainer' do
- let(:current_user) { user }
-
- it_behaves_like 'repository update release'
-
- context 'requesting with the escaped project full path' do
- let(:project_id) { CGI.escape(project.full_path) }
-
- it_behaves_like 'repository update release'
- end
-
- context 'when release does not exist' do
- it_behaves_like '403 response' do
- let(:request) { put api(route, current_user), params: { description: new_description } }
- let(:message) { '403 Forbidden' }
- end
- end
- end
- end
end
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index d318b22cf27..b04f5ad9a94 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
let_it_be(:package) { create(:terraform_module_package, project: project) }
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user }
- let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
@@ -188,6 +188,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/file?token=#{token}") }
+ let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
before do
group.update!(visibility: visibility.to_s)
@@ -330,6 +331,16 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:user_headers) { user_role == :anonymous ? {} : { token_header => token } }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: snowplow_user } }
+ let(:snowplow_user) do
+ if token_type == :deploy_token
+ deploy_token
+ elsif token_type == :job_token
+ job.user
+ else
+ user
+ end
+ end
before do
project.update!(visibility: visibility.to_s)
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index 2cb3c8e9ab5..5d2635126e8 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -25,10 +25,6 @@ RSpec.describe API::Terraform::State do
context 'without authentication' do
let(:auth_header) { basic_auth_header('bad', 'token') }
- before do
- stub_feature_flags(usage_data_p_terraform_state_api_unique_users: false)
- end
-
it 'does not track unique event' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
@@ -156,15 +152,6 @@ RSpec.describe API::Terraform::State do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end
-
- context 'on Unicorn', :unicorn do
- it 'updates the state' do
- expect { request }.to change { Terraform::State.count }.by(0)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(Gitlab::Json.parse(response.body)).to be_empty
- end
- end
end
context 'without body' do
@@ -200,15 +187,6 @@ RSpec.describe API::Terraform::State do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to be_empty
end
-
- context 'on Unicorn', :unicorn do
- it 'creates a new state' do
- expect { request }.to change { Terraform::State.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(Gitlab::Json.parse(response.body)).to be_empty
- end
- end
end
context 'without body' do
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 0b70d62b093..9989f8d28bd 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -176,34 +176,9 @@ RSpec.describe API::Unleash do
it_behaves_like 'authenticated request'
context 'with version 1 (legacy) feature flags' do
- let(:feature_flag) { create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 1) }
+ let(:feature_flag) { create(:operations_feature_flag, :legacy_flag, project: project, name: 'feature1', active: true, version: 1) }
- it_behaves_like 'support multiple environments'
-
- context 'with a list of feature flags' do
- let(:headers) { { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "production" } }
- let!(:enabled_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature1', active: true, version: 1) }
- let!(:disabled_feature_flag) { create(:operations_feature_flag, project: project, name: 'feature2', active: false, version: 1) }
-
- it 'responds with a list of features' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['version']).to eq(1)
- expect(json_response['features']).not_to be_empty
- expect(json_response['features'].map { |f| f['name'] }.sort).to eq(%w[feature1 feature2])
- expect(json_response['features'].sort_by {|f| f['name'] }.map { |f| f['enabled'] }).to eq([true, false])
- end
-
- it 'matches json schema' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('unleash/unleash')
- end
- end
-
- it 'returns a feature flag strategy' do
+ it 'does not return a legacy feature flag' do
create(:operations_feature_flag_scope,
feature_flag: feature_flag,
environment_scope: 'sandbox',
@@ -215,81 +190,7 @@ RSpec.describe API::Unleash do
get api(features_url), headers: headers
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['features'].first['enabled']).to eq(true)
- strategies = json_response['features'].first['strategies']
- expect(strategies).to eq([{
- "name" => "gradualRolloutUserId",
- "parameters" => {
- "percentage" => "50",
- "groupId" => "default"
- }
- }])
- end
-
- it 'returns a default strategy for a scope' do
- create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'sandbox', active: true)
- headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "sandbox" }
-
- get api(features_url), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['features'].first['enabled']).to eq(true)
- strategies = json_response['features'].first['strategies']
- expect(strategies).to eq([{ "name" => "default", "parameters" => {} }])
- end
-
- it 'returns multiple strategies for a feature flag' do
- create(:operations_feature_flag_scope,
- feature_flag: feature_flag,
- environment_scope: 'staging',
- active: true,
- strategies: [{ name: "userWithId", parameters: { userIds: "max,fred" } },
- { name: "gradualRolloutUserId",
- parameters: { groupId: "default", percentage: "50" } }])
- headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "staging" }
-
- get api(features_url), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['features'].first['enabled']).to eq(true)
- strategies = json_response['features'].first['strategies'].sort_by { |s| s['name'] }
- expect(strategies).to eq([{
- "name" => "gradualRolloutUserId",
- "parameters" => {
- "percentage" => "50",
- "groupId" => "default"
- }
- }, {
- "name" => "userWithId",
- "parameters" => {
- "userIds" => "max,fred"
- }
- }])
- end
-
- it 'returns a disabled feature when the flag is disabled' do
- flag = create(:operations_feature_flag, project: project, name: 'test_feature', active: false, version: 1)
- create(:operations_feature_flag_scope, feature_flag: flag, environment_scope: 'production', active: true)
- headers = { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "production" }
-
- get api(features_url), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['features'].first['enabled']).to eq(false)
- end
-
- context "with an inactive scope" do
- let!(:scope) { create(:operations_feature_flag_scope, feature_flag: feature_flag, environment_scope: 'production', active: false, strategies: [{ name: "default", parameters: {} }]) }
- let(:headers) { { "UNLEASH-INSTANCEID" => client.token, "UNLEASH-APPNAME" => "production" } }
-
- it 'returns a disabled feature' do
- get api(features_url), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- feature_json = json_response['features'].first
- expect(feature_json['enabled']).to eq(false)
- expect(feature_json['strategies']).to eq([{ 'name' => 'default', 'parameters' => {} }])
- end
+ expect(json_response['features']).to be_empty
end
end
@@ -534,63 +435,6 @@ RSpec.describe API::Unleash do
}])
end
end
-
- context 'when mixing version 1 and version 2 feature flags' do
- it 'returns both types of flags when both match' do
- feature_flag_a = create(:operations_feature_flag, project: project,
- name: 'feature_a', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag_a,
- name: 'userWithId', parameters: { userIds: 'user8' })
- create(:operations_scope, strategy: strategy, environment_scope: 'staging')
- feature_flag_b = create(:operations_feature_flag, project: project,
- name: 'feature_b', active: true, version: 1)
- create(:operations_feature_flag_scope, feature_flag: feature_flag_b,
- active: true, strategies: [{ name: 'default', parameters: {} }], environment_scope: 'staging')
-
- get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'staging' }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['features'].sort_by {|f| f['name']}).to eq([{
- 'name' => 'feature_a',
- 'enabled' => true,
- 'strategies' => [{
- 'name' => 'userWithId',
- 'parameters' => { 'userIds' => 'user8' }
- }]
- }, {
- 'name' => 'feature_b',
- 'enabled' => true,
- 'strategies' => [{
- 'name' => 'default',
- 'parameters' => {}
- }]
- }])
- end
-
- it 'returns legacy flags when only legacy flags match' do
- feature_flag_a = create(:operations_feature_flag, project: project,
- name: 'feature_a', active: true, version: 2)
- strategy = create(:operations_strategy, feature_flag: feature_flag_a,
- name: 'userWithId', parameters: { userIds: 'user8' })
- create(:operations_scope, strategy: strategy, environment_scope: 'production')
- feature_flag_b = create(:operations_feature_flag, project: project,
- name: 'feature_b', active: true, version: 1)
- create(:operations_feature_flag_scope, feature_flag: feature_flag_b,
- active: true, strategies: [{ name: 'default', parameters: {} }], environment_scope: 'staging')
-
- get api(features_url), headers: { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'staging' }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['features']).to eq([{
- 'name' => 'feature_b',
- 'enabled' => true,
- 'strategies' => [{
- 'name' => 'default',
- 'parameters' => {}
- }]
- }])
- end
- end
end
end
diff --git a/spec/requests/api/users_preferences_spec.rb b/spec/requests/api/users_preferences_spec.rb
index db03786ed2a..97e37263ee6 100644
--- a/spec/requests/api/users_preferences_spec.rb
+++ b/spec/requests/api/users_preferences_spec.rb
@@ -8,11 +8,19 @@ RSpec.describe API::Users do
describe 'PUT /user/preferences/' do
context "with correct attributes and a logged in user" do
it 'returns a success status and the value has been changed' do
- put api("/user/preferences", user), params: { view_diffs_file_by_file: true }
+ put api("/user/preferences", user), params: {
+ view_diffs_file_by_file: true,
+ show_whitespace_in_diffs: true
+ }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['view_diffs_file_by_file']).to eq(true)
- expect(user.reload.view_diffs_file_by_file).to be_truthy
+ expect(json_response['show_whitespace_in_diffs']).to eq(true)
+
+ user.reload
+
+ expect(user.view_diffs_file_by_file).to be_truthy
+ expect(user.show_whitespace_in_diffs).to be_truthy
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 71fdd986f20..a9231b65c8f 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -2077,6 +2077,29 @@ RSpec.describe API::Users do
it_behaves_like 'get user info', 'v4'
end
+ describe "GET /user/preferences" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get api("/user/preferences")
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns user preferences" do
+ user.user_preference.view_diffs_file_by_file = false
+ user.user_preference.show_whitespace_in_diffs = true
+ user.save!
+
+ get api("/user/preferences", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["view_diffs_file_by_file"]).to eq(user.user_preference.view_diffs_file_by_file)
+ expect(json_response["show_whitespace_in_diffs"]).to eq(user.user_preference.show_whitespace_in_diffs)
+ end
+ end
+ end
+
describe "GET /user/keys" do
context "when unauthenticated" do
it "returns authentication error" do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index d35aab40ca9..64fde3db19f 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -16,8 +16,19 @@ RSpec.describe API::Wikis do
include WorkhorseHelpers
include AfterNextHelpers
- let(:user) { create(:user) }
- let(:group) { create(:group).tap { |g| g.add_owner(user) } }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group).tap { |g| g.add_owner(user) } }
+ let_it_be(:group_project) { create(:project, :wiki_repo, namespace: group) }
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:project_wiki_disabled) do
+ create(:project, :wiki_repo, :wiki_disabled).tap do |project|
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
+ end
+
let(:project_wiki) { create(:project_wiki, project: project, user: user) }
let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } }
let(:expected_keys_with_content) { %w(content format slug title) }
@@ -32,7 +43,7 @@ RSpec.describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
+ let(:project) { project_wiki_disabled }
context 'when user is guest' do
before do
@@ -44,9 +55,7 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
- project.add_developer(user)
-
- get api(url, user)
+ get api(url, developer)
end
include_examples 'wiki API 403 Forbidden'
@@ -54,9 +63,7 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
- project.add_maintainer(user)
-
- get api(url, user)
+ get api(url, maintainer)
end
include_examples 'wiki API 403 Forbidden'
@@ -125,7 +132,7 @@ RSpec.describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
+ let(:project) { project_wiki_disabled }
context 'when user is guest' do
before do
@@ -137,9 +144,7 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
- project.add_developer(user)
-
- get api(url, user)
+ get api(url, developer)
end
include_examples 'wiki API 403 Forbidden'
@@ -147,9 +152,7 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
- project.add_maintainer(user)
-
- get api(url, user)
+ get api(url, maintainer)
end
include_examples 'wiki API 403 Forbidden'
@@ -249,7 +252,7 @@ RSpec.describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
+ let(:project) { project_wiki_disabled }
context 'when user is guest' do
before do
@@ -261,8 +264,7 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
- project.add_developer(user)
- post(api(url, user), params: payload)
+ post(api(url, developer), params: payload)
end
include_examples 'wiki API 403 Forbidden'
@@ -270,8 +272,7 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
- project.add_maintainer(user)
- post(api(url, user), params: payload)
+ post(api(url, maintainer), params: payload)
end
include_examples 'wiki API 403 Forbidden'
@@ -469,7 +470,7 @@ RSpec.describe API::Wikis do
end
context 'when wiki belongs to a group project' do
- let(:project) { create(:project, :wiki_repo, namespace: group) }
+ let(:project) { group_project }
include_examples 'wikis API updates wiki page'
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 279c65fc2f4..7cf46f6adc6 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -883,10 +883,10 @@ RSpec.describe 'Git HTTP requests' do
context 'when admin mode is enabled', :enable_admin_mode do
it_behaves_like 'can download code only'
- it 'downloads from other project get status 403' do
+ it 'downloads from other project get status 404' do
clone_get "#{other_project.full_path}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
diff --git a/spec/requests/groups/email_campaigns_controller_spec.rb b/spec/requests/groups/email_campaigns_controller_spec.rb
index 48297ec4cb6..4d630ef6710 100644
--- a/spec/requests/groups/email_campaigns_controller_spec.rb
+++ b/spec/requests/groups/email_campaigns_controller_spec.rb
@@ -9,10 +9,11 @@ RSpec.describe Groups::EmailCampaignsController do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
+
let(:track) { 'create' }
let(:series) { '0' }
let(:schema) { described_class::EMAIL_CAMPAIGNS_SCHEMA_URL }
- let(:subject_line_text) { Gitlab::Email::Message::InProductMarketing.for(track.to_sym).new(group: group, series: series.to_i).subject_line }
+ let(:subject_line_text) { Gitlab::Email::Message::InProductMarketing.for(track.to_sym).new(group: group, user: user, series: series.to_i).subject_line }
let(:data) do
{
namespace_id: group.id,
@@ -51,7 +52,9 @@ RSpec.describe Groups::EmailCampaignsController do
context: [{
schema: described_class::EMAIL_CAMPAIGNS_SCHEMA_URL,
data: { namespace_id: group.id, series: series.to_i, subject_line: subject_line_text, track: track.to_s }
- }]
+ }],
+ user: user,
+ namespace: group
)
end
@@ -91,7 +94,7 @@ RSpec.describe Groups::EmailCampaignsController do
describe 'track parameter' do
context 'when valid' do
- where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys)
+ where(track: Namespaces::InProductMarketingEmailsService::TRACKS.keys.without(:experience))
with_them do
it_behaves_like 'track and redirect'
@@ -109,7 +112,7 @@ RSpec.describe Groups::EmailCampaignsController do
describe 'series parameter' do
context 'when valid' do
- where(series: (0..Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.length - 1).to_a)
+ where(series: (0..Namespaces::InProductMarketingEmailsService::TRACKS[:create][:interval_days].length - 1).to_a)
with_them do
it_behaves_like 'track and redirect'
@@ -117,7 +120,7 @@ RSpec.describe Groups::EmailCampaignsController do
end
context 'when invalid' do
- where(series: [-1, nil, Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.length])
+ where(series: [-1, nil, Namespaces::InProductMarketingEmailsService::TRACKS[:create][:interval_days].length])
with_them do
it_behaves_like 'no track and 404'
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 0e3a0252638..fda8b2ecec6 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -569,7 +569,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
# I'm not sure what this tests that is different from the previous test
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 404 response'
end
end
@@ -1043,7 +1043,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
# I'm not sure what this tests that is different from the previous test
- it_behaves_like 'LFS http 403 response'
+ it_behaves_like 'LFS http 404 response'
end
end
diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb
index c3cdae2cd21..1967d0ba8b1 100644
--- a/spec/requests/oauth/tokens_controller_spec.rb
+++ b/spec/requests/oauth/tokens_controller_spec.rb
@@ -3,12 +3,71 @@
require 'spec_helper'
RSpec.describe Oauth::TokensController do
- it 'allows cross-origin POST requests' do
- post '/oauth/token', headers: { 'Origin' => 'http://notgitlab.com' }
+ let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
+ let(:other_headers) { {} }
+ let(:headers) { cors_request_headers.merge(other_headers)}
- expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
- expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST'
- expect(response.headers['Access-Control-Allow-Headers']).to be_nil
- expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ shared_examples 'cross-origin POST request' do
+ it 'allows cross-origin requests' do
+ expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST'
+ expect(response.headers['Access-Control-Allow-Headers']).to be_nil
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ end
+ end
+
+ shared_examples 'CORS preflight OPTIONS request' do
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'allows cross-origin requests' do
+ expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST'
+ expect(response.headers['Access-Control-Allow-Headers']).to eq 'Authorization'
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ end
+ end
+
+ describe 'POST /oauth/token' do
+ before do
+ post '/oauth/token', headers: headers
+ end
+
+ it_behaves_like 'cross-origin POST request'
+ end
+
+ describe 'OPTIONS /oauth/token' do
+ let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } }
+
+ before do
+ options '/oauth/token', headers: headers
+ end
+
+ it_behaves_like 'CORS preflight OPTIONS request'
+ end
+
+ describe 'POST /oauth/revoke' do
+ let(:other_headers) { { 'Content-Type' => 'application/x-www-form-urlencoded' } }
+
+ before do
+ post '/oauth/revoke', headers: headers, params: { token: '12345' }
+ end
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'cross-origin POST request'
+ end
+
+ describe 'OPTIONS /oauth/revoke' do
+ let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } }
+
+ before do
+ options '/oauth/revoke', headers: headers
+ end
+
+ it_behaves_like 'CORS preflight OPTIONS request'
end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 7b682d76150..5bf786f2290 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -41,6 +41,8 @@ RSpec.describe 'OpenID Connect requests' do
}
end
+ let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
+
def request_access_token!
login_as user
@@ -81,6 +83,24 @@ RSpec.describe 'OpenID Connect requests' do
end
end
+ shared_examples 'cross-origin GET request' do
+ it 'allows cross-origin request' do
+ expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, HEAD'
+ expect(response.headers['Access-Control-Allow-Headers']).to be_nil
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ end
+ end
+
+ shared_examples 'cross-origin GET and POST request' do
+ it 'allows cross-origin request' do
+ expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, HEAD, POST'
+ expect(response.headers['Access-Control-Allow-Headers']).to be_nil
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ end
+ end
+
context 'Application with OpenID scope' do
let(:application) { create :oauth_application, scopes: 'openid' }
@@ -180,6 +200,51 @@ RSpec.describe 'OpenID Connect requests' do
expect(response).to redirect_to('/users/sign_in')
end
end
+
+ context 'OpenID Discovery keys' do
+ context 'with a cross-origin request' do
+ before do
+ get '/oauth/discovery/keys', headers: cors_request_headers
+ end
+
+ it 'returns data' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'cross-origin GET request'
+ end
+
+ context 'with a cross-origin preflight OPTIONS request' do
+ before do
+ options '/oauth/discovery/keys', headers: cors_request_headers
+ end
+
+ it_behaves_like 'cross-origin GET request'
+ end
+ end
+
+ context 'OpenID WebFinger endpoint' do
+ context 'with a cross-origin request' do
+ before do
+ get '/.well-known/webfinger', headers: cors_request_headers, params: { resource: 'user@example.com' }
+ end
+
+ it 'returns data' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['subject']).to eq('user@example.com')
+ end
+
+ it_behaves_like 'cross-origin GET request'
+ end
+ end
+
+ context 'with a cross-origin preflight OPTIONS request' do
+ before do
+ options '/.well-known/webfinger', headers: cors_request_headers, params: { resource: 'user@example.com' }
+ end
+
+ it_behaves_like 'cross-origin GET request'
+ end
end
context 'OpenID configuration information' do
@@ -191,6 +256,27 @@ RSpec.describe 'OpenID Connect requests' do
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
expect(json_response['scopes_supported']).to eq(%w[api read_user read_api read_repository write_repository sudo openid profile email])
end
+
+ context 'with a cross-origin request' do
+ before do
+ get '/.well-known/openid-configuration', headers: cors_request_headers
+
+ 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 eq(%w[api read_user read_api read_repository write_repository sudo openid profile email])
+ end
+
+ it_behaves_like 'cross-origin GET request'
+ end
+
+ context 'with a cross-origin preflight OPTIONS request' do
+ before do
+ options '/.well-known/openid-configuration', headers: cors_request_headers
+ end
+
+ it_behaves_like 'cross-origin GET request'
+ end
end
context 'Application with OpenID and email scopes' do
@@ -218,6 +304,30 @@ RSpec.describe 'OpenID Connect requests' do
it 'has true in email_verified claim' do
expect(json_response['email_verified']).to eq(true)
end
+
+ context 'with a cross-origin request' do
+ before do
+ get '/oauth/userinfo', headers: cors_request_headers
+ end
+
+ it_behaves_like 'cross-origin GET and POST request'
+ end
+
+ context 'with a cross-origin POST request' do
+ before do
+ post '/oauth/userinfo', headers: cors_request_headers
+ end
+
+ it_behaves_like 'cross-origin GET and POST request'
+ end
+
+ context 'with a cross-origin preflight OPTIONS request' do
+ before do
+ options '/oauth/userinfo', headers: cors_request_headers
+ end
+
+ it_behaves_like 'cross-origin GET and POST request'
+ end
end
context 'ID token payload' do
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index f092cbf26a4..5a38f92221f 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -675,48 +675,6 @@ RSpec.describe UsersController do
end
end
- describe 'GET #suggests' do
- context 'when user exists' do
- it 'returns JSON indicating the user exists and a suggestion' do
- get user_suggests_url user.username
-
- expected_json = { exists: true, suggests: ["#{user.username}1"] }.to_json
- expect(response.body).to eq(expected_json)
- end
-
- context 'when the casing is different' do
- let(:user) { create(:user, username: 'CamelCaseUser') }
-
- it 'returns JSON indicating the user exists and a suggestion' do
- get user_suggests_url user.username.downcase
-
- expected_json = { exists: true, suggests: ["#{user.username.downcase}1"] }.to_json
- expect(response.body).to eq(expected_json)
- end
- end
- end
-
- context 'when the user does not exist' do
- it 'returns JSON indicating the user does not exist' do
- get user_suggests_url 'foo'
-
- expected_json = { exists: false, suggests: [] }.to_json
- expect(response.body).to eq(expected_json)
- end
-
- context 'when a user changed their username' do
- let(:redirect_route) { user.namespace.redirect_routes.create!(path: 'old-username') }
-
- it 'returns JSON indicating a user by that username does not exist' do
- get user_suggests_url 'old-username'
-
- expected_json = { exists: false, suggests: [] }.to_json
- expect(response.body).to eq(expected_json)
- end
- end
- end
- end
-
describe '#ensure_canonical_path' do
before do
sign_in(user)
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 056f4d30ea5..fe04a1d7c4a 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -202,6 +202,16 @@ RSpec.describe 'project routing' do
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "stable", path: "new\n\nline.txt" })
end
+
+ it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/refs/switch', '/gitlab/gitlabhq/-/refs/switch'
+
+ it_behaves_like 'redirecting a legacy path',
+ '/gitlab/gitlabhq/refs/feature%2345/logs_tree',
+ '/gitlab/gitlabhq/-/refs/feature%2345/logs_tree'
+
+ it_behaves_like 'redirecting a legacy path',
+ '/gitlab/gitlabhq/refs/stable/logs_tree/new%0A%0Aline.txt',
+ '/gitlab/gitlabhq/-/refs/stable/logs_tree/new%0A%0Aline.txt'
end
describe Projects::MergeRequestsController, 'routing' do
@@ -310,9 +320,11 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
- it 'to #show from unscope routing' do
- expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ it 'to #raw from unscope routing' do
+ expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
+
+ it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/snippets/1', '/gitlab/gitlabhq/-/snippets/1'
end
# test_project_hook POST /:project_id/-/hooks/:id/test(.:format) hooks#test
@@ -357,9 +369,7 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
end
- it 'to #show unscoped routing' do
- expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd')
- end
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commit/4246fbd", "/gitlab/gitlabhq/-/commit/4246fbd"
end
# patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch
@@ -376,9 +386,7 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
end
- it 'to #show unscoped routing' do
- expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
- end
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commits/master", "/gitlab/gitlabhq/-/commits/master"
end
# project_project_members GET /:project_id/project_members(.:format) project_members#index
@@ -517,6 +525,7 @@ RSpec.describe 'project routing' do
end
it 'to #show from unscoped routing' do
+ expect(get('/gitlab/gitlabhq/tree/master')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
end
end
@@ -545,6 +554,9 @@ RSpec.describe 'project routing' do
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "#{newline_file}" })
end
+
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/find_file", "/gitlab/gitlabhq/-/find_file"
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/files/master", "/gitlab/gitlabhq/-/files/master"
end
describe Projects::BlobController, 'routing' do
@@ -575,6 +587,9 @@ RSpec.describe 'project routing' do
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "master/docs/#{newline_file}" })
end
+
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/new/master", "/gitlab/gitlabhq/-/new/master"
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/edit/master/README", "/gitlab/gitlabhq/-/edit/master/README"
end
# project_raw GET /:project_id/-/raw/:id(.:format) raw#show {id: /[^\0]+/, project_id: /[^\/]+/}
@@ -610,6 +625,9 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable')
expect(get('/gitlab/gitlabhq/-/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable')
end
+
+ it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare', '/gitlab/gitlabhq/-/compare'
+ it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare/master...stable', '/gitlab/gitlabhq/-/compare/master...stable'
end
describe Projects::NetworkController, 'routing' do
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 7b9ba783885..e7ea5b79897 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -96,9 +96,11 @@ RSpec.describe SnippetsController, "routing" do
expect(get("/-/snippets/1")).to route_to('snippets#show', id: '1')
end
- it 'to #show from unscoped routing' do
- expect(get("/snippets/1")).to route_to('snippets#show', id: '1')
+ it 'to #raw from unscoped routing' do
+ expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1')
end
+
+ it_behaves_like 'redirecting a legacy path', '/snippets/1', '/-/snippets/1'
end
# help GET /help(.:format) help#index
@@ -132,12 +134,10 @@ RSpec.describe ProfilesController, "routing" do
it "to #account" do
expect(get("/-/profile/account")).to route_to('profiles/accounts#show')
end
- it_behaves_like 'redirecting a legacy path', '/profile/account', '/-/profile/account'
it "to #audit_log" do
expect(get("/-/profile/audit_log")).to route_to('profiles#audit_log')
end
- it_behaves_like 'redirecting a legacy path', '/profile/audit_log', '/-/profile/audit_log'
it "to #reset_feed_token" do
expect(put("/-/profile/reset_feed_token")).to route_to('profiles#reset_feed_token')
@@ -146,7 +146,6 @@ RSpec.describe ProfilesController, "routing" do
it "to #show" do
expect(get("/-/profile")).to route_to('profiles#show')
end
- it_behaves_like 'redirecting a legacy path', '/profile', '/-/profile'
end
# profile_preferences GET /-/profile/preferences(.:format) profiles/preferences#show
@@ -156,7 +155,6 @@ RSpec.describe Profiles::PreferencesController, 'routing' do
it 'to #show' do
expect(get('/-/profile/preferences')).to route_to('profiles/preferences#show')
end
- it_behaves_like 'redirecting a legacy path', '/profile/preferences', '/-/profile/preferences'
it 'to #update' do
expect(put('/-/profile/preferences')).to route_to('profiles/preferences#update')
@@ -174,7 +172,6 @@ RSpec.describe Profiles::KeysController, "routing" do
it "to #index" do
expect(get("/-/profile/keys")).to route_to('profiles/keys#index')
end
- it_behaves_like 'redirecting a legacy path', '/profile/keys', '/-/profile/keys'
it "to #create" do
expect(post("/-/profile/keys")).to route_to('profiles/keys#create')
@@ -183,7 +180,6 @@ RSpec.describe Profiles::KeysController, "routing" do
it "to #show" do
expect(get("/-/profile/keys/1")).to route_to('profiles/keys#show', id: '1')
end
- it_behaves_like 'redirecting a legacy path', '/profile/keys/1', '/-/profile/keys/1'
it "to #destroy" do
expect(delete("/-/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1')
@@ -198,7 +194,6 @@ RSpec.describe Profiles::GpgKeysController, "routing" do
it "to #index" do
expect(get("/-/profile/gpg_keys")).to route_to('profiles/gpg_keys#index')
end
- it_behaves_like 'redirecting a legacy path', '/profile/gpg_keys', '/-/profile/gpg_keys'
it "to #create" do
expect(post("/-/profile/gpg_keys")).to route_to('profiles/gpg_keys#create')
@@ -216,7 +211,6 @@ RSpec.describe Profiles::EmailsController, "routing" do
it "to #index" do
expect(get("/-/profile/emails")).to route_to('profiles/emails#index')
end
- it_behaves_like 'redirecting a legacy path', '/profile/emails', '/-/profile/emails'
it "to #create" do
expect(post("/-/profile/emails")).to route_to('profiles/emails#create')
@@ -335,10 +329,6 @@ RSpec.describe InvitesController, 'routing' do
it 'to #show' do
expect(get("/-/invites/#{member.invite_token}")).to route_to('invites#show', id: member.invite_token)
end
-
- it 'to legacy route' do
- expect(get("/invites/#{member.invite_token}")).to route_to('invites#show', id: member.invite_token)
- end
end
RSpec.describe AbuseReportsController, 'routing' do
@@ -347,10 +337,6 @@ RSpec.describe AbuseReportsController, 'routing' do
it 'to #new' do
expect(get("/-/abuse_reports/new?user_id=#{user.id}")).to route_to('abuse_reports#new', user_id: user.id.to_s)
end
-
- it 'to legacy route' do
- expect(get("/abuse_reports/new?user_id=#{user.id}")).to route_to('abuse_reports#new', user_id: user.id.to_s)
- end
end
RSpec.describe SentNotificationsController, 'routing' do
@@ -376,12 +362,6 @@ RSpec.describe AutocompleteController, 'routing' do
it 'to #merge_request_target_branches' do
expect(get("/-/autocomplete/merge_request_target_branches")).to route_to('autocomplete#merge_request_target_branches')
end
-
- it 'to legacy route' do
- expect(get("/autocomplete/users")).to route_to('autocomplete#users')
- expect(get("/autocomplete/projects")).to route_to('autocomplete#projects')
- expect(get("/autocomplete/award_emojis")).to route_to('autocomplete#award_emojis')
- end
end
RSpec.describe Snippets::BlobsController, "routing" do
diff --git a/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb b/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
new file mode 100644
index 00000000000..56aecc3ec4e
--- /dev/null
+++ b/spec/rubocop/cop/usage_data/histogram_with_large_table_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../../rubocop/cop/usage_data/histogram_with_large_table'
+
+RSpec.describe RuboCop::Cop::UsageData::HistogramWithLargeTable do
+ let(:high_traffic_models) { %w[Issue Ci::Build] }
+ let(:msg) { 'Avoid histogram method on' }
+
+ let(:config) do
+ RuboCop::Config.new('UsageData/HistogramWithLargeTable' => {
+ 'HighTrafficModels' => high_traffic_models
+ })
+ end
+
+ subject(:cop) { described_class.new(config) }
+
+ context 'with large tables' do
+ context 'with one-level constants' do
+ context 'when calling histogram(Issue)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Issue, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
+ end
+
+ context 'when calling histogram(::Issue)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Issue, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
+ end
+
+ context 'when calling histogram(Issue.closed)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Issue.closed, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
+ end
+
+ context 'when calling histogram(::Issue.closed)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Issue.closed, :project_id, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Issue
+ CODE
+ end
+ end
+ end
+
+ context 'with two-level constants' do
+ context 'when calling histogram(::Ci::Build)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Ci::Build, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
+ end
+
+ context 'when calling histogram(::Ci::Build.active)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(::Ci::Build.active, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
+ end
+
+ context 'when calling histogram(Ci::Build)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Ci::Build, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
+ end
+
+ context 'when calling histogram(Ci::Build.active)' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ histogram(Ci::Build.active, buckets: 1..100)
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} Ci::Build
+ CODE
+ end
+ end
+ end
+ end
+
+ context 'with non related class' do
+ it 'does not register an offense' do
+ expect_no_offenses('histogram(MergeRequest, buckets: 1..100)')
+ end
+ end
+
+ context 'with non related method' do
+ it 'does not register an offense' do
+ expect_no_offenses('count(Issue, buckets: 1..100)')
+ end
+ end
+end
diff --git a/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb b/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
new file mode 100644
index 00000000000..31324331e61
--- /dev/null
+++ b/spec/rubocop/cop/usage_data/instrumentation_superclass_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../../rubocop/cop/usage_data/instrumentation_superclass'
+
+RSpec.describe RuboCop::Cop::UsageData::InstrumentationSuperclass do
+ let(:allowed_classes) { %i[GenericMetric DatabaseMetric RedisHllMetric] }
+ let(:msg) { "Instrumentation classes should subclass one of the following: #{allowed_classes.join(', ')}." }
+
+ let(:config) do
+ RuboCop::Config.new('UsageData/InstrumentationSuperclass' => {
+ 'AllowedClasses' => allowed_classes
+ })
+ end
+
+ subject(:cop) { described_class.new(config) }
+
+ context 'with class definition' do
+ context 'when inheriting from allowed superclass' do
+ it 'does not register an offense' do
+ expect_no_offenses('class NewMetric < GenericMetric; end')
+ end
+ end
+
+ context 'when inheriting from some other superclass' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ class NewMetric < BaseMetric; end
+ ^^^^^^^^^^ #{msg}
+ CODE
+ end
+ end
+
+ context 'when not inheriting' do
+ it 'does not register an offense' do
+ expect_no_offenses('class NewMetric; end')
+ end
+ end
+ end
+
+ context 'with dynamic class definition' do
+ context 'when inheriting from allowed superclass' do
+ it 'does not register an offense' do
+ expect_no_offenses('NewMetric = Class.new(GenericMetric)')
+ end
+ end
+
+ context 'when inheriting from some other superclass' do
+ it 'registers an offense' do
+ expect_offense(<<~CODE)
+ NewMetric = Class.new(BaseMetric)
+ ^^^^^^^^^^ #{msg}
+ CODE
+ end
+ end
+
+ context 'when not inheriting' do
+ it 'does not register an offense' do
+ expect_no_offenses('NewMetric = Class.new')
+ end
+ end
+ end
+end
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
index cd8be07827d..9429c9d571a 100644
--- a/spec/serializers/analytics_summary_serializer_spec.rb
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe AnalyticsSummarySerializer do
let(:resource) do
Gitlab::CycleAnalytics::Summary::Issue
- .new(project: double, from: 1.day.ago, current_user: user)
+ .new(project: double, options: { from: 1.day.ago }, current_user: user)
end
before do
@@ -36,7 +36,7 @@ RSpec.describe AnalyticsSummarySerializer do
context 'when representing with unit' do
let(:resource) do
Gitlab::CycleAnalytics::Summary::DeploymentFrequency
- .new(deployments: 10, from: 1.day.ago)
+ .new(deployments: 10, options: { from: 1.day.ago })
end
subject { described_class.new.represent(resource, with_unit: true) }
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index aa2bb25b17c..1e71e45948c 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -77,17 +77,5 @@ RSpec.describe ClusterApplicationEntity do
expect(subject[:pages_domain]).to eq(id: pages_domain.id, domain: pages_domain.domain)
end
end
-
- context 'for fluentd application' do
- let(:application) { build(:clusters_applications_fluentd, :installed) }
-
- it 'includes host, port, protocol and log fields' do
- expect(subject[:port]).to eq(514)
- expect(subject[:host]).to eq("example.com")
- expect(subject[:protocol]).to eq("tcp")
- expect(subject[:waf_log_enabled]).to be true
- expect(subject[:cilium_log_enabled]).to be true
- end
- end
end
end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 10c6bc0e42a..ee1388024ea 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -91,9 +91,9 @@ RSpec.describe ClusterEntity do
end
end
- context 'elastic stack is installed on cluster' do
+ context 'elastic stack is enabled on cluster' do
it 'returns true' do
- create(:clusters_applications_elastic_stack, :installed, cluster: cluster)
+ create(:clusters_integrations_elastic_stack, cluster: cluster)
expect(subject[:enable_advanced_logs_querying]).to be true
end
diff --git a/spec/serializers/fork_namespace_entity_spec.rb b/spec/serializers/fork_namespace_entity_spec.rb
index 5e9918a89ff..32223b0d41a 100644
--- a/spec/serializers/fork_namespace_entity_spec.rb
+++ b/spec/serializers/fork_namespace_entity_spec.rb
@@ -9,12 +9,15 @@ RSpec.describe ForkNamespaceEntity do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:namespace) { create(:group, :with_avatar, description: 'test') }
+ let_it_be(:forked_project) { build(:project) }
let(:memberships) do
user.members.index_by(&:source_id)
end
- let(:entity) { described_class.new(namespace, current_user: user, project: project, memberships: memberships) }
+ let(:forked_projects) { { namespace.id => forked_project } }
+
+ let(:entity) { described_class.new(namespace, current_user: user, project: project, memberships: memberships, forked_projects: forked_projects) }
subject(:json) { entity.as_json }
@@ -46,10 +49,7 @@ RSpec.describe ForkNamespaceEntity do
end
it 'exposes forked_project_path when fork exists in namespace' do
- namespace.add_maintainer(user)
- fork_in_namespace = fork_project(project, user, namespace: namespace)
-
- expect(json[:forked_project_path]).to eql project_path(fork_in_namespace)
+ expect(json[:forked_project_path]).to eql project_path(forked_project)
end
it 'exposes relative path to the namespace' do
@@ -60,15 +60,25 @@ RSpec.describe ForkNamespaceEntity do
expect(json[:permission]).to eql 'Developer'
end
- it 'sets can_create_project to true when user can create projects in namespace' do
- allow(user).to receive(:can?).with(:create_projects, namespace).and_return(true)
-
+ it 'exposes can_create_project' do
expect(json[:can_create_project]).to be true
end
- it 'sets can_create_project to false when user is not allowed create projects in namespace' do
- allow(user).to receive(:can?).with(:create_projects, namespace).and_return(false)
+ context 'when fork_project_form feature flag is disabled' do
+ before do
+ stub_feature_flags(fork_project_form: false)
+ end
+
+ it 'sets can_create_project to true when user can create projects in namespace' do
+ allow(user).to receive(:can?).with(:create_projects, namespace).and_return(true)
- expect(json[:can_create_project]).to be false
+ expect(json[:can_create_project]).to be true
+ end
+
+ it 'sets can_create_project to false when user is not allowed create projects in namespace' do
+ allow(user).to receive(:can?).with(:create_projects, namespace).and_return(false)
+
+ expect(json[:can_create_project]).to be false
+ end
end
end
diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb
index 138b8174d81..30423ceba6d 100644
--- a/spec/serializers/issue_board_entity_spec.rb
+++ b/spec/serializers/issue_board_entity_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe IssueBoardEntity do
it 'has basic attributes' do
expect(subject).to include(:id, :iid, :title, :confidential, :due_date, :project_id, :relative_position,
- :labels, :assignees, project: hash_including(:id, :path))
+ :labels, :assignees, project: hash_including(:id, :path, :path_with_namespace))
end
it 'has path and endpoints' do
diff --git a/spec/serializers/member_serializer_spec.rb b/spec/serializers/member_serializer_spec.rb
index 687d69f86ea..bc256432c46 100644
--- a/spec/serializers/member_serializer_spec.rb
+++ b/spec/serializers/member_serializer_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe MemberSerializer do
it_behaves_like 'members.json'
it 'does not invoke group owner assignment' do
- expect(Members::LastGroupOwnerAssigner).not_to receive(:new)
+ expect(LastGroupOwnerAssigner).not_to receive(:new)
representation
end
diff --git a/spec/serializers/merge_request_diff_entity_spec.rb b/spec/serializers/merge_request_diff_entity_spec.rb
index a3b356505b8..9bf95e68874 100644
--- a/spec/serializers/merge_request_diff_entity_spec.rb
+++ b/spec/serializers/merge_request_diff_entity_spec.rb
@@ -10,8 +10,16 @@ RSpec.describe MergeRequestDiffEntity do
let(:merge_request_diffs) { merge_request.merge_request_diffs }
let(:merge_request_diff) { merge_request_diffs.first }
- let(:entity) do
- described_class.new(merge_request_diff, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
+ let(:entity) { initialize_entity(merge_request, merge_request_diff) }
+
+ def initialize_entity(merge_request, merge_request_diff)
+ described_class.new(
+ merge_request_diff,
+ request: request,
+ merge_request: merge_request,
+ merge_request_diff: merge_request_diff,
+ merge_request_diffs: merge_request_diffs
+ )
end
subject { entity.as_json }
@@ -26,6 +34,62 @@ RSpec.describe MergeRequestDiffEntity do
end
end
+ describe '#version_index' do
+ shared_examples 'version_index is nil' do
+ it 'returns nil' do
+ expect(subject[:version_index]).to be_nil
+ end
+ end
+
+ context 'when diff is not present' do
+ let(:entity) do
+ described_class.new(
+ merge_request_diff,
+ request: request,
+ merge_request: merge_request,
+ merge_request_diffs: merge_request_diffs
+ )
+ end
+
+ it_behaves_like 'version_index is nil'
+ end
+
+ context 'when diff is not included in @merge_request_diffs' do
+ let(:merge_request_diff) { create(:merge_request_diff) }
+ let(:merge_request_diff_2) { create(:merge_request_diff) }
+
+ before do
+ merge_request_diffs << merge_request_diff_2
+ end
+
+ it_behaves_like 'version_index is nil'
+ end
+
+ context 'when @merge_request_diffs.size <= 1' do
+ before do
+ expect(merge_request_diffs.size).to eq(1)
+ end
+
+ it_behaves_like 'version_index is nil'
+ end
+
+ context 'when @merge_request_diffs.size > 1' do
+ let(:merge_request) { create(:merge_request_with_multiple_diffs) }
+
+ it 'returns difference between size and diff index' do
+ expect(merge_request_diffs.size).to eq(2)
+
+ # diff index: 0
+ subject = initialize_entity(merge_request, merge_request_diffs.first)
+ expect(subject.as_json[:version_index]).to eq(2)
+
+ # diff index: 1
+ subject = initialize_entity(merge_request, merge_request_diffs.last)
+ expect(subject.as_json[:version_index]).to eq(1)
+ end
+ end
+ end
+
describe '#short_commit_sha' do
it 'returns short sha' do
expect(subject[:short_commit_sha]).to eq('b83d6e39')
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 1111290cade..bcad9eb6e23 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -212,18 +212,17 @@ RSpec.describe PipelineSerializer do
context 'with build environments' do
let(:ref) { 'feature' }
- it 'verifies number of queries', :request_store do
- stub_licensed_features(protected_environments: true)
+ let_it_be(:production) { create(:environment, :production, project: project) }
+ let_it_be(:staging) { create(:environment, :staging, project: project) }
- env = create(:environment, project: project)
- create(:ci_build, :scheduled, project: project, environment: env.name)
- create(:ci_build, :scheduled, project: project, environment: env.name)
- create(:ci_build, :scheduled, project: project, environment: env.name)
+ it 'executes one query to fetch all related environments', :request_store do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, :manual, pipeline: pipeline, environment: production.name)
+ create(:ci_build, :manual, pipeline: pipeline, environment: staging.name)
+ create(:ci_build, :scheduled, pipeline: pipeline, environment: production.name)
+ create(:ci_build, :scheduled, pipeline: pipeline, environment: staging.name)
- recorded = ActiveRecord::QueryRecorder.new { subject }
- expected_queries = Gitlab.ee? ? 56 : 52
- expect(recorded.count).to be_within(1).of(expected_queries)
- expect(recorded.cached_count).to eq(0)
+ expect { subject }.not_to exceed_query_limit(1).for_query /SELECT "environments".*/
end
end
diff --git a/spec/services/admin/propagate_service_template_spec.rb b/spec/services/admin/propagate_service_template_spec.rb
index 406da790a66..1bcf9af78ce 100644
--- a/spec/services/admin/propagate_service_template_spec.rb
+++ b/spec/services/admin/propagate_service_template_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Admin::PropagateServiceTemplate do
describe '.propagate' do
let_it_be(:project) { create(:project) }
let!(:service_template) do
- PushoverService.create!(
+ Integrations::Pushover.create!(
template: true,
active: true,
push_events: false,
diff --git a/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb
index c776e013fdf..782f6858870 100644
--- a/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb
+++ b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe AuthorizedProjectUpdate::PeriodicRecalculateService do
end
it 'calls AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker' do
- (1..User.maximum(:id)).each_slice(batch_size).with_index do |batch, index|
+ (1..User.maximum(:id)).each_slice(batch_size).with_index(1) do |batch, index|
delay = AuthorizedProjectUpdate::PeriodicRecalculateService::DELAY_INTERVAL * index
expect(AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker).to(
diff --git a/spec/services/authorized_project_update/project_recalculate_service_spec.rb b/spec/services/authorized_project_update/project_recalculate_service_spec.rb
new file mode 100644
index 00000000000..c339faaeabf
--- /dev/null
+++ b/spec/services/authorized_project_update/project_recalculate_service_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuthorizedProjectUpdate::ProjectRecalculateService, '#execute' do
+ let_it_be(:project) { create(:project) }
+
+ subject(:execute) { described_class.new(project).execute }
+
+ it 'returns success' do
+ expect(execute.success?).to eq(true)
+ end
+
+ context 'when there are no changes to be made' do
+ it 'does not change authorizations' do
+ expect { execute }.not_to(change { ProjectAuthorization.count })
+ end
+ end
+
+ context 'when there are changes to be made' do
+ let(:user) { create(:user) }
+
+ context 'when addition is required' do
+ before do
+ project.add_developer(user)
+ project.project_authorizations.where(user: user).delete_all
+ end
+
+ it 'adds a new authorization record' do
+ expect { execute }.to(
+ change { project.project_authorizations.where(user: user).count }
+ .from(0).to(1)
+ )
+ end
+
+ it 'adds a new authorization record with the correct access level' do
+ execute
+
+ project_authorization = project.project_authorizations.where(
+ user: user,
+ access_level: Gitlab::Access::DEVELOPER
+ )
+
+ expect(project_authorization).to exist
+ end
+ end
+
+ context 'when removal is required' do
+ before do
+ create(:project_authorization, user: user, project: project)
+ end
+
+ it 'removes the authorization record' do
+ expect { execute }.to(
+ change { project.project_authorizations.where(user: user).count }
+ .from(1).to(0)
+ )
+ end
+ end
+
+ context 'when an update in access level is required' do
+ before do
+ project.add_developer(user)
+ project.project_authorizations.where(user: user).delete_all
+ create(:project_authorization, project: project, user: user, access_level: Gitlab::Access::GUEST)
+ end
+
+ it 'updates the authorization of the user to the correct access level' do
+ expect { execute }.to(
+ change { project.project_authorizations.find_by(user: user).access_level }
+ .from(Gitlab::Access::GUEST).to(Gitlab::Access::DEVELOPER)
+ )
+ end
+ end
+ end
+end
diff --git a/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb b/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb
deleted file mode 100644
index 95e2c0380bf..00000000000
--- a/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe AuthorizedProjectUpdate::RecalculateForUserRangeService do
- describe '#execute' do
- let_it_be(:users) { create_list(:user, 2) }
-
- it 'calls Users::RefreshAuthorizedProjectsService' do
- user_ids = users.map(&:id)
-
- User.where(id: user_ids).select(:id).each do |user|
- expect(Users::RefreshAuthorizedProjectsService).to(
- receive(:new).with(user, source: described_class.name).and_call_original)
- end
-
- range = user_ids.minmax
- described_class.new(*range).execute
- end
- end
-end
diff --git a/spec/services/bulk_imports/file_decompression_service_spec.rb b/spec/services/bulk_imports/file_decompression_service_spec.rb
new file mode 100644
index 00000000000..4e8f78c8243
--- /dev/null
+++ b/spec/services/bulk_imports/file_decompression_service_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FileDecompressionService do
+ let_it_be(:tmpdir) { Dir.mktmpdir }
+ let_it_be(:ndjson_filename) { 'labels.ndjson' }
+ let_it_be(:ndjson_filepath) { File.join(tmpdir, ndjson_filename) }
+ let_it_be(:gz_filename) { "#{ndjson_filename}.gz" }
+ let_it_be(:gz_filepath) { "spec/fixtures/bulk_imports/gz/#{gz_filename}" }
+
+ before do
+ FileUtils.copy_file(gz_filepath, File.join(tmpdir, gz_filename))
+ FileUtils.remove_entry(ndjson_filepath) if File.exist?(ndjson_filepath)
+ end
+
+ after(:all) do
+ FileUtils.remove_entry(tmpdir)
+ end
+
+ subject { described_class.new(dir: tmpdir, filename: gz_filename) }
+
+ describe '#execute' do
+ it 'decompresses specified file' do
+ subject.execute
+
+ expect(File.exist?(File.join(tmpdir, ndjson_filename))).to eq(true)
+ expect(File.open(ndjson_filepath, &:readline)).to include('title', 'description')
+ end
+
+ context 'when validate_import_decompressed_archive_size feature flag is enabled' do
+ before do
+ stub_feature_flags(validate_import_decompressed_archive_size: true)
+ end
+
+ it 'performs decompressed file size validation' do
+ expect_next_instance_of(Gitlab::ImportExport::DecompressedArchiveSizeValidator) do |validator|
+ expect(validator).to receive(:valid?).and_return(true)
+ end
+
+ subject.execute
+ end
+ end
+
+ context 'when validate_import_decompressed_archive_size feature flag is disabled' do
+ before do
+ stub_feature_flags(validate_import_decompressed_archive_size: false)
+ end
+
+ it 'does not perform decompressed file size validation' do
+ expect(Gitlab::ImportExport::DecompressedArchiveSizeValidator).not_to receive(:new)
+
+ subject.execute
+ end
+ end
+
+ context 'when dir is not in tmpdir' do
+ subject { described_class.new(dir: '/etc', filename: 'filename') }
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid target directory')
+ end
+ end
+
+ context 'when compressed file is a symlink' do
+ let_it_be(:symlink) { File.join(tmpdir, 'symlink.gz') }
+
+ before do
+ FileUtils.ln_s(File.join(tmpdir, gz_filename), symlink)
+ end
+
+ subject { described_class.new(dir: tmpdir, filename: 'symlink.gz') }
+
+ it 'raises an error and removes the file' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid file')
+
+ expect(File.exist?(symlink)).to eq(false)
+ end
+ end
+
+ context 'when decompressed file is a symlink' do
+ let_it_be(:symlink) { File.join(tmpdir, 'symlink') }
+
+ before do
+ FileUtils.ln_s(File.join(tmpdir, ndjson_filename), symlink)
+
+ subject.instance_variable_set(:@decompressed_filepath, symlink)
+ end
+
+ subject { described_class.new(dir: tmpdir, filename: gz_filename) }
+
+ it 'raises an error and removes the file' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid file')
+
+ expect(File.exist?(symlink)).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb
new file mode 100644
index 00000000000..0961ddce553
--- /dev/null
+++ b/spec/services/bulk_imports/file_download_service_spec.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::FileDownloadService do
+ describe '#execute' do
+ let_it_be(:config) { build(:bulk_import_configuration) }
+ let_it_be(:content_type) { 'application/octet-stream' }
+ let_it_be(:filename) { 'file_download_service_spec' }
+ let_it_be(:tmpdir) { Dir.tmpdir }
+ let_it_be(:filepath) { File.join(tmpdir, filename) }
+
+ let(:chunk_double) { double('chunk', size: 1000, code: 200) }
+ let(:response_double) do
+ double(
+ code: 200,
+ success?: true,
+ parsed_response: {},
+ headers: {
+ 'content-length' => 100,
+ 'content-type' => content_type
+ }
+ )
+ end
+
+ subject { described_class.new(configuration: config, relative_url: '/test', dir: tmpdir, filename: filename) }
+
+ before do
+ allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
+ allow(client).to receive(:head).and_return(response_double)
+ allow(client).to receive(:stream).and_yield(chunk_double)
+ end
+ end
+
+ shared_examples 'downloads file' do
+ it 'downloads file' do
+ subject.execute
+
+ expect(File.exist?(filepath)).to eq(true)
+ expect(File.read(filepath)).to include('chunk')
+ end
+ end
+
+ include_examples 'downloads file'
+
+ context 'when content-type is application/gzip' do
+ let_it_be(:content_type) { 'application/gzip' }
+
+ include_examples 'downloads file'
+ end
+
+ context 'when url is not valid' do
+ it 'raises an error' do
+ stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
+
+ double = instance_double(BulkImports::Configuration, url: 'https://localhost', access_token: 'token')
+ service = described_class.new(configuration: double, relative_url: '/test', dir: tmpdir, filename: filename)
+
+ expect { service.execute }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
+ end
+ end
+
+ context 'when content-type is not valid' do
+ let(:content_type) { 'invalid' }
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid content type')
+ end
+ end
+
+ context 'when content-length is not valid' do
+ context 'when content-length exceeds limit' do
+ before do
+ stub_const("#{described_class}::FILE_SIZE_LIMIT", 1)
+ end
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid content length')
+ end
+ end
+
+ context 'when content-length is missing' do
+ let(:response_double) { double(success?: true, headers: { 'content-type' => content_type }) }
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid content length')
+ end
+ end
+ end
+
+ context 'when partially downloaded file exceeds limit' do
+ before do
+ stub_const("#{described_class}::FILE_SIZE_LIMIT", 150)
+ end
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid downloaded file')
+ end
+ end
+
+ context 'when chunk code is not 200' do
+ let(:chunk_double) { double('chunk', size: 1000, code: 307) }
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'File download error 307')
+ end
+ end
+
+ context 'when file is a symlink' do
+ let_it_be(:symlink) { File.join(tmpdir, 'symlink') }
+
+ before do
+ FileUtils.ln_s(File.join(tmpdir, filename), symlink)
+ end
+
+ subject { described_class.new(configuration: config, relative_url: '/test', dir: tmpdir, filename: 'symlink') }
+
+ it 'raises an error and removes the file' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid downloaded file')
+
+ expect(File.exist?(symlink)).to eq(false)
+ end
+ end
+
+ context 'when dir is not in tmpdir' do
+ subject { described_class.new(configuration: config, relative_url: '/test', dir: '/etc', filename: filename) }
+
+ it 'raises an error' do
+ expect { subject.execute }.to raise_error(described_class::ServiceError, 'Invalid target directory')
+ end
+ end
+ end
+end
diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb
index bf286998df2..333cd9201d8 100644
--- a/spec/services/bulk_imports/relation_export_service_spec.rb
+++ b/spec/services/bulk_imports/relation_export_service_spec.rb
@@ -62,7 +62,7 @@ RSpec.describe BulkImports::RelationExportService do
let(:upload) { create(:bulk_import_export_upload, export: export) }
it 'removes existing export before exporting' do
- upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/labels.ndjson.gz'))
+ upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz'))
expect_any_instance_of(BulkImports::ExportUpload) do |upload|
expect(upload).to receive(:remove_export_file!)
diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb
index cd50a2a5708..a866e0852bc 100644
--- a/spec/services/bulk_update_integration_service_spec.rb
+++ b/spec/services/bulk_update_integration_service_spec.rb
@@ -17,14 +17,14 @@ RSpec.describe BulkUpdateIntegrationService do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:group_integration) do
- JiraService.create!(
+ Integrations::Jira.create!(
group: group,
url: 'http://group.jira.com'
)
end
let_it_be(:subgroup_integration) do
- JiraService.create!(
+ Integrations::Jira.create!(
inherit_from_id: group_integration.id,
group: subgroup,
url: 'http://subgroup.jira.com',
@@ -33,7 +33,7 @@ RSpec.describe BulkUpdateIntegrationService do
end
let_it_be(:excluded_integration) do
- JiraService.create!(
+ Integrations::Jira.create!(
group: create(:group),
url: 'http://another.jira.com',
push_events: false
@@ -41,7 +41,7 @@ RSpec.describe BulkUpdateIntegrationService do
end
let_it_be(:integration) do
- JiraService.create!(
+ Integrations::Jira.create!(
project: create(:project, group: subgroup),
inherit_from_id: subgroup_integration.id,
url: 'http://project.jira.com',
diff --git a/spec/services/ci/append_build_trace_service_spec.rb b/spec/services/ci/append_build_trace_service_spec.rb
index a0a7f594881..8812680b665 100644
--- a/spec/services/ci/append_build_trace_service_spec.rb
+++ b/spec/services/ci/append_build_trace_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Ci::AppendBuildTraceService do
expect(::Gitlab::ErrorTracking)
.to receive(:log_exception)
- .with(anything, hash_including(chunk_index: 0, chunk_store: 'redis'))
+ .with(anything, hash_including(chunk_index: 0, chunk_store: 'redis_trace_chunks'))
result = described_class
.new(build, content_range: '0-128')
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
index 8bab7856375..18bd59a17f0 100644
--- a/spec/services/ci/create_downstream_pipeline_service_spec.rb
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
bridge_id: bridge.id, project_id: bridge.project.id)
.and_call_original
expect(Ci::CreatePipelineService).not_to receive(:new)
- expect(service.execute(bridge)).to be_nil
+ expect(service.execute(bridge)).to eq({ message: "Already has a downstream pipeline", status: :error })
end
end
@@ -393,6 +393,51 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
end
end
end
+
+ context 'when multi-project pipeline runs from child pipelines bridge job' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(rspec: { script: 'rspec' }))
+ end
+
+ # instantiate new service, to clear memoized values from child pipeline run
+ subject(:execute_with_trigger_project_bridge) do
+ described_class.new(upstream_project, user).execute(trigger_project_bridge)
+ end
+
+ let!(:child_pipeline) do
+ service.execute(bridge)
+ bridge.downstream_pipeline
+ end
+
+ let!(:trigger_downstream_project) do
+ {
+ trigger: {
+ project: downstream_project.full_path,
+ branch: 'feature'
+ }
+ }
+ end
+
+ let!(:trigger_project_bridge) do
+ create(
+ :ci_bridge, status: :pending,
+ user: user,
+ options: trigger_downstream_project,
+ pipeline: child_pipeline
+ )
+ end
+
+ it 'creates a new pipeline' do
+ expect { execute_with_trigger_project_bridge }
+ .to change { Ci::Pipeline.count }.by(1)
+
+ new_pipeline = trigger_project_bridge.downstream_pipeline
+
+ expect(new_pipeline.child?).to eq(false)
+ expect(new_pipeline.triggered_by_pipeline).to eq child_pipeline
+ expect(trigger_project_bridge.reload).not_to be_failed
+ end
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
index 9ccf289df7c..7193e5bd7d4 100644
--- a/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
+++ b/spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb
@@ -14,7 +14,6 @@ RSpec.describe Ci::CreatePipelineService do
before do
stub_ci_pipeline_yaml_file(config)
- stub_feature_flags(ci_raise_job_rules_without_workflow_rules_warning: true)
end
context 'when created successfully' do
@@ -35,18 +34,6 @@ RSpec.describe Ci::CreatePipelineService do
/jobs:test may allow multiple pipelines to run/
)
end
-
- context 'when feature flag is disabled for the particular warning' do
- before do
- stub_feature_flags(ci_raise_job_rules_without_workflow_rules_warning: false)
- end
-
- it 'does not contain warnings' do
- expect(pipeline.error_messages.map(&:content)).to be_empty
-
- expect(pipeline.warning_messages.map(&:content)).to be_empty
- end
- end
end
context 'when no warnings are raised' do
diff --git a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
index b3b8e34dd8e..7fd32288893 100644
--- a/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
+++ b/spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb
@@ -53,6 +53,8 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
end
context 'when sidekiq processes the job', :sidekiq_inline do
+ let_it_be(:runner) { create(:ci_runner, :online) }
+
it 'transitions to pending status and triggers a downstream pipeline' do
pipeline = create_pipeline!
diff --git a/spec/services/ci/create_pipeline_service/needs_spec.rb b/spec/services/ci/create_pipeline_service/needs_spec.rb
index 4521067cd52..3b4a6178b8f 100644
--- a/spec/services/ci/create_pipeline_service/needs_spec.rb
+++ b/spec/services/ci/create_pipeline_service/needs_spec.rb
@@ -211,7 +211,7 @@ RSpec.describe Ci::CreatePipelineService do
deploy_a = processables.find { |processable| processable.name == 'deploy_a' }
deploy_b = processables.find { |processable| processable.name == 'deploy_b' }
- expect(pipeline).to be_persisted
+ expect(pipeline).to be_created_successfully
expect(build_a.status).to eq('pending')
expect(test_a.status).to eq('created')
expect(test_b.status).to eq('pending')
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 9fdce1ae926..052727401dd 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Ci::CreatePipelineService do
let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:user, reload: true) { project.owner }
+ let_it_be(:runner) { create(:ci_runner, :online, tag_list: %w[postgres mysql ruby]) }
let(:ref_name) { 'refs/heads/master' }
@@ -532,7 +533,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'pull it from Auto-DevOps' do
pipeline = execute_service
expect(pipeline).to be_auto_devops_source
- expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality eslint-sast secret_detection_default_branch semgrep-sast test])
+ expect(pipeline.builds.map(&:name)).to match_array(%w[brakeman-sast build code_quality eslint-sast secret_detection semgrep-sast test])
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 97c65dc005e..e6d9f208096 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -203,53 +203,6 @@ RSpec.describe Ci::JobArtifacts::CreateService do
end
end
- context 'when artifact type is cluster_applications' do
- let(:artifacts_file) do
- file_to_upload('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz', sha256: artifacts_sha256)
- end
-
- let(:params) do
- {
- 'artifact_type' => 'cluster_applications',
- 'artifact_format' => 'gzip'
- }.with_indifferent_access
- end
-
- it 'calls cluster applications parse service' do
- expect_next_instance_of(Clusters::ParseClusterApplicationsArtifactService) do |service|
- expect(service).to receive(:execute).once.and_call_original
- end
-
- subject
- end
-
- context 'when there is a deployment cluster' do
- let(:user) { project.owner }
-
- before do
- job.update!(user: user)
- end
-
- it 'calls cluster applications parse service with job and job user', :aggregate_failures do
- expect(Clusters::ParseClusterApplicationsArtifactService).to receive(:new).with(job, user).and_call_original
-
- subject
- end
- end
-
- context 'when ci_synchronous_artifact_parsing feature flag is disabled' do
- before do
- stub_feature_flags(ci_synchronous_artifact_parsing: false)
- end
-
- it 'does not call parse service' do
- expect(Clusters::ParseClusterApplicationsArtifactService).not_to receive(:new)
-
- expect(subject[:status]).to eq(:success)
- end
- end
- end
-
shared_examples 'rescues object storage error' do |klass, message, expected_message|
it "handles #{klass}" do
allow_next_instance_of(JobArtifactUploader) do |uploader|
diff --git a/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb b/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb
new file mode 100644
index 00000000000..2aa810e8ea1
--- /dev/null
+++ b/spec/services/ci/pipeline_creation/start_pipeline_service_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineCreation::StartPipelineService do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ subject(:service) { described_class.new(pipeline) }
+
+ describe '#execute' do
+ it 'calls the pipeline process service' do
+ expect(Ci::ProcessPipelineService)
+ .to receive(:new)
+ .with(pipeline)
+ .and_return(double('service', execute: true))
+
+ service.execute
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_processing/shared_processing_service.rb b/spec/services/ci/pipeline_processing/shared_processing_service.rb
index 13c924a3089..34d9b60217f 100644
--- a/spec/services/ci/pipeline_processing/shared_processing_service.rb
+++ b/spec/services/ci/pipeline_processing/shared_processing_service.rb
@@ -859,6 +859,8 @@ RSpec.shared_examples 'Pipeline Processing Service' do
end
context 'when a bridge job has parallel:matrix config', :sidekiq_inline do
+ let_it_be(:runner) { create(:ci_runner, :online) }
+
let(:parent_config) do
<<-EOY
test:
diff --git a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
index 572808cd2db..9c8e6fd3292 100644
--- a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
+++ b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
@@ -3,6 +3,7 @@
RSpec.shared_context 'Pipeline Processing Service Tests With Yaml' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
+ let_it_be(:runner) { create(:ci_runner, :online) }
where(:test_file_path) do
Dir.glob(Rails.root.join('spec/services/ci/pipeline_processing/test_cases/*.yml'))
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 839a3c53f07..c4b1e2133ed 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -11,9 +11,37 @@ module Ci
let!(:shared_runner) { create(:ci_runner, :instance) }
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
- let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
describe '#execute' do
+ context 'checks database loadbalancing stickiness' do
+ subject { described_class.new(shared_runner).execute }
+
+ before do
+ project.update!(shared_runners_enabled: false)
+ end
+
+ it 'result is valid if replica did caught-up' do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?)
+ .and_return(true)
+
+ expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:all_caught_up?)
+ .with(:runner, shared_runner.id) { true }
+
+ expect(subject).to be_valid
+ end
+
+ it 'result is invalid if replica did not caught-up' do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?)
+ .and_return(true)
+
+ expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:all_caught_up?)
+ .with(:runner, shared_runner.id) { false }
+
+ expect(subject).not_to be_valid
+ end
+ end
+
shared_examples 'handles runner assignment' do
context 'runner follow tag list' do
it "picks build with the same tag" do
@@ -76,11 +104,11 @@ module Ci
let!(:project3) { create :project, shared_runners_enabled: true }
let!(:pipeline3) { create :ci_pipeline, project: project3 }
let!(:build1_project1) { pending_job }
- let!(:build2_project1) { FactoryBot.create :ci_build, pipeline: pipeline }
- let!(:build3_project1) { FactoryBot.create :ci_build, pipeline: pipeline }
- let!(:build1_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 }
- let!(:build2_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 }
- let!(:build1_project3) { FactoryBot.create :ci_build, pipeline: pipeline3 }
+ 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) }
context 'when using fair scheduling' do
context 'when all builds are pending' do
@@ -227,17 +255,17 @@ module Ci
let!(:pipeline3) { create(:ci_pipeline, project: project3) }
let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create(:ci_build, pipeline: pipeline) }
- let!(:build3_project1) { create(:ci_build, pipeline: pipeline) }
- let!(:build1_project2) { create(:ci_build, pipeline: pipeline2) }
- let!(:build2_project2) { create(:ci_build, pipeline: pipeline2) }
- let!(:build1_project3) { create(:ci_build, pipeline: pipeline3) }
+ 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, pipeline: unrelated_pipeline) }
+ 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
@@ -318,7 +346,7 @@ module Ci
subject { described_class.new(specific_runner).execute }
context 'with multiple builds are in queue' do
- let!(:other_build) { create :ci_build, pipeline: pipeline }
+ let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
@@ -359,7 +387,7 @@ module Ci
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
it 'picks the job' do
expect(execute(specific_runner)).to eq(pending_job)
@@ -367,7 +395,7 @@ module Ci
end
context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
it 'picks the job' do
expect(execute(specific_runner)).to eq(pending_job)
@@ -375,7 +403,7 @@ module Ci
end
context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do
pending_job.update_attribute(:protected, nil)
@@ -391,7 +419,7 @@ module Ci
let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
context 'when a job is protected' do
- let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) }
it 'picks the job' do
expect(execute(specific_runner)).to eq(pending_job)
@@ -399,7 +427,7 @@ module Ci
end
context 'when a job is unprotected' do
- let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
it 'does not pick the job' do
expect(execute(specific_runner)).to be_nil
@@ -407,7 +435,7 @@ module Ci
end
context 'when protected attribute of a job is nil' do
- let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do
pending_job.update_attribute(:protected, nil)
@@ -421,7 +449,7 @@ module Ci
context 'runner feature set is verified' do
let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } }
- let!(:pending_job) { create(:ci_build, :pending, pipeline: pipeline, options: options) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
subject { execute(specific_runner, params) }
@@ -457,7 +485,7 @@ module Ci
shared_examples 'validation is active' do
context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
it { expect(subject).to eq(pending_job) }
end
@@ -494,7 +522,7 @@ module Ci
shared_examples 'validation is not active' do
context 'when depended job has not been completed yet' do
- let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
it { expect(subject).to eq(pending_job) }
end
@@ -519,7 +547,7 @@ module Ci
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
let!(:pending_job) do
- create(:ci_build, :pending,
+ create(:ci_build, :pending, :queued,
pipeline: pipeline, stage_idx: 1,
options: { script: ["bash"], dependencies: ['test'] })
end
@@ -530,7 +558,7 @@ module Ci
end
context 'when build is degenerated' do
- let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
subject { execute(specific_runner, {}) }
@@ -545,7 +573,7 @@ module Ci
context 'when build has data integrity problem' do
let!(:pending_job) do
- create(:ci_build, :pending, pipeline: pipeline)
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
end
before do
@@ -570,7 +598,7 @@ module Ci
context 'when build fails to be run!' do
let!(:pending_job) do
- create(:ci_build, :pending, pipeline: pipeline)
+ create(:ci_build, :pending, :queued, pipeline: pipeline)
end
before do
@@ -612,12 +640,12 @@ module Ci
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, pipeline: pipeline, 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, pipeline: pipeline, tag_list: %w[matching])
- create(:ci_build, pipeline: pipeline, tag_list: %w[non-matching])
+ 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 size of only matching jobs' do
@@ -665,7 +693,7 @@ module Ci
end
context 'when there is another build in queue' do
- let!(:next_pending_job) { create(:ci_build, pipeline: pipeline) }
+ 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)
@@ -683,11 +711,7 @@ module Ci
end
end
- context 'when ci_register_job_service_one_by_one is enabled' do
- before do
- stub_feature_flags(ci_register_job_service_one_by_one: true)
- end
-
+ context 'when a long queue is created' do
it 'picks builds one-by-one' do
expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original
@@ -697,9 +721,17 @@ module Ci
include_examples 'handles runner assignment'
end
- context 'when ci_register_job_service_one_by_one is disabled' do
+ context 'when joining with pending builds table' do
+ before do
+ stub_feature_flags(ci_pending_builds_queue_join: true)
+ end
+
+ include_examples 'handles runner assignment'
+ end
+
+ context 'when not joining with pending builds table' do
before do
- stub_feature_flags(ci_register_job_service_one_by_one: false)
+ stub_feature_flags(ci_pending_builds_queue_join: false)
end
include_examples 'handles runner assignment'
@@ -747,8 +779,8 @@ module Ci
end
context 'when project already has running jobs' do
- let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
- let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+ let!(:build2) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
+ let!(:build3) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) }
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
@@ -831,42 +863,21 @@ module Ci
end
context 'when max queue depth is reached' do
- let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
- let!(:pending_job_2) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
- let!(:pending_job_3) { create(:ci_build, :pending, pipeline: pipeline) }
+ let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
+ let!(:pending_job_2) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) }
+ let!(:pending_job_3) { create(:ci_build, :pending, :queued, pipeline: pipeline) }
before do
stub_const("#{described_class}::MAX_QUEUE_DEPTH", 2)
end
- context 'when feature is enabled' do
- before do
- stub_feature_flags(gitlab_ci_builds_queue_limit: true)
- end
+ it 'returns 409 conflict' do
+ expect(Ci::Build.pending.unstarted.count).to eq 3
- 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(specific_runner).execute
-
- expect(result).not_to be_valid
- expect(result.build).to be_nil
- end
- end
-
- context 'when feature is disabled' do
- before do
- stub_feature_flags(gitlab_ci_builds_queue_limit: false)
- end
-
- it 'returns a valid result' do
- expect(Ci::Build.pending.unstarted.count).to eq 3
-
- result = described_class.new(specific_runner).execute
-
- expect(result).to be_valid
- expect(result.build).to eq pending_job_3
- end
+ expect(result).not_to be_valid
+ expect(result.build).to be_nil
end
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 86bda868625..c71bec31984 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -40,7 +40,7 @@ RSpec.describe Ci::RetryBuildService do
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_secret_detection job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
- job_artifacts_license_management job_artifacts_license_scanning
+ job_artifacts_license_scanning
job_artifacts_performance job_artifacts_browser_performance job_artifacts_load_performance
job_artifacts_lsif job_artifacts_terraform job_artifacts_cluster_applications
job_artifacts_codequality job_artifacts_metrics scheduled_at
@@ -59,13 +59,14 @@ RSpec.describe Ci::RetryBuildService do
metadata 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].freeze
+ pipeline_id report_results pending_state pages_deployments
+ queuing_entry runtime_metadata].freeze
shared_examples 'build duplication' do
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be(:build) do
- create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags,
+ create(:ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags,
:allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group,
description: 'my-job', stage: 'test', stage_id: stage.id,
pipeline: pipeline, auto_canceled_by: another_pipeline,
@@ -73,9 +74,6 @@ RSpec.describe Ci::RetryBuildService do
end
before_all do
- # Test correctly behaviour of deprecated artifact because it can be still in use
- stub_feature_flags(drop_license_management_artifact: false)
-
# Make sure that build has both `stage_id` and `stage` because FactoryBot
# can reset one of the fields when assigning another. We plan to deprecate
# and remove legacy `stage` column in the future.
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index 2d9f80a249d..44d7809b85f 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -4,154 +4,344 @@ require 'spec_helper'
RSpec.describe Ci::UpdateBuildQueueService do
let(:project) { create(:project, :repository) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ describe 'pending builds queue push / pop' do
+ describe '#push' do
+ let(:transition) { double('transition') }
+
+ before do
+ allow(transition).to receive(:to).and_return('pending')
+ allow(transition).to receive(:within_transaction).and_yield
+ end
+
+ context 'when pending build can be created' do
+ it 'creates a new pending build in transaction' do
+ queued = subject.push(build, transition)
+
+ expect(queued).to eq build.id
+ end
+
+ it 'increments queue push metric' do
+ metrics = spy('metrics')
+
+ described_class.new(metrics).push(build, transition)
+
+ expect(metrics)
+ .to have_received(:increment_queue_operation)
+ .with(:build_queue_push)
+ end
+ end
- shared_examples 'refreshes runner' do
- it 'ticks runner queue value' do
- expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
+ context 'when invalid transition is detected' do
+ it 'raises an error' do
+ allow(transition).to receive(:to).and_return('created')
+
+ expect { subject.push(build, transition) }
+ .to raise_error(described_class::InvalidQueueTransition)
+ end
+ end
+
+ context 'when duplicate entry exists' do
+ before do
+ ::Ci::PendingBuild.create!(build: build, project: project)
+ end
+
+ it 'does nothing and returns build id' do
+ queued = subject.push(build, transition)
+
+ expect(queued).to eq build.id
+ end
+ end
end
- end
- shared_examples 'does not refresh runner' do
- it 'ticks runner queue value' do
- expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
+ describe '#pop' do
+ let(:transition) { double('transition') }
+
+ before do
+ allow(transition).to receive(:from).and_return('pending')
+ allow(transition).to receive(:within_transaction).and_yield
+ end
+
+ context 'when pending build exists' do
+ before do
+ Ci::PendingBuild.create!(build: build, project: project)
+ end
+
+ it 'removes pending build in a transaction' do
+ dequeued = subject.pop(build, transition)
+
+ expect(dequeued).to eq build.id
+ end
+
+ it 'increments queue pop metric' do
+ metrics = spy('metrics')
+
+ described_class.new(metrics).pop(build, transition)
+
+ expect(metrics)
+ .to have_received(:increment_queue_operation)
+ .with(:build_queue_pop)
+ end
+ end
+
+ context 'when pending build does not exist' do
+ it 'does nothing if there is no pending build to remove' do
+ dequeued = subject.pop(build, transition)
+
+ expect(dequeued).to be_nil
+ end
+ end
+
+ context 'when invalid transition is detected' do
+ it 'raises an error' do
+ allow(transition).to receive(:from).and_return('created')
+
+ expect { subject.pop(build, transition) }
+ .to raise_error(described_class::InvalidQueueTransition)
+ end
+ end
end
end
- shared_examples 'matching build' do
- context 'when there is a online runner that can pick build' do
+ describe 'shared runner builds tracking' do
+ let(:runner) { create(:ci_runner, :instance_type) }
+ let(:build) { create(:ci_build, runner: runner, pipeline: pipeline) }
+
+ describe '#track' do
+ let(:transition) { double('transition') }
+
before do
- runner.update!(contacted_at: 30.minutes.ago)
+ allow(transition).to receive(:to).and_return('running')
+ allow(transition).to receive(:within_transaction).and_yield
end
- it_behaves_like 'refreshes runner'
+ context 'when a shared runner build can be tracked' do
+ it 'creates a new shared runner build tracking entry' do
+ build_id = subject.track(build, transition)
+
+ expect(build_id).to eq build.id
+ end
+
+ it 'increments new shared runner build metric' do
+ metrics = spy('metrics')
- it 'avoids running redundant queries' do
- expect(Ci::Runner).not_to receive(:owned_or_instance_wide)
+ described_class.new(metrics).track(build, transition)
- subject.execute(build)
+ expect(metrics)
+ .to have_received(:increment_queue_operation)
+ .with(:shared_runner_build_new)
+ end
end
- context 'when feature flag ci_reduce_queries_when_ticking_runner_queue is disabled' do
+ context 'when invalid transition is detected' do
+ it 'raises an error' do
+ allow(transition).to receive(:to).and_return('pending')
+
+ expect { subject.track(build, transition) }
+ .to raise_error(described_class::InvalidQueueTransition)
+ end
+ end
+
+ context 'when duplicate entry exists' do
before do
- stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: false)
- stub_feature_flags(ci_runners_short_circuit_assignable_for: false)
+ ::Ci::RunningBuild.create!(
+ build: build, project: project, runner: runner, runner_type: runner.runner_type
+ )
end
- it 'runs redundant queries using `owned_or_instance_wide` scope' do
- expect(Ci::Runner).to receive(:owned_or_instance_wide).and_call_original
+ it 'does nothing and returns build id' do
+ build_id = subject.track(build, transition)
- subject.execute(build)
+ expect(build_id).to eq build.id
end
end
end
- end
- shared_examples 'mismatching tags' do
- context 'when there is no runner that can pick build due to tag mismatch' do
+ describe '#untrack' do
+ let(:transition) { double('transition') }
+
before do
- build.tag_list = [:docker]
+ allow(transition).to receive(:from).and_return('running')
+ allow(transition).to receive(:within_transaction).and_yield
end
- it_behaves_like 'does not refresh runner'
+ context 'when shared runner build tracking entry exists' do
+ before do
+ Ci::RunningBuild.create!(
+ build: build, project: project, runner: runner, runner_type: runner.runner_type
+ )
+ end
+
+ it 'removes shared runner build' do
+ build_id = subject.untrack(build, transition)
+
+ expect(build_id).to eq build.id
+ end
+
+ it 'increments shared runner build done metric' do
+ metrics = spy('metrics')
+
+ described_class.new(metrics).untrack(build, transition)
+
+ expect(metrics)
+ .to have_received(:increment_queue_operation)
+ .with(:shared_runner_build_done)
+ end
+ end
+
+ context 'when tracking entry does not exist' do
+ it 'does nothing if there is no tracking entry to remove' do
+ build_id = subject.untrack(build, transition)
+
+ expect(build_id).to be_nil
+ end
+ end
+
+ context 'when invalid transition is detected' do
+ it 'raises an error' do
+ allow(transition).to receive(:from).and_return('pending')
+
+ expect { subject.untrack(build, transition) }
+ .to raise_error(described_class::InvalidQueueTransition)
+ end
+ end
end
end
- shared_examples 'recent runner queue' do
- context 'when there is runner with expired cache' do
- before do
- runner.update!(contacted_at: Ci::Runner.recent_queue_deadline)
+ describe '#tick' do
+ shared_examples 'refreshes runner' do
+ it 'ticks runner queue value' do
+ expect { subject.tick(build) }.to change { runner.ensure_runner_queue_value }
end
+ end
- it_behaves_like 'does not refresh runner'
+ shared_examples 'does not refresh runner' do
+ it 'ticks runner queue value' do
+ expect { subject.tick(build) }.not_to change { runner.ensure_runner_queue_value }
+ end
end
- end
- context 'when updating specific runners' do
- let(:runner) { create(:ci_runner, :project, projects: [project]) }
+ shared_examples 'matching build' do
+ context 'when there is a online runner that can pick build' do
+ before do
+ runner.update!(contacted_at: 30.minutes.ago)
+ end
- it_behaves_like 'matching build'
- it_behaves_like 'mismatching tags'
- it_behaves_like 'recent runner queue'
+ it_behaves_like 'refreshes runner'
- context 'when the runner is assigned to another project' do
- let(:another_project) { create(:project) }
- let(:runner) { create(:ci_runner, :project, projects: [another_project]) }
+ it 'avoids running redundant queries' do
+ expect(Ci::Runner).not_to receive(:owned_or_instance_wide)
- it_behaves_like 'does not refresh runner'
+ subject.tick(build)
+ end
+ end
end
- end
- context 'when updating shared runners' do
- let(:runner) { create(:ci_runner, :instance) }
-
- it_behaves_like 'matching build'
- it_behaves_like 'mismatching tags'
- it_behaves_like 'recent runner queue'
+ shared_examples 'mismatching tags' do
+ context 'when there is no runner that can pick build due to tag mismatch' do
+ before do
+ build.tag_list = [:docker]
+ end
- context 'when there is no runner that can pick build due to being disabled on project' do
- before do
- build.project.shared_runners_enabled = false
+ it_behaves_like 'does not refresh runner'
end
+ end
- it_behaves_like 'does not refresh runner'
+ shared_examples 'recent runner queue' do
+ context 'when there is runner with expired cache' do
+ before do
+ runner.update!(contacted_at: Ci::Runner.recent_queue_deadline)
+ end
+
+ it_behaves_like 'does not refresh runner'
+ end
end
- end
- context 'when updating group runners' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
- let(:runner) { create(:ci_runner, :group, groups: [group]) }
+ context 'when updating specific runners' do
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
- it_behaves_like 'matching build'
- it_behaves_like 'mismatching tags'
- it_behaves_like 'recent runner queue'
+ it_behaves_like 'matching build'
+ it_behaves_like 'mismatching tags'
+ it_behaves_like 'recent runner queue'
- context 'when there is no runner that can pick build due to being disabled on project' do
- before do
- build.project.group_runners_enabled = false
- end
+ context 'when the runner is assigned to another project' do
+ let(:another_project) { create(:project) }
+ let(:runner) { create(:ci_runner, :project, projects: [another_project]) }
- it_behaves_like 'does not refresh runner'
+ it_behaves_like 'does not refresh runner'
+ end
end
- end
- context 'avoids N+1 queries', :request_store do
- let!(:build) { create(:ci_build, pipeline: pipeline, tag_list: %w[a b]) }
- let!(:project_runner) { create(:ci_runner, :project, :online, projects: [project], tag_list: %w[a b c]) }
+ context 'when updating shared runners' do
+ let(:runner) { create(:ci_runner, :instance) }
- context 'when ci_preload_runner_tags and ci_reduce_queries_when_ticking_runner_queue are enabled' do
- before do
- stub_feature_flags(
- ci_reduce_queries_when_ticking_runner_queue: true,
- ci_preload_runner_tags: true
- )
+ it_behaves_like 'matching build'
+ it_behaves_like 'mismatching tags'
+ it_behaves_like 'recent runner queue'
+
+ context 'when there is no runner that can pick build due to being disabled on project' do
+ before do
+ build.project.shared_runners_enabled = false
+ end
+
+ it_behaves_like 'does not refresh runner'
end
+ end
- it 'does execute the same amount of queries regardless of number of runners' do
- control_count = ActiveRecord::QueryRecorder.new { subject.execute(build) }.count
+ context 'when updating group runners' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
- create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d])
+ it_behaves_like 'matching build'
+ it_behaves_like 'mismatching tags'
+ it_behaves_like 'recent runner queue'
- expect { subject.execute(build) }.not_to exceed_all_query_limit(control_count)
+ context 'when there is no runner that can pick build due to being disabled on project' do
+ before do
+ build.project.group_runners_enabled = false
+ end
+
+ it_behaves_like 'does not refresh runner'
end
end
- context 'when ci_preload_runner_tags and ci_reduce_queries_when_ticking_runner_queue are disabled' do
- before do
- stub_feature_flags(
- ci_reduce_queries_when_ticking_runner_queue: false,
- ci_preload_runner_tags: false
- )
+ context 'avoids N+1 queries', :request_store do
+ let!(:build) { create(:ci_build, pipeline: pipeline, tag_list: %w[a b]) }
+ let!(:project_runner) { create(:ci_runner, :project, :online, projects: [project], tag_list: %w[a b c]) }
+
+ context 'when ci_preload_runner_tags is enabled' do
+ before do
+ stub_feature_flags(
+ ci_preload_runner_tags: true
+ )
+ end
+
+ it 'does execute the same amount of queries regardless of number of runners' do
+ control_count = ActiveRecord::QueryRecorder.new { subject.tick(build) }.count
+
+ create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d])
+
+ expect { subject.tick(build) }.not_to exceed_all_query_limit(control_count)
+ end
end
- it 'does execute more queries for more runners' do
- control_count = ActiveRecord::QueryRecorder.new { subject.execute(build) }.count
+ context 'when ci_preload_runner_tags are disabled' do
+ before do
+ stub_feature_flags(
+ ci_preload_runner_tags: false
+ )
+ end
+
+ it 'does execute more queries for more runners' do
+ control_count = ActiveRecord::QueryRecorder.new { subject.tick(build) }.count
- create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d])
+ create_list(:ci_runner, 10, :project, :online, projects: [project], tag_list: %w[b c d])
- expect { subject.execute(build) }.to exceed_all_query_limit(control_count)
+ expect { subject.tick(build) }.to exceed_all_query_limit(control_count)
+ end
end
end
end
diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb
index 63190cc5d49..5bb3843da8f 100644
--- a/spec/services/ci/update_build_state_service_spec.rb
+++ b/spec/services/ci/update_build_state_service_spec.rb
@@ -3,8 +3,9 @@
require 'spec_helper'
RSpec.describe Ci::UpdateBuildStateService do
- let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
let(:metrics) { spy('metrics') }
@@ -14,6 +15,24 @@ RSpec.describe Ci::UpdateBuildStateService do
stub_feature_flags(ci_enable_live_trace: true)
end
+ context 'when build has unknown failure reason' do
+ let(:params) do
+ {
+ output: { checksum: 'crc32:12345678', bytesize: 123 },
+ state: 'failed',
+ failure_reason: 'no idea here',
+ exit_code: 42
+ }
+ end
+
+ it 'updates a build status' do
+ result = subject.execute
+
+ expect(build).to be_failed
+ expect(result.status).to eq 200
+ end
+ end
+
context 'when build does not have checksum' do
context 'when state has changed' do
let(:params) { { state: 'success' } }
@@ -47,25 +66,6 @@ RSpec.describe Ci::UpdateBuildStateService do
end
end
- context 'when request payload carries a trace' do
- let(:params) { { state: 'success', trace: 'overwritten' } }
-
- it 'overwrites a trace' do
- result = subject.execute
-
- expect(build.trace.raw).to eq 'overwritten'
- expect(result.status).to eq 200
- end
-
- it 'updates overwrite operation metric' do
- execute_with_stubbed_metrics!
-
- expect(metrics)
- .to have_received(:increment_trace_operation)
- .with(operation: :overwrite)
- end
- end
-
context 'when state is unknown' do
let(:params) { { state: 'unknown' } }
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index f3b420510a6..eb907377ca8 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -46,8 +46,7 @@ RSpec.describe Clusters::Applications::CreateService do
context 'ingress application' do
let(:params) do
{
- application: 'ingress',
- modsecurity_enabled: true
+ application: 'ingress'
}
end
@@ -64,10 +63,6 @@ RSpec.describe Clusters::Applications::CreateService do
cluster.reload
end.to change(cluster, :application_ingress)
end
-
- it 'sets modsecurity_enabled' do
- expect(subject.modsecurity_enabled).to eq(true)
- end
end
context 'cert manager application' do
diff --git a/spec/services/clusters/cleanup/app_service_spec.rb b/spec/services/clusters/cleanup/app_service_spec.rb
deleted file mode 100644
index ea1194d2100..00000000000
--- a/spec/services/clusters/cleanup/app_service_spec.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Cleanup::AppService do
- describe '#execute' do
- let!(:cluster) { create(:cluster, :project, :cleanup_uninstalling_applications, provider_type: :gcp) }
- let(:service) { described_class.new(cluster) }
- let(:logger) { service.send(:logger) }
- let(:log_meta) do
- {
- service: described_class.name,
- cluster_id: cluster.id,
- execution_count: 0
- }
- end
-
- subject { service.execute }
-
- shared_examples 'does not reschedule itself' do
- it 'does not reschedule itself' do
- expect(Clusters::Cleanup::AppWorker).not_to receive(:perform_in)
- end
- end
-
- context 'when cluster has no applications available or transitioning applications' do
- it_behaves_like 'does not reschedule itself'
-
- it 'transitions cluster to cleanup_removing_project_namespaces' do
- expect { subject }
- .to change { cluster.reload.cleanup_status_name }
- .from(:cleanup_uninstalling_applications)
- .to(:cleanup_removing_project_namespaces)
- end
-
- it 'schedules Clusters::Cleanup::ProjectNamespaceWorker' do
- expect(Clusters::Cleanup::ProjectNamespaceWorker).to receive(:perform_async).with(cluster.id)
- subject
- end
-
- it 'logs all events' do
- expect(logger).to receive(:info)
- .with(log_meta.merge(event: :schedule_remove_project_namespaces))
-
- subject
- end
- end
-
- context 'when cluster has uninstallable applications' do
- shared_examples 'reschedules itself' do
- it 'reschedules itself' do
- expect(Clusters::Cleanup::AppWorker)
- .to receive(:perform_in)
- .with(1.minute, cluster.id, 1)
-
- subject
- end
- end
-
- context 'has applications with dependencies' do
- let!(:helm) { create(:clusters_applications_helm, :installed, cluster: cluster) }
- let!(:ingress) { create(:clusters_applications_ingress, :installed, cluster: cluster) }
- let!(:cert_manager) { create(:clusters_applications_cert_manager, :installed, cluster: cluster) }
- let!(:jupyter) { create(:clusters_applications_jupyter, :installed, cluster: cluster) }
-
- it_behaves_like 'reschedules itself'
-
- it 'only uninstalls apps that are not dependencies for other installed apps' do
- expect(Clusters::Applications::UninstallWorker)
- .to receive(:perform_async).with(helm.name, helm.id)
- .and_call_original
-
- expect(Clusters::Applications::UninstallWorker)
- .not_to receive(:perform_async).with(ingress.name, ingress.id)
-
- expect(Clusters::Applications::UninstallWorker)
- .to receive(:perform_async).with(cert_manager.name, cert_manager.id)
- .and_call_original
-
- expect(Clusters::Applications::UninstallWorker)
- .to receive(:perform_async).with(jupyter.name, jupyter.id)
- .and_call_original
-
- subject
- end
-
- it 'logs application uninstalls and next execution' do
- expect(logger).to receive(:info)
- .with(log_meta.merge(event: :uninstalling_app, application: kind_of(String))).exactly(3).times
- expect(logger).to receive(:info)
- .with(log_meta.merge(event: :scheduling_execution, next_execution: 1))
-
- subject
- end
-
- context 'cluster is not cleanup_uninstalling_applications' do
- let!(:cluster) { create(:cluster, :project, provider_type: :gcp) }
-
- it_behaves_like 'does not reschedule itself'
- end
- end
-
- context 'when applications are still uninstalling/scheduled/depending on others' do
- let!(:helm) { create(:clusters_applications_helm, :installed, cluster: cluster) }
- let!(:ingress) { create(:clusters_applications_ingress, :scheduled, cluster: cluster) }
- let!(:runner) { create(:clusters_applications_runner, :uninstalling, cluster: cluster) }
-
- it_behaves_like 'reschedules itself'
-
- it 'does not call the uninstallation service' do
- expect(Clusters::Applications::UninstallWorker).not_to receive(:new)
-
- subject
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/destroy_service_spec.rb b/spec/services/clusters/destroy_service_spec.rb
index 76d9cc34b5d..dc600c9e830 100644
--- a/spec/services/clusters/destroy_service_spec.rb
+++ b/spec/services/clusters/destroy_service_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe Clusters::DestroyService do
let(:params) { { cleanup: 'true' } }
before do
- allow(Clusters::Cleanup::AppWorker).to receive(:perform_async)
+ allow(Clusters::Cleanup::ProjectNamespaceWorker).to receive(:perform_async)
end
it 'does not destroy cluster' do
@@ -45,10 +45,10 @@ RSpec.describe Clusters::DestroyService do
expect(Clusters::Cluster.where(id: cluster.id).exists?).not_to be_falsey
end
- it 'transition cluster#cleanup_status from cleanup_not_started to cleanup_uninstalling_applications' do
+ it 'transition cluster#cleanup_status from cleanup_not_started to cleanup_removing_project_namespaces' do
expect { subject }.to change { cluster.cleanup_status_name }
.from(:cleanup_not_started)
- .to(:cleanup_uninstalling_applications)
+ .to(:cleanup_removing_project_namespaces)
end
end
end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index d8c95a70bd0..9c553d0eec2 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -11,8 +11,6 @@ RSpec.describe Clusters::Gcp::FinalizeCreationService, '#execute' do
let(:platform) { cluster.platform }
let(:endpoint) { '111.111.111.111' }
let(:api_url) { 'https://' + endpoint }
- let(:username) { 'sample-username' }
- let(:password) { 'sample-password' }
let(:secret_name) { 'gitlab-token' }
let(:token) { 'sample-token' }
let(:namespace) { "#{cluster.project.path}-#{cluster.project.id}" }
@@ -34,8 +32,6 @@ RSpec.describe Clusters::Gcp::FinalizeCreationService, '#execute' do
expect(provider.endpoint).to eq(endpoint)
expect(platform.api_url).to eq(api_url)
expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert).strip)
- expect(platform.username).to eq(username)
- expect(platform.password).to eq(password)
expect(platform.token).to eq(token)
end
end
@@ -83,7 +79,7 @@ RSpec.describe Clusters::Gcp::FinalizeCreationService, '#execute' do
shared_context 'kubernetes information successfully fetched' do
before do
stub_cloud_platform_get_zone_cluster(
- provider.gcp_project_id, provider.zone, cluster.name, { endpoint: endpoint, username: username, password: password }
+ provider.gcp_project_id, provider.zone, cluster.name, { endpoint: endpoint }
)
stub_kubeclient_discover(api_url)
diff --git a/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
deleted file mode 100644
index 1f6ad218927..00000000000
--- a/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::ParseClusterApplicationsArtifactService do
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
-
- before do
- project.add_maintainer(user)
- end
-
- describe 'RELEASE_NAMES' do
- it 'is included in Cluster application names', :aggregate_failures do
- described_class::RELEASE_NAMES.each do |release_name|
- expect(Clusters::Cluster::APPLICATIONS).to include(release_name)
- end
- end
- end
-
- describe '.new' do
- let(:job) { build(:ci_build) }
-
- it 'sets the project and current user', :aggregate_failures do
- service = described_class.new(job, user)
-
- expect(service.project).to eq(job.project)
- expect(service.current_user).to eq(user)
- end
- end
-
- describe '#execute' do
- let_it_be(:cluster, reload: true) { create(:cluster, projects: [project]) }
- let_it_be(:deployment, reload: true) { create(:deployment, cluster: cluster) }
-
- let(:job) { deployment.deployable }
- let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job) }
-
- it 'calls Gitlab::Kubernetes::Helm::Parsers::ListV2' do
- expect(Gitlab::Kubernetes::Helm::Parsers::ListV2).to receive(:new).and_call_original
-
- result = described_class.new(job, user).execute(artifact)
-
- expect(result[:status]).to eq(:success)
- end
-
- context 'artifact is not of cluster_applications type' do
- let(:artifact) { create(:ci_job_artifact, :archive) }
- let(:job) { artifact.job }
-
- it 'raise ArgumentError' do
- expect do
- described_class.new(job, user).execute(artifact)
- end.to raise_error(ArgumentError, 'Artifact is not cluster_applications file type')
- end
- end
-
- context 'artifact exceeds acceptable size' do
- it 'returns an error' do
- stub_const("#{described_class}::MAX_ACCEPTABLE_ARTIFACT_SIZE", 1.byte)
-
- result = described_class.new(job, user).execute(artifact)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('Cluster_applications artifact too big. Maximum allowable size: 1 Byte')
- end
- end
-
- context 'job has no deployment' do
- let(:job) { build(:ci_build) }
-
- it 'returns an error' do
- result = described_class.new(job, user).execute(artifact)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('No deployment found for this job')
- end
- end
-
- context 'job has no deployment cluster' do
- let(:deployment) { create(:deployment) }
- let(:job) { deployment.deployable }
-
- it 'returns an error' do
- result = described_class.new(job, user).execute(artifact)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('No deployment cluster found for this job')
- end
- end
-
- context 'blob is empty' do
- let(:file) { fixture_file_upload(Rails.root.join("spec/fixtures/helm/helm_list_v2_empty_blob.json.gz")) }
- let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
-
- it 'returns success' do
- result = described_class.new(job, user).execute(artifact)
-
- expect(result[:status]).to eq(:success)
- end
- end
-
- context 'job has deployment cluster' do
- context 'current user does not have access to deployment cluster' do
- let(:other_user) { create(:user) }
-
- it 'returns an error' do
- result = described_class.new(job, other_user).execute(artifact)
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq('No deployment cluster found for this job')
- end
- end
-
- it 'does not affect unpermitted cluster applications' do
- expect(Clusters::ParseClusterApplicationsArtifactService::RELEASE_NAMES).to contain_exactly('cilium')
- end
-
- Clusters::ParseClusterApplicationsArtifactService::RELEASE_NAMES.each do |release_name|
- context release_name do
- include_examples 'parse cluster applications artifact', release_name
- end
- end
- end
- end
-end
diff --git a/spec/services/commits/cherry_pick_service_spec.rb b/spec/services/commits/cherry_pick_service_spec.rb
index 8fad5164b77..2565e17ac90 100644
--- a/spec/services/commits/cherry_pick_service_spec.rb
+++ b/spec/services/commits/cherry_pick_service_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Commits::CherryPickService do
repository.add_branch(user, branch_name, merge_base_sha)
end
- def cherry_pick(sha, branch_name)
+ def cherry_pick(sha, branch_name, message: nil)
commit = project.commit(sha)
described_class.new(
@@ -32,7 +32,8 @@ RSpec.describe Commits::CherryPickService do
user,
commit: commit,
start_branch: branch_name,
- branch_name: branch_name
+ branch_name: branch_name,
+ message: message
).execute
end
@@ -45,6 +46,14 @@ RSpec.describe Commits::CherryPickService do
head = repository.find_branch(branch_name).target
expect(head).not_to eq(merge_base_sha)
end
+
+ it 'supports a custom commit message' do
+ result = cherry_pick(merge_commit_sha, branch_name, message: 'foo')
+ branch = repository.find_branch(branch_name)
+
+ expect(result[:status]).to eq(:success)
+ expect(branch.dereferenced_target.message).to eq('foo')
+ end
end
it_behaves_like 'successful cherry-pick'
diff --git a/spec/services/container_expiration_policies/cleanup_service_spec.rb b/spec/services/container_expiration_policies/cleanup_service_spec.rb
index c6faae7449d..5f284b9dd8b 100644
--- a/spec/services/container_expiration_policies/cleanup_service_spec.rb
+++ b/spec/services/container_expiration_policies/cleanup_service_spec.rb
@@ -9,37 +9,68 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
let(:service) { described_class.new(repository) }
describe '#execute' do
+ let(:policy) { repository.project.container_expiration_policy }
+
subject { service.execute }
- shared_examples 'cleaning up a container repository' do
- context 'with a successful cleanup tags service execution' do
- let(:cleanup_tags_service_params) { project.container_expiration_policy.policy_params.merge('container_expiration_policy' => true) }
- let(:cleanup_tags_service) { instance_double(Projects::ContainerRepository::CleanupTagsService) }
+ before do
+ policy.update!(enabled: true)
+ policy.update_column(:next_run_at, 5.minutes.ago)
+ end
- it 'completely clean up the repository' do
- expect(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:new).with(project, nil, cleanup_tags_service_params).and_return(cleanup_tags_service)
- expect(cleanup_tags_service).to receive(:execute).with(repository).and_return(status: :success)
+ context 'with a successful cleanup tags service execution' do
+ let(:cleanup_tags_service_params) { project.container_expiration_policy.policy_params.merge('container_expiration_policy' => true) }
+ let(:cleanup_tags_service) { instance_double(Projects::ContainerRepository::CleanupTagsService) }
- response = subject
+ it 'completely clean up the repository' do
+ expect(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:new).with(project, nil, cleanup_tags_service_params).and_return(cleanup_tags_service)
+ expect(cleanup_tags_service).to receive(:execute).with(repository).and_return(status: :success)
- aggregate_failures "checking the response and container repositories" do
- expect(response.success?).to eq(true)
- expect(response.payload).to include(cleanup_status: :finished, container_repository_id: repository.id)
- expect(ContainerRepository.waiting_for_cleanup.count).to eq(0)
- expect(repository.reload.cleanup_unscheduled?).to be_truthy
- expect(repository.expiration_policy_completed_at).not_to eq(nil)
- expect(repository.expiration_policy_started_at).not_to eq(nil)
- end
+ response = subject
+
+ aggregate_failures "checking the response and container repositories" do
+ expect(response.success?).to eq(true)
+ expect(response.payload).to include(cleanup_status: :finished, container_repository_id: repository.id)
+ expect(ContainerRepository.waiting_for_cleanup.count).to eq(0)
+ expect(repository.reload.cleanup_unscheduled?).to be_truthy
+ expect(repository.expiration_policy_completed_at).not_to eq(nil)
+ expect(repository.expiration_policy_started_at).not_to eq(nil)
end
end
+ end
- context 'without a successful cleanup tags service execution' do
- let(:cleanup_tags_service_response) { { status: :error, message: 'timeout' } }
+ context 'without a successful cleanup tags service execution' do
+ let(:cleanup_tags_service_response) { { status: :error, message: 'timeout' } }
- before do
- expect(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:new).and_return(double(execute: cleanup_tags_service_response))
+ before do
+ expect(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:new).and_return(double(execute: cleanup_tags_service_response))
+ end
+
+ it 'partially clean up the repository' do
+ response = subject
+
+ aggregate_failures "checking the response and container repositories" do
+ expect(response.success?).to eq(true)
+ expect(response.payload).to include(cleanup_status: :unfinished, container_repository_id: repository.id)
+ expect(ContainerRepository.waiting_for_cleanup.count).to eq(1)
+ expect(repository.reload.cleanup_unfinished?).to be_truthy
+ expect(repository.expiration_policy_started_at).not_to eq(nil)
+ expect(repository.expiration_policy_completed_at).to eq(nil)
+ end
+ end
+
+ context 'with a truncated cleanup tags service response' do
+ let(:cleanup_tags_service_response) do
+ {
+ status: :error,
+ original_size: 1000,
+ before_truncate_size: 800,
+ after_truncate_size: 200,
+ before_delete_size: 100,
+ deleted_size: 100
+ }
end
it 'partially clean up the repository' do
@@ -47,179 +78,134 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
aggregate_failures "checking the response and container repositories" do
expect(response.success?).to eq(true)
- expect(response.payload).to include(cleanup_status: :unfinished, container_repository_id: repository.id)
+ expect(response.payload)
+ .to include(
+ cleanup_status: :unfinished,
+ container_repository_id: repository.id,
+ cleanup_tags_service_original_size: 1000,
+ cleanup_tags_service_before_truncate_size: 800,
+ cleanup_tags_service_after_truncate_size: 200,
+ cleanup_tags_service_before_delete_size: 100,
+ cleanup_tags_service_deleted_size: 100
+ )
expect(ContainerRepository.waiting_for_cleanup.count).to eq(1)
expect(repository.reload.cleanup_unfinished?).to be_truthy
expect(repository.expiration_policy_started_at).not_to eq(nil)
expect(repository.expiration_policy_completed_at).to eq(nil)
end
end
-
- context 'with a truncated cleanup tags service response' do
- let(:cleanup_tags_service_response) do
- {
- status: :error,
- original_size: 1000,
- before_truncate_size: 800,
- after_truncate_size: 200,
- before_delete_size: 100,
- deleted_size: 100
- }
- end
-
- it 'partially clean up the repository' do
- response = subject
-
- aggregate_failures "checking the response and container repositories" do
- expect(response.success?).to eq(true)
- expect(response.payload)
- .to include(
- cleanup_status: :unfinished,
- container_repository_id: repository.id,
- cleanup_tags_service_original_size: 1000,
- cleanup_tags_service_before_truncate_size: 800,
- cleanup_tags_service_after_truncate_size: 200,
- cleanup_tags_service_before_delete_size: 100,
- cleanup_tags_service_deleted_size: 100
- )
- expect(ContainerRepository.waiting_for_cleanup.count).to eq(1)
- expect(repository.reload.cleanup_unfinished?).to be_truthy
- expect(repository.expiration_policy_started_at).not_to eq(nil)
- expect(repository.expiration_policy_completed_at).to eq(nil)
- end
- end
- end
end
+ end
- context 'with no repository' do
- let(:service) { described_class.new(nil) }
+ context 'with no repository' do
+ let(:service) { described_class.new(nil) }
- it 'returns an error response' do
- expect(subject.success?).to eq(false)
- expect(subject.message).to eq('no repository')
- end
+ it 'returns an error response' do
+ expect(subject.success?).to eq(false)
+ expect(subject.message).to eq('no repository')
end
+ end
- context 'with an invalid policy' do
- let(:policy) { repository.project.container_expiration_policy }
+ context 'with an invalid policy' do
+ let(:policy) { repository.project.container_expiration_policy }
- before do
- policy.name_regex = nil
- policy.enabled = true
- repository.expiration_policy_cleanup_status = :cleanup_ongoing
- end
+ before do
+ policy.name_regex = nil
+ policy.enabled = true
+ repository.expiration_policy_cleanup_status = :cleanup_ongoing
+ end
- it 'returns an error response' do
- expect { subject }.to change { repository.expiration_policy_cleanup_status }.from('cleanup_ongoing').to('cleanup_unscheduled')
- expect(subject.success?).to eq(false)
- expect(subject.message).to eq('invalid policy')
- expect(policy).not_to be_enabled
- end
+ it 'returns an error response' do
+ expect { subject }.to change { repository.expiration_policy_cleanup_status }.from('cleanup_ongoing').to('cleanup_unscheduled')
+ expect(subject.success?).to eq(false)
+ expect(subject.message).to eq('invalid policy')
+ expect(policy).not_to be_enabled
end
+ end
- context 'with a network error' do
- before do
- expect(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:new).and_raise(Faraday::TimeoutError)
- end
+ context 'with a network error' do
+ before do
+ expect(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:new).and_raise(Faraday::TimeoutError)
+ end
- it 'raises an error' do
- expect { subject }.to raise_error(Faraday::TimeoutError)
+ it 'raises an error' do
+ expect { subject }.to raise_error(Faraday::TimeoutError)
- expect(ContainerRepository.waiting_for_cleanup.count).to eq(1)
- expect(repository.reload.cleanup_unfinished?).to be_truthy
- expect(repository.expiration_policy_started_at).not_to eq(nil)
- expect(repository.expiration_policy_completed_at).to eq(nil)
- end
+ expect(ContainerRepository.waiting_for_cleanup.count).to eq(1)
+ expect(repository.reload.cleanup_unfinished?).to be_truthy
+ expect(repository.expiration_policy_started_at).not_to eq(nil)
+ expect(repository.expiration_policy_completed_at).to eq(nil)
end
end
- context 'with loopless enabled' do
- let(:policy) { repository.project.container_expiration_policy }
+ context 'next run scheduling' do
+ let_it_be_with_reload(:repository2) { create(:container_repository, project: project) }
+ let_it_be_with_reload(:repository3) { create(:container_repository, project: project) }
before do
- policy.update!(enabled: true)
- policy.update_column(:next_run_at, 5.minutes.ago)
+ cleanup_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService)
+ allow(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:new).and_return(cleanup_tags_service)
+ allow(cleanup_tags_service).to receive(:execute).and_return(status: :success)
end
- it_behaves_like 'cleaning up a container repository'
-
- context 'next run scheduling' do
- let_it_be_with_reload(:repository2) { create(:container_repository, project: project) }
- let_it_be_with_reload(:repository3) { create(:container_repository, project: project) }
+ shared_examples 'not scheduling the next run' do
+ it 'does not scheduled the next run' do
+ expect(policy).not_to receive(:schedule_next_run!)
- before do
- cleanup_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService)
- allow(Projects::ContainerRepository::CleanupTagsService)
- .to receive(:new).and_return(cleanup_tags_service)
- allow(cleanup_tags_service).to receive(:execute).and_return(status: :success)
- end
-
- shared_examples 'not scheduling the next run' do
- it 'does not scheduled the next run' do
- expect(policy).not_to receive(:schedule_next_run!)
-
- expect { subject }.not_to change { policy.reload.next_run_at }
- end
+ expect { subject }.not_to change { policy.reload.next_run_at }
end
+ end
- shared_examples 'scheduling the next run' do
- it 'schedules the next run' do
- expect(policy).to receive(:schedule_next_run!).and_call_original
+ shared_examples 'scheduling the next run' do
+ it 'schedules the next run' do
+ expect(policy).to receive(:schedule_next_run!).and_call_original
- expect { subject }.to change { policy.reload.next_run_at }
- end
+ expect { subject }.to change { policy.reload.next_run_at }
end
+ end
- context 'with cleanups started_at before policy next_run_at' do
- before do
- ContainerRepository.update_all(expiration_policy_started_at: 10.minutes.ago)
- end
-
- it_behaves_like 'not scheduling the next run'
+ context 'with cleanups started_at before policy next_run_at' do
+ before do
+ ContainerRepository.update_all(expiration_policy_started_at: 10.minutes.ago)
end
- context 'with cleanups started_at around policy next_run_at' do
- before do
- repository3.update!(expiration_policy_started_at: policy.next_run_at + 10.minutes.ago)
- end
+ it_behaves_like 'not scheduling the next run'
+ end
- it_behaves_like 'not scheduling the next run'
+ context 'with cleanups started_at around policy next_run_at' do
+ before do
+ repository3.update!(expiration_policy_started_at: policy.next_run_at + 10.minutes.ago)
end
- context 'with only the current repository started_at before the policy next_run_at' do
- before do
- repository2.update!(expiration_policy_started_at: policy.next_run_at + 10.minutes)
- repository3.update!(expiration_policy_started_at: policy.next_run_at + 12.minutes)
- end
+ it_behaves_like 'not scheduling the next run'
+ end
- it_behaves_like 'scheduling the next run'
+ context 'with only the current repository started_at before the policy next_run_at' do
+ before do
+ repository2.update!(expiration_policy_started_at: policy.next_run_at + 10.minutes)
+ repository3.update!(expiration_policy_started_at: policy.next_run_at + 12.minutes)
end
- context 'with cleanups started_at after policy next_run_at' do
- before do
- ContainerRepository.update_all(expiration_policy_started_at: policy.next_run_at + 10.minutes)
- end
+ it_behaves_like 'scheduling the next run'
+ end
- it_behaves_like 'scheduling the next run'
+ context 'with cleanups started_at after policy next_run_at' do
+ before do
+ ContainerRepository.update_all(expiration_policy_started_at: policy.next_run_at + 10.minutes)
end
- context 'with a future policy next_run_at' do
- before do
- policy.update_column(:next_run_at, 5.minutes.from_now)
- end
+ it_behaves_like 'scheduling the next run'
+ end
- it_behaves_like 'not scheduling the next run'
+ context 'with a future policy next_run_at' do
+ before do
+ policy.update_column(:next_run_at, 5.minutes.from_now)
end
- end
- end
- context 'with loopless disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_loopless: false)
+ it_behaves_like 'not scheduling the next run'
end
-
- it_behaves_like 'cleaning up a container repository'
end
end
end
diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb
index 4d15258a186..6996563fdb8 100644
--- a/spec/services/deployments/update_environment_service_spec.rb
+++ b/spec/services/deployments/update_environment_service_spec.rb
@@ -95,6 +95,42 @@ RSpec.describe Deployments::UpdateEnvironmentService do
end
end
+ context 'when external URL is specified and the tier is unset' do
+ let(:options) { { name: 'production', url: external_url } }
+
+ before do
+ environment.update_columns(external_url: external_url, tier: nil)
+ job.update!(environment: 'production')
+ end
+
+ context 'when external URL is valid' do
+ let(:external_url) { 'https://google.com' }
+
+ it 'succeeds to update the tier automatically' do
+ expect { subject.execute }.to change { environment.tier }.from(nil).to('production')
+ end
+ end
+
+ context 'when external URL is invalid' do
+ let(:external_url) { 'google.com' }
+
+ it 'fails to update the tier due to validation error' do
+ expect { subject.execute }.not_to change { environment.tier }
+ end
+
+ it 'tracks an exception' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(an_instance_of(described_class::EnvironmentUpdateFailure),
+ project_id: project.id,
+ environment_id: environment.id,
+ reason: %q{External url is blocked: Only allowed schemes are http, https})
+ .once
+
+ subject.execute
+ end
+ end
+ end
+
context 'when variables are used' do
let(:options) do
{ name: 'review-apps/$CI_COMMIT_REF_NAME',
diff --git a/spec/services/design_management/copy_design_collection/copy_service_spec.rb b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
index 03242487b53..186d2481c19 100644
--- a/spec/services/design_management/copy_design_collection/copy_service_spec.rb
+++ b/spec/services/design_management/copy_design_collection/copy_service_spec.rb
@@ -195,6 +195,14 @@ RSpec.describe DesignManagement::CopyDesignCollection::CopyService, :clean_gitla
expect { subject }.to change { target_repository.branch_names }.from([]).to(['master'])
end
+ it 'does not create default branch when one exists' do
+ target_repository.create_if_not_exists
+ target_repository.create_file(user, '.meta', '.gitlab', branch_name: 'new-branch', message: 'message')
+
+ expect { subject }.not_to change { target_repository.branch_names }
+ expect(target_repository.branch_names).to eq(['new-branch'])
+ end
+
it 'leaves the design collection in the correct copy state' do
subject
diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb
index 2e30455eb0a..24de1d90526 100644
--- a/spec/services/discussions/resolve_service_spec.rb
+++ b/spec/services/discussions/resolve_service_spec.rb
@@ -121,5 +121,50 @@ RSpec.describe Discussions::ResolveService do
service.execute
end
end
+
+ context 'when resolving a discussion' do
+ def resolve_discussion(discussion, user)
+ described_class.new(project, user, one_or_more_discussions: discussion).execute
+ end
+
+ context 'in a design' do
+ let_it_be(:design) { create(:design, :with_file, issue: create(:issue, project: project)) }
+ let_it_be(:user_1) { create(:user) }
+ let_it_be(:user_2) { create(:user) }
+ let_it_be(:discussion_1) { create(:diff_note_on_design, noteable: design, project: project, author: user_1).to_discussion }
+ let_it_be(:discussion_2) { create(:diff_note_on_design, noteable: design, project: project, author: user_2).to_discussion }
+
+ before do
+ project.add_developer(user_1)
+ project.add_developer(user_2)
+ end
+
+ context 'when user resolving discussion has open todos' do
+ let!(:user_1_todo_for_discussion_1) { create(:todo, :pending, user: user_1, target: design, note: discussion_1.notes.first, project: project) }
+ let!(:user_1_todo_2_for_discussion_1) { create(:todo, :pending, user: user_1, target: design, note: discussion_1.notes.first, project: project) }
+ let!(:user_1_todo_for_discussion_2) { create(:todo, :pending, user: user_1, target: design, note: discussion_2.notes.first, project: project) }
+ let!(:user_2_todo_for_discussion_1) { create(:todo, :pending, user: user_2, target: design, note: discussion_1.notes.first, project: project) }
+
+ it 'marks user todos for given discussion as done' do
+ resolve_discussion(discussion_1, user_1)
+
+ expect(user_1_todo_for_discussion_1.reload).to be_done
+ expect(user_1_todo_2_for_discussion_1.reload).to be_done
+ expect(user_1_todo_for_discussion_2.reload).to be_pending
+ expect(user_2_todo_for_discussion_1.reload).to be_pending
+ end
+ end
+ end
+
+ context 'in a merge request' do
+ let!(:user_todo_for_discussion) { create(:todo, :pending, user: user, target: merge_request, note: discussion.notes.first, project: project) }
+
+ it 'does not mark user todo as done' do
+ resolve_discussion(discussion, user)
+
+ expect(user_todo_for_discussion).to be_pending
+ end
+ end
+ end
end
end
diff --git a/spec/services/feature_flags/disable_service_spec.rb b/spec/services/feature_flags/disable_service_spec.rb
deleted file mode 100644
index 4b2137be35c..00000000000
--- a/spec/services/feature_flags/disable_service_spec.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe FeatureFlags::DisableService do
- include FeatureFlagHelpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
-
- let(:params) { {} }
- let(:service) { described_class.new(project, user, params) }
-
- before_all do
- project.add_developer(user)
- end
-
- describe '#execute' do
- subject { service.execute }
-
- context 'with params to disable default strategy on prd scope' do
- let(:params) do
- {
- name: 'awesome',
- environment_scope: 'prd',
- strategy: { name: 'userWithId', parameters: { 'userIds': 'User:1' } }.deep_stringify_keys
- }
- end
-
- context 'when there is a persisted feature flag' do
- let!(:feature_flag) { create_flag(project, params[:name]) }
-
- context 'when there is a persisted scope' do
- let!(:scope) do
- create_scope(feature_flag, params[:environment_scope], true, strategies)
- end
-
- context 'when there is a persisted strategy' do
- let(:strategies) do
- [
- { name: 'userWithId', parameters: { 'userIds': 'User:1' } }.deep_stringify_keys,
- { name: 'userWithId', parameters: { 'userIds': 'User:2' } }.deep_stringify_keys
- ]
- end
-
- it 'deletes the specified strategy' do
- subject
-
- scope.reload
- expect(scope.strategies.count).to eq(1)
- expect(scope.strategies).not_to include(params[:strategy])
- end
-
- context 'when strategies will be empty' do
- let(:strategies) { [params[:strategy]] }
-
- it 'deletes the persisted scope' do
- subject
-
- expect(feature_flag.scopes.exists?(environment_scope: params[:environment_scope]))
- .to eq(false)
- end
- end
- end
-
- context 'when there is no persisted strategy' do
- let(:strategies) { [{ name: 'default', parameters: {} }] }
-
- it 'returns error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to include('Strategy not found')
- end
- end
- end
-
- context 'when there is no persisted scope' do
- it 'returns error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to include('Feature Flag Scope not found')
- end
- end
- end
-
- context 'when there is no persisted feature flag' do
- it 'returns error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to include('Feature Flag not found')
- end
- end
- end
- end
-end
diff --git a/spec/services/feature_flags/enable_service_spec.rb b/spec/services/feature_flags/enable_service_spec.rb
deleted file mode 100644
index c0008b1933f..00000000000
--- a/spec/services/feature_flags/enable_service_spec.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe FeatureFlags::EnableService do
- include FeatureFlagHelpers
-
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
-
- let(:params) { {} }
- let(:service) { described_class.new(project, user, params) }
-
- before_all do
- project.add_developer(user)
- end
-
- describe '#execute' do
- subject { service.execute }
-
- context 'with params to enable default strategy on prd scope' do
- let(:params) do
- {
- name: 'awesome',
- environment_scope: 'prd',
- strategy: { name: 'default', parameters: {} }.stringify_keys
- }
- end
-
- context 'when there is no persisted feature flag' do
- it 'creates a new feature flag with scope' do
- feature_flag = subject[:feature_flag]
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- expect(subject[:status]).to eq(:success)
- expect(feature_flag.name).to eq(params[:name])
- expect(feature_flag.default_scope).not_to be_active
- expect(scope).to be_active
- expect(scope.strategies).to include(params[:strategy])
- end
-
- context 'when params include default scope' do
- let(:params) do
- {
- name: 'awesome',
- environment_scope: '*',
- strategy: { name: 'userWithId', parameters: { 'userIds': 'abc' } }.deep_stringify_keys
- }
- end
-
- it 'create a new feature flag with an active default scope with the specified strategy' do
- feature_flag = subject[:feature_flag]
- expect(subject[:status]).to eq(:success)
- expect(feature_flag.default_scope).to be_active
- expect(feature_flag.default_scope.strategies).to include(params[:strategy])
- end
- end
- end
-
- context 'when there is a persisted feature flag' do
- let!(:feature_flag) { create_flag(project, params[:name]) }
-
- context 'when there is no persisted scope' do
- it 'creates a new scope for the persisted feature flag' do
- feature_flag = subject[:feature_flag]
- scope = feature_flag.scopes.find_by_environment_scope(params[:environment_scope])
- expect(subject[:status]).to eq(:success)
- expect(feature_flag.name).to eq(params[:name])
- expect(scope).to be_active
- expect(scope.strategies).to include(params[:strategy])
- end
- end
-
- context 'when there is a persisted scope' do
- let!(:feature_flag_scope) do
- create_scope(feature_flag, params[:environment_scope], active, strategies)
- end
-
- let(:active) { true }
-
- context 'when the persisted scope does not have the specified strategy yet' do
- let(:strategies) { [{ name: 'userWithId', parameters: { 'userIds': 'abc' } }] }
-
- it 'adds the specified strategy to the scope' do
- subject
-
- feature_flag_scope.reload
- expect(feature_flag_scope.strategies).to include(params[:strategy])
- end
-
- context 'when the persisted scope is inactive' do
- let(:active) { false }
-
- it 'reactivates the scope' do
- expect { subject }
- .to change { feature_flag_scope.reload.active }.from(false).to(true)
- end
- end
- end
-
- context 'when the persisted scope has the specified strategy already' do
- let(:strategies) { [params[:strategy]] }
-
- it 'does not add a duplicated strategy to the scope' do
- expect { subject }
- .not_to change { feature_flag_scope.reload.strategies.count }
- end
- end
- end
- end
- end
-
- context 'when strategy is not specified in params' do
- let(:params) do
- {
- name: 'awesome',
- environment_scope: 'prd'
- }
- end
-
- it 'returns error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to include('Scopes strategies must be an array of strategy hashes')
- end
- end
-
- context 'when environment scope is not specified in params' do
- let(:params) do
- {
- name: 'awesome',
- strategy: { name: 'default', parameters: {} }.stringify_keys
- }
- end
-
- it 'returns error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to include("Scopes environment scope can't be blank")
- end
- end
-
- context 'when name is not specified in params' do
- let(:params) do
- {
- environment_scope: 'prd',
- strategy: { name: 'default', parameters: {} }.stringify_keys
- }
- end
-
- it 'returns error' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to include("Name can't be blank")
- end
- end
- end
-end
diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb
index 1a127a0d472..d838549891a 100644
--- a/spec/services/feature_flags/update_service_spec.rb
+++ b/spec/services/feature_flags/update_service_spec.rb
@@ -121,150 +121,5 @@ RSpec.describe FeatureFlags::UpdateService do
subject
end
end
-
- context 'when scope active state is changed' do
- let(:params) do
- {
- scopes_attributes: [{ id: feature_flag.scopes.first.id, active: false }]
- }
- end
-
- it 'creates audit event about changing active state' do
- expect { subject }.to change { AuditEvent.count }.by(1)
- expect(audit_event_message).to(
- include("Updated rule <strong>*</strong> active state "\
- "from <strong>true</strong> to <strong>false</strong>.")
- )
- end
- end
-
- context 'when scope is renamed' do
- let(:changed_scope) { feature_flag.scopes.create!(environment_scope: 'review', active: true) }
- let(:params) do
- {
- scopes_attributes: [{ id: changed_scope.id, environment_scope: 'staging' }]
- }
- end
-
- it 'creates audit event with changed name' do
- expect { subject }.to change { AuditEvent.count }.by(1)
- expect(audit_event_message).to(
- include("Updated rule <strong>staging</strong> environment scope "\
- "from <strong>review</strong> to <strong>staging</strong>.")
- )
- end
-
- context 'when scope can not be updated' do
- let(:params) do
- {
- scopes_attributes: [{ id: changed_scope.id, environment_scope: '' }]
- }
- end
-
- it 'returns error status' do
- expect(subject[:status]).to eq(:error)
- end
-
- it 'returns error messages' do
- expect(subject[:message]).to include("Scopes environment scope can't be blank")
- end
-
- it 'does not create audit event' do
- expect { subject }.not_to change { AuditEvent.count }
- end
- end
- end
-
- context 'when scope is deleted' do
- let(:deleted_scope) { feature_flag.scopes.create!(environment_scope: 'review', active: true) }
- let(:params) do
- {
- scopes_attributes: [{ id: deleted_scope.id, '_destroy': true }]
- }
- end
-
- it 'creates audit event with deleted scope' do
- expect { subject }.to change { AuditEvent.count }.by(1)
- expect(audit_event_message).to include("Deleted rule <strong>review</strong>.")
- end
-
- context 'when scope can not be deleted' do
- before do
- allow(deleted_scope).to receive(:destroy).and_return(false)
- end
-
- it 'does not create audit event' do
- expect do
- subject
- end.to not_change { AuditEvent.count }.and raise_error(ActiveRecord::RecordNotDestroyed)
- end
- end
- end
-
- context 'when new scope is being added' do
- let(:new_environment_scope) { 'review' }
- let(:params) do
- {
- scopes_attributes: [{ environment_scope: new_environment_scope, active: true }]
- }
- end
-
- it 'creates audit event with new scope' do
- expected = 'Created rule <strong>review</strong> and set it as <strong>active</strong> '\
- 'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>.'
-
- subject
-
- expect(audit_event_message).to include(expected)
- end
-
- context 'when scope can not be created' do
- let(:new_environment_scope) { '' }
-
- it 'returns error status' do
- expect(subject[:status]).to eq(:error)
- end
-
- it 'returns error messages' do
- expect(subject[:message]).to include("Scopes environment scope can't be blank")
- end
-
- it 'does not create audit event' do
- expect { subject }.not_to change { AuditEvent.count }
- end
- end
- end
-
- context 'when the strategy is changed' do
- let(:scope) do
- create(:operations_feature_flag_scope,
- feature_flag: feature_flag,
- environment_scope: 'sandbox',
- strategies: [{ name: "default", parameters: {} }])
- end
-
- let(:params) do
- {
- scopes_attributes: [{
- id: scope.id,
- environment_scope: 'sandbox',
- strategies: [{
- name: 'gradualRolloutUserId',
- parameters: {
- groupId: 'mygroup',
- percentage: "40"
- }
- }]
- }]
- }
- end
-
- it 'creates an audit event' do
- expected = %r{Updated rule <strong>sandbox</strong> strategies from <strong>.*</strong> to <strong>.*</strong>.}
-
- expect { subject }.to change { AuditEvent.count }.by(1)
- expect(audit_event_message).to match(expected)
- end
- end
end
end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index dca5497de06..b59ee894fe8 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -140,7 +140,7 @@ RSpec.describe Groups::CreateService, '#execute' do
end
it 'create the chat team with the group' do
- allow_any_instance_of(Mattermost::Team).to receive(:create)
+ allow_any_instance_of(::Mattermost::Team).to receive(:create)
.and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' })
expect { subject }.to change { ChatTeam.count }.from(0).to(1)
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index a5fce315d91..c794acdf76d 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Groups::DestroyService do
let!(:chat_team) { create(:chat_team, namespace: group) }
it 'destroys the team too' do
- expect_next_instance_of(Mattermost::Team) do |instance|
+ expect_next_instance_of(::Mattermost::Team) do |instance|
expect(instance).to receive(:destroy)
end
diff --git a/spec/services/groups/group_links/create_service_spec.rb b/spec/services/groups/group_links/create_service_spec.rb
index df994b9f2a3..b1bb9a8de23 100644
--- a/spec/services/groups/group_links/create_service_spec.rb
+++ b/spec/services/groups/group_links/create_service_spec.rb
@@ -11,8 +11,8 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
let_it_be(:group) { create(:group, :private, parent: group_parent) }
let_it_be(:group_child) { create(:group, :private, parent: group) }
- let_it_be(:shared_group_parent) { create(:group, :private) }
- let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ let_it_be(:shared_group_parent, refind: true) { create(:group, :private) }
+ let_it_be(:shared_group, refind: true) { create(:group, :private, parent: shared_group_parent) }
let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
let_it_be(:project_parent) { create(:project, group: shared_group_parent) }
@@ -28,7 +28,7 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
let(:user) { group_user }
- subject { described_class.new(group, user, opts) }
+ subject { described_class.new(shared_group, group, user, opts) }
before do
group.add_guest(group_user)
@@ -36,11 +36,11 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
end
it 'adds group to another group' do
- expect { subject.execute(shared_group) }.to change { group.shared_group_links.count }.from(0).to(1)
+ expect { subject.execute }.to change { group.shared_group_links.count }.from(0).to(1)
end
it 'returns false if shared group is blank' do
- expect { subject.execute(nil) }.not_to change { group.shared_group_links.count }
+ expect { described_class.new(nil, group, user, opts) }.not_to change { group.shared_group_links.count }
end
context 'user does not have access to group' do
@@ -51,7 +51,7 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
end
it 'returns error' do
- result = subject.execute(shared_group)
+ result = subject.execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(404)
@@ -67,7 +67,7 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
end
it 'returns error' do
- result = subject.execute(shared_group)
+ result = subject.execute
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(404)
@@ -85,7 +85,7 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
it 'is executed only for the direct members of the group' do
expect(UserProjectAccessChangedService).to receive(:new).with(contain_exactly(group_user.id)).and_call_original
- subject.execute(shared_group)
+ subject.execute
end
end
@@ -94,7 +94,7 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
let(:user) { group_user }
it 'create proper authorizations' do
- subject.execute(shared_group)
+ subject.execute
expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
expect(Ability.allowed?(user, :read_project, project)).to be_truthy
@@ -106,7 +106,7 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
let(:user) { parent_group_user }
it 'create proper authorizations' do
- subject.execute(shared_group)
+ subject.execute
expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
expect(Ability.allowed?(user, :read_project, project)).to be_falsey
@@ -118,7 +118,7 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
let(:user) { child_group_user }
it 'create proper authorizations' do
- subject.execute(shared_group)
+ subject.execute
expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
expect(Ability.allowed?(user, :read_project, project)).to be_falsey
@@ -127,4 +127,28 @@ RSpec.describe Groups::GroupLinks::CreateService, '#execute' do
end
end
end
+
+ context 'sharing outside the hierarchy is disabled' do
+ before do
+ shared_group_parent.namespace_settings.update!(prevent_sharing_groups_outside_hierarchy: true)
+ end
+
+ it 'prevents sharing with a group outside the hierarchy' do
+ result = subject.execute
+
+ expect(group.reload.shared_group_links.count).to eq(0)
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(404)
+ end
+
+ it 'allows sharing with a group within the hierarchy' do
+ sibling_group = create(:group, :private, parent: shared_group_parent)
+ sibling_group.add_guest(group_user)
+
+ result = described_class.new(shared_group, sibling_group, user, opts).execute
+
+ expect(sibling_group.reload.shared_group_links.count).to eq(1)
+ expect(result[:status]).to eq(:success)
+ end
+ end
end
diff --git a/spec/services/groups/participants_service_spec.rb b/spec/services/groups/participants_service_spec.rb
new file mode 100644
index 00000000000..750aead277f
--- /dev/null
+++ b/spec/services/groups/participants_service_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::ParticipantsService do
+ describe '#group_members' do
+ let(:user) { create(:user) }
+ let(:parent_group) { create(:group) }
+ let(:group) { create(:group, parent: parent_group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:subproject) { create(:project, group: subgroup) }
+
+ it 'returns all members in parent groups, sub-groups, and sub-projects' do
+ parent_group.add_developer(create(:user))
+ subgroup.add_developer(create(:user))
+ subproject.add_developer(create(:user))
+
+ result = described_class.new(group, user).execute(nil)
+
+ expected_users = (group.self_and_hierarchy.flat_map(&:users) + subproject.users)
+ .map { |user| user_to_autocompletable(user) }
+
+ expect(expected_users.count).to eq(3)
+ expect(result).to include(*expected_users)
+ end
+ end
+
+ def user_to_autocompletable(user)
+ {
+ type: user.class.name,
+ username: user.username,
+ name: user.name,
+ avatar_url: user.avatar_url,
+ availability: user&.status&.availability
+ }
+ end
+end
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
index 4101b13adf9..2bcdfa6dd8f 100644
--- a/spec/services/import_export_clean_up_service_spec.rb
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -8,7 +8,13 @@ RSpec.describe ImportExportCleanUpService do
let(:tmp_import_export_folder) { 'tmp/gitlab_exports' }
- context 'when the import/export directory does not exist' do
+ before do
+ allow_next_instance_of(Gitlab::Import::Logger) do |logger|
+ allow(logger).to receive(:info)
+ end
+ end
+
+ context 'when the import/export tmp storage directory does not exist' do
it 'does not remove any archives' do
path = '/invalid/path/'
stub_repository_downloads_path(path)
@@ -19,49 +25,84 @@ RSpec.describe ImportExportCleanUpService do
end
end
- context 'when the import/export directory exists' do
- it 'removes old files' do
- in_directory_with_files(mtime: 2.days.ago) do |dir, files|
- service.execute
-
- files.each { |file| expect(File.exist?(file)).to eq false }
- expect(File.directory?(dir)).to eq false
+ context 'when the import/export tmp storage directory exists' do
+ shared_examples 'removes old tmp files' do |subdir|
+ it 'removes old files and logs' do
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:info)
+ .with(
+ message: 'Removed Import/Export tmp directory',
+ dir_path: anything
+ )
+ end
+
+ validate_cleanup(subdir: subdir, mtime: 2.days.ago, expected: false)
end
- end
- it 'does not remove new files' do
- in_directory_with_files(mtime: 2.hours.ago) do |dir, files|
- service.execute
+ it 'does not remove new files or logs' do
+ expect(Gitlab::Import::Logger).not_to receive(:new)
- files.each { |file| expect(File.exist?(file)).to eq true }
- expect(File.directory?(dir)).to eq true
+ validate_cleanup(subdir: subdir, mtime: 2.hours.ago, expected: true)
end
end
+
+ include_examples 'removes old tmp files', '@hashed'
+ include_examples 'removes old tmp files', '@groups'
end
context 'with uploader exports' do
- it 'removes old files' do
+ it 'removes old files and logs' do
upload = create(:import_export_upload,
updated_at: 2.days.ago,
export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:info)
+ .with(
+ message: 'Removed Import/Export export_file',
+ project_id: upload.project_id,
+ group_id: upload.group_id
+ )
+ end
+
expect { service.execute }.to change { upload.reload.export_file.file.nil? }.to(true)
+
+ expect(ImportExportUpload.where(export_file: nil)).to include(upload)
end
- it 'does not remove new files' do
+ it 'does not remove new files or logs' do
upload = create(:import_export_upload,
updated_at: 1.hour.ago,
export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+ expect(Gitlab::Import::Logger).not_to receive(:new)
+
expect { service.execute }.not_to change { upload.reload.export_file.file.nil? }
+
+ expect(ImportExportUpload.where.not(export_file: nil)).to include(upload)
+ end
+ end
+
+ def validate_cleanup(subdir:, mtime:, expected:)
+ in_directory_with_files(mtime: mtime, subdir: subdir) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq(expected) }
+ expect(File.directory?(dir)).to eq(expected)
end
end
- def in_directory_with_files(mtime:)
+ def in_directory_with_files(mtime:, subdir:)
Dir.mktmpdir do |tmpdir|
stub_repository_downloads_path(tmpdir)
- dir = File.join(tmpdir, tmp_import_export_folder, 'subfolder')
+ hashed = Digest::SHA2.hexdigest(subdir)
+ subdir_path = [subdir, hashed[0..1], hashed[2..3], hashed, hashed[4..10]]
+ dir = File.join(tmpdir, tmp_import_export_folder, *[subdir_path])
+
FileUtils.mkdir_p(dir)
+ File.utime(mtime.to_i, mtime.to_i, dir)
files = FileUtils.touch(file_list(dir) + [dir], mtime: mtime.to_time)
diff --git a/spec/services/issue_rebalancing_service_spec.rb b/spec/services/issue_rebalancing_service_spec.rb
index 1c7f74264b7..76ccb6d89ea 100644
--- a/spec/services/issue_rebalancing_service_spec.rb
+++ b/spec/services/issue_rebalancing_service_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe IssueRebalancingService do
shared_examples 'IssueRebalancingService shared examples' do
it 'rebalances a set of issues with clumps at the end and start' do
all_issues = start_clump + unclumped + end_clump.reverse
- service = described_class.new(project.issues.first)
+ service = described_class.new(Project.id_in([project.id]))
expect { service.execute }.not_to change { issues_in_position_order.map(&:id) }
@@ -55,7 +55,7 @@ RSpec.describe IssueRebalancingService do
end
it 'is idempotent' do
- service = described_class.new(project.issues.first)
+ service = described_class.new(Project.id_in(project))
expect do
service.execute
@@ -70,17 +70,17 @@ RSpec.describe IssueRebalancingService do
issue.project.group
old_pos = issue.relative_position
- service = described_class.new(issue)
+ service = described_class.new(Project.id_in(project))
expect { service.execute }.not_to exceed_query_limit(0)
expect(old_pos).to eq(issue.reload.relative_position)
end
- it 'acts if the flag is enabled for the project' do
+ it 'acts if the flag is enabled for the root namespace' do
issue = create(:issue, project: project, author: user, relative_position: max_pos)
- stub_feature_flags(rebalance_issues: issue.project)
+ stub_feature_flags(rebalance_issues: project.root_namespace)
- service = described_class.new(issue)
+ service = described_class.new(Project.id_in(project))
expect { service.execute }.to change { issue.reload.relative_position }
end
@@ -90,23 +90,22 @@ RSpec.describe IssueRebalancingService do
project.update!(group: create(:group))
stub_feature_flags(rebalance_issues: issue.project.group)
- service = described_class.new(issue)
+ service = described_class.new(Project.id_in(project))
expect { service.execute }.to change { issue.reload.relative_position }
end
it 'aborts if there are too many issues' do
- issue = project.issues.first
base = double(count: 10_001)
- allow(Issue).to receive(:relative_positioning_query_base).with(issue).and_return(base)
+ allow(Issue).to receive(:in_projects).and_return(base)
- expect { described_class.new(issue).execute }.to raise_error(described_class::TooManyIssues)
+ expect { described_class.new(Project.id_in(project)).execute }.to raise_error(described_class::TooManyIssues)
end
end
shared_examples 'rebalancing is retried on statement timeout exceptions' do
- subject { described_class.new(project.issues.first) }
+ subject { described_class.new(Project.id_in(project)) }
it 'retries update statement' do
call_count = 0
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 8950bdd465f..0b315422be8 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -3,50 +3,22 @@
require 'spec_helper'
RSpec.describe Issues::CloseService do
- subject(:close_issue) { described_class.new(project: project, current_user: user).close_issue(issue) }
-
- let_it_be(:project, refind: true) { create(:project, :repository) }
- let_it_be(:label1) { create(:label, project: project) }
- let_it_be(:label2) { create(:label, project: project, remove_on_close: true) }
- let_it_be(:author) { create(:user) }
- let_it_be(:user) { create(:user, email: "user@example.com") }
- let_it_be(:user2) { create(:user, email: "user2@example.com") }
- let_it_be(:guest) { create(:user) }
- let_it_be(:closing_merge_request) { create(:merge_request, source_project: project) }
-
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user, email: "user@example.com") }
+ let(:user2) { create(:user, email: "user2@example.com") }
+ let(:guest) { create(:user) }
+ let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) }
let(:external_issue) { ExternalIssue.new('JIRA-123', project) }
- let!(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: author) }
+ let(:closing_merge_request) { create(:merge_request, source_project: project) }
+ let(:closing_commit) { create(:commit, project: project) }
+ let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
- before_all do
+ before do
project.add_maintainer(user)
project.add_developer(user2)
project.add_guest(guest)
end
- shared_examples 'removes labels marked for removal from issue when closed' do
- before do
- issue.update!(label_ids: [label1.id, label2.id])
- end
-
- it 'removes labels marked for removal' do
- expect do
- close_issue
- end.to change { issue.reload.label_ids }.from(containing_exactly(label1.id, label2.id)).to(containing_exactly(label1.id))
- end
-
- it 'creates system notes for the removed labels' do
- expect do
- close_issue
- end.to change(ResourceLabelEvent, :count).by(1)
-
- expect(ResourceLabelEvent.last.slice(:action, :issue_id, :label_id)).to eq(
- 'action' => 'remove',
- 'issue_id' => issue.id,
- 'label_id' => label2.id
- )
- end
- end
-
describe '#execute' do
let(:service) { described_class.new(project: project, current_user: user) }
@@ -131,7 +103,7 @@ RSpec.describe Issues::CloseService do
end
context 'with an active external issue tracker not supporting close_issue' do
- let!(:external_issue_tracker) { create(:bugzilla_service, project: project) }
+ let!(:external_issue_tracker) { create(:bugzilla_integration, project: project) }
it 'does not close the issue on the external issue tracker' do
project.reload
@@ -149,8 +121,6 @@ RSpec.describe Issues::CloseService do
end
end
- it_behaves_like 'removes labels marked for removal from issue when closed'
-
it 'mentions closure via a merge request' do
close_issue
@@ -214,18 +184,10 @@ RSpec.describe Issues::CloseService do
end
context "closed by a commit", :sidekiq_might_not_need_inline do
- subject(:close_issue) 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)
end
- end
-
- let(:closing_commit) { create(:commit, project: project) }
-
- it_behaves_like 'removes labels marked for removal from issue when closed'
-
- it 'mentions closure via a commit' do
- close_issue
email = ActionMailer::Base.deliveries.last
@@ -237,8 +199,9 @@ RSpec.describe Issues::CloseService do
context 'when user cannot read the commit' do
it 'does not mention the commit id' do
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
-
- close_issue
+ perform_enqueued_jobs do
+ described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit)
+ end
email = ActionMailer::Base.deliveries.last
body_text = email.body.parts.map(&:body).join(" ")
@@ -259,14 +222,12 @@ RSpec.describe Issues::CloseService do
it 'verifies the number of queries' do
recorded = ActiveRecord::QueryRecorder.new { close_issue }
- expected_queries = 32
+ expected_queries = 24
expect(recorded.count).to be <= expected_queries
expect(recorded.cached_count).to eq(0)
end
- it_behaves_like 'removes labels marked for removal from issue when closed'
-
it 'closes the issue' do
close_issue
@@ -296,8 +257,6 @@ RSpec.describe Issues::CloseService do
end
it 'marks todos as done' do
- todo = create(:todo, :assigned, user: user, project: project, target: issue, author: user2)
-
close_issue
expect(todo.reload).to be_done
@@ -362,32 +321,26 @@ RSpec.describe Issues::CloseService do
end
context 'when issue is not confidential' do
- it_behaves_like 'removes labels marked for removal from issue when closed'
-
it 'executes issue hooks' do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
- close_issue
+ described_class.new(project: project, current_user: user).close_issue(issue)
end
end
context 'when issue is confidential' do
- let(:issue) { create(:issue, :confidential, project: project) }
-
- it_behaves_like 'removes labels marked for removal from issue when closed'
-
it 'executes confidential issue hooks' do
+ issue = create(:issue, :confidential, project: project)
+
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
- close_issue
+ described_class.new(project: project, current_user: user).close_issue(issue)
end
end
context 'internal issues disabled' do
- let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
-
before do
project.issues_enabled = false
project.save!
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 9c84242d8ae..94810d6134a 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -18,8 +18,8 @@ RSpec.describe Issues::CreateService do
let_it_be(:labels) { create_pair(:label, project: project) }
before_all do
- project.add_maintainer(user)
- project.add_maintainer(assignee)
+ project.add_guest(user)
+ project.add_guest(assignee)
end
let(:opts) do
@@ -78,8 +78,8 @@ RSpec.describe Issues::CreateService do
opts.merge!(title: '')
end
- it 'does not create an incident label prematurely' do
- expect { subject }.not_to change(Label, :count)
+ it 'does not apply an incident label prematurely' do
+ expect { subject }.to not_change(LabelLink, :count).and not_change(Issue, :count)
end
end
end
@@ -88,15 +88,11 @@ RSpec.describe Issues::CreateService do
expect { issue }.to change { project.open_issues_count }.from(0).to(1)
end
- context 'when current user cannot admin issues in the project' do
- let_it_be(:guest) { create(:user) }
-
- before_all do
- project.add_guest(guest)
- end
+ context 'when current user cannot set issue metadata in the project' do
+ let_it_be(:non_member) { create(:user) }
- it 'filters out params that cannot be set without the :admin_issue permission' do
- issue = described_class.new(project: project, current_user: guest, params: opts).execute
+ it 'filters out params that cannot be set without the :set_issue_metadata permission' do
+ issue = described_class.new(project: project, current_user: non_member, params: opts).execute
expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue')
@@ -107,8 +103,8 @@ RSpec.describe Issues::CreateService do
expect(issue.due_date).to be_nil
end
- it 'creates confidential issues' do
- issue = described_class.new(project: project, current_user: guest, params: { confidential: true }).execute
+ it 'can create confidential issues' do
+ issue = described_class.new(project: project, current_user: non_member, params: { confidential: true }).execute
expect(issue.confidential).to be_truthy
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 8c97dd95ced..b95d94e3784 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -158,6 +158,90 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
+ context 'changing issue_type' do
+ let!(:label_1) { create(:label, project: project, title: 'incident') }
+ let!(:label_2) { create(:label, project: project, title: 'missed-sla') }
+
+ before do
+ stub_licensed_features(quality_management: true)
+ end
+
+ context 'from issue to incident' do
+ it 'adds a `incident` label if one does not exist' do
+ expect { update_issue(issue_type: 'incident') }.to change(issue.labels, :count).by(1)
+ expect(issue.labels.pluck(:title)).to eq(['incident'])
+ end
+
+ context 'for an issue with multiple labels' do
+ let(:issue) { create(:incident, project: project, labels: [label_1]) }
+
+ before do
+ update_issue(issue_type: 'incident')
+ end
+
+ it 'does not add an `incident` label if one already exist' do
+ expect(issue.labels).to eq([label_1])
+ end
+ end
+
+ context 'filtering the incident label' do
+ let(:params) { { add_label_ids: [] } }
+
+ before do
+ update_issue(issue_type: 'incident')
+ end
+
+ it 'creates and add a incident label id to add_label_ids' do
+ expect(issue.label_ids).to contain_exactly(label_1.id)
+ end
+ end
+ end
+
+ context 'from incident to issue' do
+ let(:issue) { create(:incident, project: project) }
+
+ context 'for an incident with multiple labels' do
+ let(:issue) { create(:incident, project: project, labels: [label_1, label_2]) }
+
+ before do
+ update_issue(issue_type: 'issue')
+ end
+
+ it 'removes an `incident` label if one exists on the incident' do
+ expect(issue.labels).to eq([label_2])
+ end
+ end
+
+ context 'filtering the incident label' do
+ let(:issue) { create(:incident, project: project, labels: [label_1, label_2]) }
+ let(:params) { { label_ids: [label_1.id, label_2.id], remove_label_ids: [] } }
+
+ before do
+ update_issue(issue_type: 'issue')
+ end
+
+ it 'adds an incident label id to remove_label_ids for it to be removed' do
+ expect(issue.label_ids).to contain_exactly(label_2.id)
+ end
+ end
+ end
+
+ context 'from issue to restricted issue types' do
+ context 'without sufficient permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ it 'does nothing to the labels' do
+ expect { update_issue(issue_type: 'issue') }.not_to change(issue.labels, :count)
+ expect(issue.reload.labels).to eq([])
+ end
+ end
+ end
+ end
+
it 'updates open issue counter for assignees when issue is reassigned' do
update_issue(assignee_ids: [user2.id])
@@ -225,7 +309,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
- expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
@@ -239,7 +323,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
- expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
@@ -253,7 +337,7 @@ RSpec.describe Issues::UpdateService, :mailer do
opts[:move_between_ids] = [issue1.id, issue2.id]
- expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.root_namespace.id)
update_issue(opts)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index 19db892fcae..d662d9fa978 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -53,7 +53,10 @@ RSpec.describe Issues::ZoomLinkService do
category: 'IncidentManagement::ZoomIntegration',
action: 'add_zoom_meeting',
label: 'Issue ID',
- value: issue.id
+ value: issue.id,
+ user: user,
+ project: project,
+ namespace: project.namespace
)
end
@@ -192,7 +195,10 @@ RSpec.describe Issues::ZoomLinkService do
category: 'IncidentManagement::ZoomIntegration',
action: 'remove_zoom_meeting',
label: 'Issue ID',
- value: issue.id
+ value: issue.id,
+ user: user,
+ project: project,
+ namespace: project.namespace
)
end
end
diff --git a/spec/services/jira_import/users_importer_spec.rb b/spec/services/jira_import/users_importer_spec.rb
index c825f899f80..2e8c556d62c 100644
--- a/spec/services/jira_import/users_importer_spec.rb
+++ b/spec/services/jira_import/users_importer_spec.rb
@@ -43,39 +43,37 @@ RSpec.describe JiraImport::UsersImporter do
end
end
- RSpec.shared_examples 'maps jira users to gitlab users' do
+ RSpec.shared_examples 'maps Jira users to GitLab users' do |users_mapper_service:|
context 'when Jira import is configured correctly' do
- let_it_be(:jira_service) { create(:jira_service, project: project, active: true) }
- let(:client) { double }
+ let_it_be(:jira_service) { create(:jira_service, project: project, active: true, url: "http://jira.example.net") }
- before do
- expect(importer).to receive(:client).at_least(1).and_return(client)
- allow(client).to receive_message_chain(:ServerInfo, :all, :deploymentType).and_return(deployment_type)
- end
-
- context 'when jira client raises an error' do
+ context 'when users mapper service raises an error' do
let(:error) { Timeout::Error.new }
it 'returns an error response' do
- expect(client).to receive(:get).and_raise(error)
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(error, project_id: project.id)
+ expect_next_instance_of(users_mapper_service) do |instance|
+ expect(instance).to receive(:execute).and_raise(error)
+ end
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(error, project_id: project.id)
expect(subject.error?).to be_truthy
expect(subject.message).to include('There was an error when communicating to Jira')
end
end
- context 'when jira client returns result' do
- context 'when jira client returns an empty array' do
- let(:jira_users) { [] }
-
+ context 'when users mapper service returns result' do
+ context 'when users mapper service returns an empty array' do
it 'returns nil payload' do
+ expect_next_instance_of(users_mapper_service) do |instance|
+ expect(instance).to receive(:execute).and_return([])
+ end
+
expect(subject.success?).to be_truthy
expect(subject.payload).to be_empty
end
end
- context 'when jira client returns an results' do
+ context 'when Jira client returns any users' do
let_it_be(:project_member) { create(:user, email: 'sample@jira.com') }
let_it_be(:group_member) { create(:user, name: 'user-name2') }
let_it_be(:other_user) { create(:user) }
@@ -86,6 +84,10 @@ RSpec.describe JiraImport::UsersImporter do
end
it 'returns the mapped users' do
+ expect_next_instance_of(users_mapper_service) do |instance|
+ expect(instance).to receive(:execute).and_return(mapped_users)
+ end
+
expect(subject.success?).to be_truthy
expect(subject.payload).to eq(mapped_users)
end
@@ -95,43 +97,23 @@ RSpec.describe JiraImport::UsersImporter do
end
context 'when Jira instance is of Server deployment type' do
- let(:deployment_type) { 'Server' }
- let(:url) { "/rest/api/2/user/search?username=''&maxResults=50&startAt=#{start_at}" }
- let(:jira_users) do
- [
- { 'key' => 'acc1', 'name' => 'user-name1', 'emailAddress' => 'sample@jira.com' },
- { 'key' => 'acc2', 'name' => 'user-name2' }
- ]
- end
-
before do
- allow_next_instance_of(JiraImport::ServerUsersMapperService) do |instance|
- allow(instance).to receive(:client).and_return(client)
- allow(client).to receive(:get).with(url).and_return(jira_users)
- end
+ allow(project).to receive(:jira_service).and_return(jira_service)
+
+ jira_service.data_fields.deployment_server!
end
- it_behaves_like 'maps jira users to gitlab users'
+ it_behaves_like 'maps Jira users to GitLab users', users_mapper_service: JiraImport::ServerUsersMapperService
end
- context 'when Jira instance is of Cloud deploymet type' do
- let(:deployment_type) { 'Cloud' }
- let(:url) { "/rest/api/2/users?maxResults=50&startAt=#{start_at}" }
- let(:jira_users) do
- [
- { 'accountId' => 'acc1', 'displayName' => 'user-name1', 'emailAddress' => 'sample@jira.com' },
- { 'accountId' => 'acc2', 'displayName' => 'user-name2' }
- ]
- end
-
+ context 'when Jira instance is of Cloud deployment type' do
before do
- allow_next_instance_of(JiraImport::CloudUsersMapperService) do |instance|
- allow(instance).to receive(:client).and_return(client)
- allow(client).to receive(:get).with(url).and_return(jira_users)
- end
+ allow(project).to receive(:jira_service).and_return(jira_service)
+
+ jira_service.data_fields.deployment_cloud!
end
- it_behaves_like 'maps jira users to gitlab users'
+ it_behaves_like 'maps Jira users to GitLab users', users_mapper_service: JiraImport::CloudUsersMapperService
end
end
end
diff --git a/spec/services/lfs/push_service_spec.rb b/spec/services/lfs/push_service_spec.rb
index 58fb2f3fb9b..e1564ca2359 100644
--- a/spec/services/lfs/push_service_spec.rb
+++ b/spec/services/lfs/push_service_spec.rb
@@ -8,13 +8,14 @@ RSpec.describe Lfs::PushService do
let_it_be(:project) { create(:forked_project_with_submodules) }
let_it_be(:remote_mirror) { create(:remote_mirror, project: project, enabled: true) }
- let_it_be(:lfs_object) { create_linked_lfs_object(project, :project) }
let(:params) { { url: remote_mirror.bare_url, credentials: remote_mirror.credentials } }
subject(:service) { described_class.new(project, nil, params) }
describe "#execute" do
+ let_it_be(:lfs_object) { create_linked_lfs_object(project, :project) }
+
it 'uploads the object when upload is requested' do
stub_lfs_batch(lfs_object)
@@ -25,14 +26,6 @@ RSpec.describe Lfs::PushService do
expect(service.execute).to eq(status: :success)
end
- it 'does nothing if there are no LFS objects' do
- lfs_object.destroy!
-
- expect(lfs_client).not_to receive(:upload!)
-
- expect(service.execute).to eq(status: :success)
- end
-
it 'does not upload the object when upload is not requested' do
stub_lfs_batch(lfs_object, upload: false)
@@ -88,6 +81,12 @@ RSpec.describe Lfs::PushService do
end
end
+ it 'does nothing if there are no LFS objects' do
+ expect(lfs_client).not_to receive(:upload!)
+
+ expect(service.execute).to eq(status: :success)
+ end
+
def create_linked_lfs_object(project, type)
create(:lfs_objects_project, project: project, repository_type: type).lfs_object
end
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 916941e1111..ffe63a8a94b 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -8,7 +8,8 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
let_it_be(:member) { create(:user) }
let_it_be(:user_ids) { member.id.to_s }
let_it_be(:access_level) { Gitlab::Access::GUEST }
- let(:params) { { user_ids: user_ids, access_level: access_level } }
+ let(:additional_params) { { invite_source: '_invite_source_' } }
+ let(:params) { { user_ids: user_ids, access_level: access_level }.merge(additional_params) }
subject(:execute_service) { described_class.new(user, params.merge({ source: source })).execute }
@@ -82,4 +83,46 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_
expect(OnboardingProgress.completed?(source.namespace, :user_added)).to be(false)
end
end
+
+ context 'when tracking the invite source', :snowplow do
+ context 'when invite_source is not passed' do
+ let(:additional_params) { {} }
+
+ it 'tracks the invite source as unknown' do
+ expect { execute_service }.to raise_error(ArgumentError, 'No invite source provided.')
+
+ expect_no_snowplow_event
+ end
+ end
+
+ context 'when invite_source is passed' do
+ it 'tracks the invite source from params' do
+ execute_service
+
+ expect_snowplow_event(
+ category: described_class.name,
+ action: 'create_member',
+ label: '_invite_source_',
+ property: 'existing_user',
+ user: user
+ )
+ end
+ end
+
+ context 'when it is a net_new_user' do
+ let(:additional_params) { { invite_source: '_invite_source_', user_ids: 'email@example.org' } }
+
+ it 'tracks the invite source from params' do
+ execute_service
+
+ expect_snowplow_event(
+ category: described_class.name,
+ action: 'create_member',
+ label: '_invite_source_',
+ property: 'net_new_user',
+ user: user
+ )
+ end
+ end
+ end
end
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index d7fd7d5b2ca..c530e3d0c53 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_shared_state, :sidekiq_inline do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project, reload: true) { create(:project) }
let_it_be(:user) { project.owner }
let_it_be(:project_user) { create(:user) }
let_it_be(:namespace) { project.namespace }
let(:params) { {} }
- let(:base_params) { { access_level: Gitlab::Access::GUEST, source: project } }
+ let(:base_params) { { access_level: Gitlab::Access::GUEST, source: project, invite_source: '_invite_source_' } }
subject(:result) { described_class.new(user, base_params.merge(params) ).execute }
@@ -23,6 +23,18 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
it_behaves_like 'records an onboarding progress action', :user_added
end
+ context 'when email belongs to an existing user as a secondary email' do
+ let(:secondary_email) { create(:email, email: 'secondary@example.com', user: project_user) }
+ let(:params) { { email: secondary_email.email } }
+
+ it 'adds an existing user to members', :aggregate_failures do
+ expect_to_create_members(count: 1)
+ expect(result[:status]).to eq(:success)
+ expect(project.users).to include project_user
+ expect(project.members.last).not_to be_invite
+ end
+ end
+
context 'when email is not a valid email' do
let(:params) { { email: '_bogus_' } }
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 5a6a9df3f44..d10f82289bd 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -251,22 +251,22 @@ RSpec.describe MergeRequests::BuildService do
end
context 'when the source branch matches an issue' do
- where(:issue_tracker, :source_branch, :closing_message) do
- :jira | 'FOO-123-fix-issue' | 'Closes FOO-123'
- :jira | 'fix-issue' | nil
- :custom_issue_tracker | '123-fix-issue' | 'Closes #123'
- :custom_issue_tracker | 'fix-issue' | nil
- :internal | '123-fix-issue' | 'Closes #123'
- :internal | 'fix-issue' | nil
+ where(:factory, :source_branch, :closing_message) do
+ :jira_service | 'FOO-123-fix-issue' | 'Closes FOO-123'
+ :jira_service | 'fix-issue' | nil
+ :custom_issue_tracker_integration | '123-fix-issue' | 'Closes #123'
+ :custom_issue_tracker_integration | 'fix-issue' | nil
+ nil | '123-fix-issue' | 'Closes #123'
+ nil | 'fix-issue' | nil
end
with_them do
before do
- if issue_tracker == :internal
- issue.update!(iid: 123)
- else
- create(:"#{issue_tracker}_service", project: project)
+ if factory
+ create(factory, project: project)
project.reload
+ else
+ issue.update!(iid: 123)
end
end
@@ -350,23 +350,23 @@ RSpec.describe MergeRequests::BuildService do
end
context 'when the source branch matches an issue' do
- where(:issue_tracker, :source_branch, :title, :closing_message) do
- :jira | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123'
- :jira | 'fix-issue' | 'Fix issue' | nil
- :custom_issue_tracker | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123'
- :custom_issue_tracker | 'fix-issue' | 'Fix issue' | nil
- :internal | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123'
- :internal | 'fix-issue' | 'Fix issue' | nil
- :internal | '124-fix-issue' | '124 fix issue' | nil
+ where(:factory, :source_branch, :title, :closing_message) do
+ :jira_service | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123'
+ :jira_service | 'fix-issue' | 'Fix issue' | nil
+ :custom_issue_tracker_integration | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123'
+ :custom_issue_tracker_integration | 'fix-issue' | 'Fix issue' | nil
+ nil | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123'
+ nil | 'fix-issue' | 'Fix issue' | nil
+ nil | '124-fix-issue' | '124 fix issue' | nil
end
with_them do
before do
- if issue_tracker == :internal
- issue.update!(iid: 123)
- else
- create(:"#{issue_tracker}_service", project: project)
+ if factory
+ create(factory, project: project)
project.reload
+ else
+ issue.update!(iid: 123)
end
end
@@ -399,23 +399,23 @@ RSpec.describe MergeRequests::BuildService do
end
context 'when the source branch matches an issue' do
- where(:issue_tracker, :source_branch, :title, :closing_message) do
- :jira | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123'
- :jira | 'fix-issue' | 'Fix issue' | nil
- :custom_issue_tracker | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123'
- :custom_issue_tracker | 'fix-issue' | 'Fix issue' | nil
- :internal | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123'
- :internal | 'fix-issue' | 'Fix issue' | nil
- :internal | '124-fix-issue' | '124 fix issue' | nil
+ where(:factory, :source_branch, :title, :closing_message) do
+ :jira_service | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123'
+ :jira_service | 'fix-issue' | 'Fix issue' | nil
+ :custom_issue_tracker_integration | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123'
+ :custom_issue_tracker_integration | 'fix-issue' | 'Fix issue' | nil
+ nil | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123'
+ nil | 'fix-issue' | 'Fix issue' | nil
+ nil | '124-fix-issue' | '124 fix issue' | nil
end
with_them do
before do
- if issue_tracker == :internal
- issue.update!(iid: 123)
- else
- create(:"#{issue_tracker}_service", project: project)
+ if factory
+ create(factory, project: project)
project.reload
+ else
+ issue.update!(iid: 123)
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index b2351ab53bd..da547716e1e 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -82,7 +82,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
let(:opts) do
{
title: 'Awesome merge_request',
- description: "well this is not done yet\n/wip",
+ description: "well this is not done yet\n/draft",
source_branch: 'feature',
target_branch: 'master',
assignees: [user2]
diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
index 0bf18f16abb..f9eed6eea2d 100644
--- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb
+++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:assignee) { create(:user) }
- let_it_be(:merge_request) { create(:merge_request, author: user, source_project: project, assignees: [assignee]) }
+ let_it_be_with_reload(:merge_request) { create(:merge_request, author: user, source_project: project, assignees: [assignee]) }
let_it_be(:old_assignees) { create_list(:user, 3) }
let(:options) { {} }
@@ -45,13 +45,27 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do
service.execute(merge_request, old_assignees, options)
end
+ let(:note) { merge_request.notes.system.last }
+ let(:removed_note) { "unassigned #{old_assignees.map(&:to_reference).to_sentence}" }
+
+ context 'when unassigning all users' do
+ before do
+ merge_request.update!(assignee_ids: [])
+ end
+
+ it 'creates assignee note' do
+ execute
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq removed_note
+ end
+ end
+
it 'creates assignee note' do
execute
- note = merge_request.notes.last
-
expect(note).not_to be_nil
- expect(note.note).to include "assigned to #{assignee.to_reference} and unassigned #{old_assignees.map(&:to_reference).to_sentence}"
+ expect(note.note).to include "assigned to #{assignee.to_reference} and #{removed_note}"
end
it 'sends email notifications to old and new assignees', :mailer, :sidekiq_inline do
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ac39fb59c62..503c0282bd6 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -181,7 +181,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
- expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue, user).once
+ expect_any_instance_of(Integrations::Jira).to receive(:close_issue).with(merge_request, jira_issue, user).once
service.execute(merge_request)
end
@@ -193,7 +193,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
- expect_any_instance_of(JiraService).not_to receive(:close_issue)
+ expect_any_instance_of(Integrations::Jira).not_to receive(:close_issue)
service.execute(merge_request)
end
diff --git a/spec/services/merge_requests/update_assignees_service_spec.rb b/spec/services/merge_requests/update_assignees_service_spec.rb
index 076161c9029..3a0b17c2768 100644
--- a/spec/services/merge_requests/update_assignees_service_spec.rb
+++ b/spec/services/merge_requests/update_assignees_service_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe MergeRequests::UpdateAssigneesService do
description: "FYI #{user2.to_reference}",
assignee_ids: [user3.id],
source_project: project,
+ target_project: project,
author: create(:user))
end
@@ -24,6 +25,7 @@ RSpec.describe MergeRequests::UpdateAssigneesService do
project.add_maintainer(user)
project.add_developer(user2)
project.add_developer(user3)
+ merge_request.errors.clear
end
let(:service) { described_class.new(project: project, current_user: user, params: opts) }
@@ -32,35 +34,53 @@ RSpec.describe MergeRequests::UpdateAssigneesService do
describe 'execute' do
def update_merge_request
service.execute(merge_request)
- merge_request.reload
+ end
+
+ shared_examples 'removing all assignees' do
+ it 'removes all assignees' do
+ expect(update_merge_request).to have_attributes(assignees: be_empty, errors: be_none)
+ end
+
+ it 'enqueues the correct background work' do
+ expect_next(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service|
+ expect(service)
+ .to receive(:async_execute)
+ .with(merge_request, [user3], execute_hooks: true)
+ end
+
+ update_merge_request
+ end
end
context 'when the parameters are valid' do
context 'when using sentinel values' do
- let(:opts) { { assignee_ids: [0] } }
+ context 'when using assignee_ids' do
+ let(:opts) { { assignee_ids: [0] } }
+
+ it_behaves_like 'removing all assignees'
+ end
- it 'removes all assignees' do
- expect { update_merge_request }.to change(merge_request, :assignees).to([])
+ context 'when using assignee_id' do
+ let(:opts) { { assignee_id: 0 } }
+
+ it_behaves_like 'removing all assignees'
end
end
- context 'the assignee_ids parameter is the empty list' do
+ context 'when the assignee_ids parameter is the empty list' do
let(:opts) { { assignee_ids: [] } }
- it 'removes all assignees' do
- expect { update_merge_request }.to change(merge_request, :assignees).to([])
- end
+ it_behaves_like 'removing all assignees'
end
it 'updates the MR, and queues the more expensive work for later' do
expect_next(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service|
expect(service)
- .to receive(:async_execute)
- .with(merge_request, [user3], execute_hooks: true)
+ .to receive(:async_execute).with(merge_request, [user3], execute_hooks: true)
end
expect { update_merge_request }
- .to change(merge_request, :assignees).to([user2])
+ .to change { merge_request.reload.assignees }.from([user3]).to([user2])
.and change(merge_request, :updated_at)
.and change(merge_request, :updated_by).to(user)
end
@@ -68,7 +88,10 @@ RSpec.describe MergeRequests::UpdateAssigneesService do
it 'does not update the assignees if they do not have access' do
opts[:assignee_ids] = [create(:user).id]
- expect { update_merge_request }.not_to change(merge_request, :assignee_ids)
+ expect(update_merge_request).to have_attributes(
+ assignees: [user3],
+ errors: be_any
+ )
end
it 'is more efficient than using the full update-service' do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index a85fbd77d70..6ec2b158d30 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -297,6 +297,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
reviewers: [],
milestone: nil,
total_time_spent: 0,
+ time_change: 0,
description: "FYI #{user2.to_reference}"
}
)
@@ -768,6 +769,13 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
update_merge_request({ target_branch: 'target' })
end
+
+ it "does not try to mark as unchecked if it's already unchecked" do
+ expect(merge_request).to receive(:unchecked?).and_return(true)
+ expect(merge_request).not_to receive(:mark_as_unchecked)
+
+ update_merge_request({ target_branch: "target" })
+ end
end
context 'when auto merge is enabled and target branch changed' do
diff --git a/spec/services/namespace_settings/update_service_spec.rb b/spec/services/namespace_settings/update_service_spec.rb
index 887d56df099..8e176dbc6cd 100644
--- a/spec/services/namespace_settings/update_service_spec.rb
+++ b/spec/services/namespace_settings/update_service_spec.rb
@@ -75,5 +75,37 @@ RSpec.describe NamespaceSettings::UpdateService do
end
end
end
+
+ context "updating :prevent_sharing_groups_outside_hierarchy" do
+ let(:settings) { { prevent_sharing_groups_outside_hierarchy: true } }
+
+ context 'when user is a group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'changes settings' do
+ expect { service.execute }
+ .to change { group.namespace_settings.prevent_sharing_groups_outside_hierarchy }
+ .from(false).to(true)
+ end
+ end
+
+ context 'when user is not a group owner' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it 'does not change settings' do
+ expect { service.execute }.not_to change { group.namespace_settings.prevent_sharing_groups_outside_hierarchy }
+ end
+
+ it 'returns the group owner error' do
+ service.execute
+
+ expect(group.namespace_settings.errors.messages[:prevent_sharing_groups_outside_hierarchy]).to include('can only be changed by a group admin.')
+ end
+ end
+ end
end
end
diff --git a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
index 3094f574184..2bf02e541f9 100644
--- a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
+++ b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb
@@ -41,22 +41,23 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
using RSpec::Parameterized::TableSyntax
where(:track, :interval, :actions_completed) do
- :create | 1 | { created_at: frozen_time - 2.days }
- :create | 5 | { created_at: frozen_time - 6.days }
- :create | 10 | { created_at: frozen_time - 11.days }
- :verify | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days }
- :verify | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days }
- :verify | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days }
- :trial | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days, pipeline_created_at: frozen_time - 2.days }
- :trial | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days, pipeline_created_at: frozen_time - 6.days }
- :trial | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days, pipeline_created_at: frozen_time - 11.days }
- :team | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days, pipeline_created_at: frozen_time - 2.days, trial_started_at: frozen_time - 2.days }
- :team | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days, pipeline_created_at: frozen_time - 6.days, trial_started_at: frozen_time - 6.days }
- :team | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days, pipeline_created_at: frozen_time - 11.days, trial_started_at: frozen_time - 11.days }
+ :create | 1 | { created_at: frozen_time - 2.days }
+ :create | 5 | { created_at: frozen_time - 6.days }
+ :create | 10 | { created_at: frozen_time - 11.days }
+ :verify | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days }
+ :verify | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days }
+ :verify | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days }
+ :trial | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days, pipeline_created_at: frozen_time - 2.days }
+ :trial | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days, pipeline_created_at: frozen_time - 6.days }
+ :trial | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days, pipeline_created_at: frozen_time - 11.days }
+ :team | 1 | { created_at: frozen_time - 2.days, git_write_at: frozen_time - 2.days, pipeline_created_at: frozen_time - 2.days, trial_started_at: frozen_time - 2.days }
+ :team | 5 | { created_at: frozen_time - 6.days, git_write_at: frozen_time - 6.days, pipeline_created_at: frozen_time - 6.days, trial_started_at: frozen_time - 6.days }
+ :team | 10 | { created_at: frozen_time - 11.days, git_write_at: frozen_time - 11.days, pipeline_created_at: frozen_time - 11.days, trial_started_at: frozen_time - 11.days }
+ :experience | 30 | { created_at: frozen_time - 31.days, git_write_at: frozen_time - 31.days }
end
with_them do
- it { is_expected.to send_in_product_marketing_email(user.id, group.id, track, described_class::INTERVAL_DAYS.index(interval)) }
+ it { is_expected.to send_in_product_marketing_email(user.id, group.id, track, described_class::TRACKS[track][:interval_days].index(interval)) }
end
end
@@ -235,7 +236,7 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
let(:track) { :foo }
before do
- stub_const("#{described_class}::TRACKS", { bar: :git_write })
+ stub_const("#{described_class}::TRACKS", { bar: {} })
end
it { expect { subject }.to raise_error(ArgumentError, 'Track foo not defined') }
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 31263feb947..5b4d6188b66 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -307,7 +307,7 @@ RSpec.describe Notes::CreateService do
),
# Set WIP status
QuickAction.new(
- action_text: "/wip",
+ action_text: "/draft",
before_action: -> {
issuable.reload.update!(title: "title")
},
@@ -317,7 +317,7 @@ RSpec.describe Notes::CreateService do
),
# Remove WIP status
QuickAction.new(
- action_text: "/wip",
+ action_text: "/draft",
before_action: -> {
issuable.reload.update!(title: "WIP: title")
},
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index 9692bb08379..cb7d0163cac 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -130,6 +130,17 @@ RSpec.describe Notes::QuickActionsService do
end
end
+ describe '/estimate' do
+ let(:note_text) { '/estimate 1h' }
+
+ it 'adds time estimate to noteable' do
+ content = execute(note)
+
+ expect(content).to be_empty
+ expect(note.noteable.time_estimate).to eq(3600)
+ end
+ end
+
describe 'note with command & text' do
describe '/close, /label, /assign & /milestone' do
let(:note_text) do
@@ -302,6 +313,11 @@ RSpec.describe Notes::QuickActionsService do
end
it_behaves_like 'note on noteable that supports quick actions' do
+ let_it_be(:incident, reload: true) { create(:incident, project: project) }
+ let(:note) { build(:note_on_issue, project: project, noteable: incident) }
+ end
+
+ it_behaves_like 'note on noteable that supports quick actions' do
let(:merge_request) { create(:merge_request, source_project: project) }
let(:note) { build(:note_on_merge_request, project: project, noteable: merge_request) }
end
diff --git a/spec/services/notification_recipients/builder/default_spec.rb b/spec/services/notification_recipients/builder/default_spec.rb
index 994138ea828..c142cc11384 100644
--- a/spec/services/notification_recipients/builder/default_spec.rb
+++ b/spec/services/notification_recipients/builder/default_spec.rb
@@ -160,21 +160,7 @@ RSpec.describe NotificationRecipients::Builder::Default do
end
end
- before do
- stub_feature_flags(notification_setting_recipient_refactor: enabled)
- end
-
- context 'with notification_setting_recipient_refactor enabled' do
- let(:enabled) { true }
-
- it_behaves_like 'custom notification recipients'
- end
-
- context 'with notification_setting_recipient_refactor disabled' do
- let(:enabled) { false }
-
- it_behaves_like 'custom notification recipients'
- end
+ it_behaves_like 'custom notification recipients'
end
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 87cf1070075..ecf82c6a1db 100644
--- a/spec/services/packages/debian/create_distribution_service_spec.rb
+++ b/spec/services/packages/debian/create_distribution_service_spec.rb
@@ -4,8 +4,12 @@ require 'spec_helper'
RSpec.describe Packages::Debian::CreateDistributionService 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
+
it 'returns ServiceResponse', :aggregate_failures do
if expected_message.nil?
+ expect(::Packages::Debian::GenerateDistributionWorker).to receive(:perform_async).with(container_type, an_instance_of(Integer))
+
expect { response }
.to change { container.debian_distributions.klass.all.count }
.from(0).to(1)
@@ -18,6 +22,7 @@ RSpec.describe Packages::Debian::CreateDistributionService do
.and not_change { Packages::Debian::ProjectComponentFile.count }
.and not_change { Packages::Debian::GroupComponentFile.count }
else
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async)
expect { response }
.to not_change { container.debian_distributions.klass.all.count }
.and not_change { container.debian_distributions.count }
@@ -109,13 +114,13 @@ RSpec.describe Packages::Debian::CreateDistributionService do
let(:response) { subject.execute }
context 'within a projet' do
- let_it_be(:container) { create(:project) }
+ let_it_be(:container_type) { :project }
it_behaves_like 'Debian Create Distribution Service'
end
context 'within a group' do
- let_it_be(:container) { create(:group) }
+ let_it_be(:container_type) { :group }
it_behaves_like 'Debian Create Distribution Service'
end
diff --git a/spec/services/packages/debian/destroy_distribution_service_spec.rb b/spec/services/packages/debian/destroy_distribution_service_spec.rb
deleted file mode 100644
index e4c43884bb4..00000000000
--- a/spec/services/packages/debian/destroy_distribution_service_spec.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Packages::Debian::DestroyDistributionService do
- RSpec.shared_examples 'Destroy Debian Distribution' do |expected_message|
- it 'returns ServiceResponse', :aggregate_failures do
- if expected_message.nil?
- expect { response }
- .to change { container.debian_distributions.klass.all.count }
- .from(1).to(0)
- .and change { container.debian_distributions.count }
- .from(1).to(0)
- .and change { component1.class.all.count }
- .from(2).to(0)
- .and change { architecture1.class.all.count }
- .from(3).to(0)
- .and change { component_file1.class.all.count }
- .from(4).to(0)
- else
- expect { response }
- .to not_change { container.debian_distributions.klass.all.count }
- .and not_change { container.debian_distributions.count }
- .and not_change { component1.class.all.count }
- .and not_change { architecture1.class.all.count }
- .and not_change { component_file1.class.all.count }
- end
-
- expect(response).to be_a(ServiceResponse)
- expect(response.success?).to eq(expected_message.nil?)
- expect(response.error?).to eq(!expected_message.nil?)
- expect(response.message).to eq(expected_message)
-
- if expected_message.nil?
- expect(response.payload).to eq({})
- else
- expect(response.payload).to eq(distribution: distribution)
- end
- end
- end
-
- RSpec.shared_examples 'Debian Destroy Distribution Service' do |container_type, can_freeze|
- context "with a Debian #{container_type} distribution" do
- let_it_be(:container, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
- let_it_be(:distribution, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container) }
- let_it_be(:component1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution, name: 'component1') }
- let_it_be(:component2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution, name: 'component2') }
- let_it_be(:architecture0, freeze: true) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
- let_it_be(:architecture1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture1') }
- let_it_be(:architecture2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'architecture2') }
- let_it_be(:component_file1, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component1) }
- let_it_be(:component_file2, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1, architecture: architecture1) }
- let_it_be(:component_file3, freeze: can_freeze) { create("debian_#{container_type}_component_file", :source, component: component2) }
- let_it_be(:component_file4, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2, architecture: architecture2) }
-
- subject { described_class.new(distribution) }
-
- let(:response) { subject.execute }
-
- context 'with a distribution' do
- it_behaves_like 'Destroy Debian Distribution'
- end
-
- context 'when destroy fails' do
- let(:distribution) { create("debian_#{container_type}_distribution", container: container) }
-
- before do
- expect(distribution).to receive(:destroy).and_return(false)
- end
-
- it_behaves_like 'Destroy Debian Distribution', "Unable to destroy Debian #{container_type} distribution"
- end
- end
- end
-
- it_behaves_like 'Debian Destroy Distribution Service', :project, true
- it_behaves_like 'Debian Destroy Distribution Service', :group, false
-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 2a92b8ed26e..ced846866c2 100644
--- a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
+++ b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb
@@ -6,8 +6,10 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
let_it_be(:distribution) { create(:debian_project_distribution, codename: 'unstable') }
let_it_be(:incoming) { create(:debian_incoming, project: distribution.project) }
- let(:package_file) { incoming.package_files.last }
- let(:service) { described_class.new(package_file) }
+ let(:source_file) { incoming.package_files.first }
+ let(:dsc_file) { incoming.package_files.second }
+ let(:changes_file) { incoming.package_files.last }
+ let(:service) { described_class.new(changes_file) }
subject { service.execute }
@@ -23,7 +25,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
end
context 'with invalid package file' do
- let(:package_file) { incoming.package_files.first }
+ let(:changes_file) { incoming.package_files.first }
it 'raise ArgumentError', :aggregate_failures do
expect { subject }.to raise_error(described_class::ExtractionError, "is not a changes file")
@@ -31,14 +33,14 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
end
context 'with invalid metadata' do
- let(:md5_dsc) { '3b0817804f669e16cdefac583ad88f0e 671 libs optional sample_1.2.3~alpha2.dsc' }
- let(:md5_source) { 'd79b34f58f61ff4ad696d9bd0b8daa68 864 libs optional sample_1.2.3~alpha2.tar.xz' }
+ let(:md5_dsc) { "#{dsc_file.file_md5} 671 libs optional sample_1.2.3~alpha2.dsc" }
+ let(:md5_source) { "#{source_file.file_md5} 864 libs optional sample_1.2.3~alpha2.tar.xz" }
let(:md5s) { "#{md5_dsc}\n#{md5_source}" }
- let(:sha1_dsc) { '32ecbd674f0bfd310df68484d87752490685a8d6 671 sample_1.2.3~alpha2.dsc' }
- let(:sha1_source) { '5f8bba5574eb01ac3b1f5e2988e8c29307788236 864 sample_1.2.3~alpha2.tar.xz' }
+ let(:sha1_dsc) { "#{dsc_file.file_sha1} 671 sample_1.2.3~alpha2.dsc" }
+ let(:sha1_source) { "#{source_file.file_sha1} 864 sample_1.2.3~alpha2.tar.xz" }
let(:sha1s) { "#{sha1_dsc}\n#{sha1_source}" }
- let(:sha256_dsc) { '844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba 671 sample_1.2.3~alpha2.dsc' }
- let(:sha256_source) { 'b5a599e88e7cbdda3bde808160a21ba1dd1ec76b2ec8d4912aae769648d68362 864 sample_1.2.3~alpha2.tar.xz' }
+ let(:sha256_dsc) { "#{dsc_file.file_sha256} 671 sample_1.2.3~alpha2.dsc" }
+ let(:sha256_source) { "#{source_file.file_sha256} 864 sample_1.2.3~alpha2.tar.xz" }
let(:sha256s) { "#{sha256_dsc}\n#{sha256_source}" }
let(:fields) { { 'Files' => md5s, 'Checksums-Sha1' => sha1s, 'Checksums-Sha256' => sha256s } }
let(:metadata) { { file_type: :changes, architecture: 'amd64', fields: fields } }
@@ -82,7 +84,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
end
context 'with different size in Checksums-Sha1' do
- let(:sha1_dsc) { '32ecbd674f0bfd310df68484d87752490685a8d6 42 sample_1.2.3~alpha2.dsc' }
+ let(:sha1_dsc) { "#{dsc_file.file_sha1} 42 sample_1.2.3~alpha2.dsc" }
it 'raise ArgumentError', :aggregate_failures do
expect { subject }.to raise_error(described_class::ExtractionError, "Size for sample_1.2.3~alpha2.dsc in Files and Checksums-Sha1 differ")
@@ -99,7 +101,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
end
context 'with different size in Checksums-Sha256' do
- let(:sha256_dsc) { '844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba 42 sample_1.2.3~alpha2.dsc' }
+ let(:sha256_dsc) { "#{dsc_file.file_sha256} 42 sample_1.2.3~alpha2.dsc" }
it 'raise ArgumentError', :aggregate_failures do
expect { subject }.to raise_error(described_class::ExtractionError, "Size for sample_1.2.3~alpha2.dsc in Files and Checksums-Sha256 differ")
@@ -126,7 +128,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
let(:md5_dsc) { '1234567890123456789012345678012 671 libs optional sample_1.2.3~alpha2.dsc' }
it 'raise ArgumentError', :aggregate_failures do
- expect { subject }.to raise_error(described_class::ExtractionError, "Validation failed: Md5sum mismatch for sample_1.2.3~alpha2.dsc: 3b0817804f669e16cdefac583ad88f0e != 1234567890123456789012345678012")
+ expect { subject }.to raise_error(described_class::ExtractionError, "Validation failed: Md5sum mismatch for sample_1.2.3~alpha2.dsc: #{dsc_file.file_md5} != 1234567890123456789012345678012")
end
end
@@ -134,7 +136,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
let(:sha1_dsc) { '1234567890123456789012345678901234567890 671 sample_1.2.3~alpha2.dsc' }
it 'raise ArgumentError', :aggregate_failures do
- expect { subject }.to raise_error(described_class::ExtractionError, "Validation failed: Sha1sum mismatch for sample_1.2.3~alpha2.dsc: 32ecbd674f0bfd310df68484d87752490685a8d6 != 1234567890123456789012345678901234567890")
+ expect { subject }.to raise_error(described_class::ExtractionError, "Validation failed: Sha1sum mismatch for sample_1.2.3~alpha2.dsc: #{dsc_file.file_sha1} != 1234567890123456789012345678901234567890")
end
end
@@ -142,7 +144,7 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do
let(:sha256_dsc) { '1234567890123456789012345678901234567890123456789012345678901234 671 sample_1.2.3~alpha2.dsc' }
it 'raise ArgumentError', :aggregate_failures do
- expect { subject }.to raise_error(described_class::ExtractionError, "Validation failed: Sha256sum mismatch for sample_1.2.3~alpha2.dsc: 844f79825b7e8aaa191e514b58a81f9ac1e58e2180134b0c9512fa66d896d7ba != 1234567890123456789012345678901234567890123456789012345678901234")
+ expect { subject }.to raise_error(described_class::ExtractionError, "Validation failed: Sha256sum mismatch for sample_1.2.3~alpha2.dsc: #{dsc_file.file_sha256} != 1234567890123456789012345678901234567890123456789012345678901234")
end
end
end
diff --git a/spec/services/packages/debian/generate_distribution_service_spec.rb b/spec/services/packages/debian/generate_distribution_service_spec.rb
index 0547d18c8bc..a162e492e7e 100644
--- a/spec/services/packages/debian/generate_distribution_service_spec.rb
+++ b/spec/services/packages/debian/generate_distribution_service_spec.rb
@@ -1,182 +1,25 @@
# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe Packages::Debian::GenerateDistributionService do
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:project) { create(:project, :public, group: group) }
- let_it_be(:project_distribution) { create("debian_project_distribution", container: project, codename: 'unstable', valid_time_duration_seconds: 48.hours.to_i) }
-
- let_it_be(:incoming) { create(:debian_incoming, project: project) }
-
- before_all do
- ::Packages::Debian::ProcessChangesService.new(incoming.package_files.last, nil).execute
- end
-
- let(:service) { described_class.new(distribution) }
-
describe '#execute' do
- subject { service.execute }
-
- shared_examples 'Generate Distribution' do |container_type|
- context "for #{container_type}" do
- if container_type == :group
- let_it_be(:container) { group }
- let_it_be(:distribution, reload: true) { create('debian_group_distribution', container: group, codename: 'unstable', valid_time_duration_seconds: 48.hours.to_i) }
- else
- let_it_be(:container) { project }
- let_it_be(:distribution, reload: true) { project_distribution }
- end
-
- context 'with components and architectures' do
- let_it_be(:component_main ) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') }
- let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') }
-
- let_it_be(:architecture_all ) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
- let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
- let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
-
- let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, created_at: '2020-01-24T09:00:00.000Z') } # destroyed
- let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, created_at: '2020-01-24T10:29:59.000Z') } # destroyed
- let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, created_at: '2020-01-24T10:30:00.000Z') } # kept
- let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, created_at: '2020-01-24T11:30:00.000Z') } # kept
-
- def check_component_file(component_name, component_file_type, architecture_name, expected_content)
- component_file = distribution
- .component_files
- .with_component_name(component_name)
- .with_file_type(component_file_type)
- .with_architecture_name(architecture_name)
- .last
+ subject { described_class.new(distribution).execute }
- expect(component_file).not_to be_nil
- expect(component_file.file.exists?).to eq(!expected_content.nil?)
+ include_context 'with published Debian package'
- unless expected_content.nil?
- component_file.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_content)
- end
- end
- end
-
- it 'updates distribution and component files', :aggregate_failures do
- travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- expect { subject }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and change { distribution.component_files.count }.from(4).to(2 + 6)
-
- expected_main_amd64_content = <<~EOF
- Package: libsample0
- Source: sample
- Version: 1.2.3~alpha2
- Installed-Size: 7
- Maintainer: John Doe <john.doe@example.com>
- Architecture: amd64
- Description: Some mostly empty lib
- Used in GitLab tests.
- .
- Testing another paragraph.
- Multi-Arch: same
- Homepage: https://gitlab.com/
- Section: libs
- Priority: optional
- Filename: pool/unstable/#{project.id}/s/sample/libsample0_1.2.3~alpha2_amd64.deb
- Size: 409600
- MD5sum: fb0842b21adc44207996296fe14439dd
- SHA256: 1c383a525bfcba619c7305ccd106d61db501a6bbaf0003bf8d0c429fbdb7fcc1
-
- Package: sample-dev
- Source: sample (1.2.3~alpha2)
- Version: 1.2.3~binary
- Installed-Size: 7
- Maintainer: John Doe <john.doe@example.com>
- Architecture: amd64
- Depends: libsample0 (= 1.2.3~binary)
- Description: Some mostly empty developpement files
- Used in GitLab tests.
- .
- Testing another paragraph.
- Multi-Arch: same
- Homepage: https://gitlab.com/
- Section: libdevel
- Priority: optional
- Filename: pool/unstable/#{project.id}/s/sample/sample-dev_1.2.3~binary_amd64.deb
- Size: 409600
- MD5sum: d2afbd28e4d74430d22f9504e18bfdf5
- SHA256: 9fbeee2191ce4dab5288fad5ecac1bd369f58fef9a992a880eadf0caf25f086d
- EOF
-
- check_component_file('main', :packages, 'all', nil)
- check_component_file('main', :packages, 'amd64', expected_main_amd64_content)
- check_component_file('main', :packages, 'arm64', nil)
-
- check_component_file('contrib', :packages, 'all', nil)
- check_component_file('contrib', :packages, 'amd64', nil)
- check_component_file('contrib', :packages, 'arm64', nil)
-
- size = expected_main_amd64_content.length
- md5sum = Digest::MD5.hexdigest(expected_main_amd64_content)
- sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
-
- expected_release_content = <<~EOF
- Codename: unstable
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
- Architectures: all amd64 arm64
- Components: contrib main
- MD5Sum:
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-all/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages
- #{md5sum} #{size} main/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages
- SHA256:
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-all/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
- #{sha256} #{size} main/binary-amd64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
- EOF
+ [:project, :group].each do |container_type|
+ context "for #{container_type}" do
+ include_context 'with Debian distribution', container_type
- distribution.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_release_content)
- end
- end
- end
+ context 'with Debian components and architectures' do
+ it_behaves_like 'Generate Debian Distribution and component files'
end
context 'without components and architectures' do
- it 'updates distribution and component files', :aggregate_failures do
- travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- expect { subject }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and not_change { distribution.component_files.count }
-
- expected_release_content = <<~EOF
- Codename: unstable
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
- MD5Sum:
- SHA256:
- EOF
-
- distribution.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_release_content)
- end
- end
- end
+ it_behaves_like 'Generate minimal Debian Distribution'
end
end
end
-
- it_behaves_like 'Generate Distribution', :project
- it_behaves_like 'Generate Distribution', :group
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 f43e38991ce..cad4e81f350 100644
--- a/spec/services/packages/debian/parse_debian822_service_spec.rb
+++ b/spec/services/packages/debian/parse_debian822_service_spec.rb
@@ -68,7 +68,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do
'Architecture' => 'any',
'Multi-Arch' => 'same',
'Depends' => 'libsample0 (= ${binary:Version}), ${misc:Depends}',
- 'Description' => "Some mostly empty developpement files\nUsed in GitLab tests.\n\nTesting another paragraph."
+ 'Description' => "Some mostly empty development files\nUsed in GitLab tests.\n\nTesting another paragraph."
},
'Package: libsample0' => {
'Package' => 'libsample0',
diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb
index f23471659bc..3069a2806b2 100644
--- a/spec/services/packages/debian/process_changes_service_spec.rb
+++ b/spec/services/packages/debian/process_changes_service_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe Packages::Debian::ProcessChangesService do
context 'with valid 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 change { Packages::Package.count }.from(1).to(2)
.and not_change { Packages::PackageFile.count }
@@ -30,6 +31,7 @@ RSpec.describe Packages::Debian::ProcessChangesService do
let(:package_file) { incoming.package_files.first }
it 'raise ExtractionError', :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 }
@@ -47,6 +49,7 @@ RSpec.describe Packages::Debian::ProcessChangesService do
end
it 'remove the package file', :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 }
diff --git a/spec/services/packages/debian/update_distribution_service_spec.rb b/spec/services/packages/debian/update_distribution_service_spec.rb
index 852fc713e34..2aa34a62111 100644
--- a/spec/services/packages/debian/update_distribution_service_spec.rb
+++ b/spec/services/packages/debian/update_distribution_service_spec.rb
@@ -6,6 +6,8 @@ RSpec.describe Packages::Debian::UpdateDistributionService 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?
+ expect(::Packages::Debian::GenerateDistributionWorker).to receive(:perform_async).with(distribution.class.container_type, distribution.id).and_call_original if expected_message.nil?
+ expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async) unless expected_message.nil?
if component_file_delta.zero?
expect { response }
diff --git a/spec/services/packages/helm/extract_file_metadata_service_spec.rb b/spec/services/packages/helm/extract_file_metadata_service_spec.rb
new file mode 100644
index 00000000000..ea196190e24
--- /dev/null
+++ b/spec/services/packages/helm/extract_file_metadata_service_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Helm::ExtractFileMetadataService do
+ let_it_be(:package_file) { create(:helm_package_file) }
+
+ let(:service) { described_class.new(package_file) }
+
+ let(:expected) do
+ {
+ 'apiVersion' => 'v2',
+ 'description' => 'File, Block, and Object Storage Services for your Cloud-Native Environment',
+ 'icon' => 'https://rook.io/images/rook-logo.svg',
+ 'name' => 'rook-ceph',
+ 'sources' => ['https://github.com/rook/rook'],
+ 'version' => 'v1.5.8'
+ }
+ end
+
+ subject { service.execute }
+
+ context 'with a valid file' do
+ it { is_expected.to eq(expected) }
+ end
+
+ context 'without Chart.yaml' do
+ before do
+ expect_next_instances_of(Gem::Package::TarReader::Entry, 14) do |entry|
+ expect(entry).to receive(:full_name).exactly(:once).and_wrap_original do |m, *args|
+ m.call(*args) + '_suffix'
+ end
+ end
+ end
+
+ it { expect { subject }.to raise_error(described_class::ExtractionError, 'Chart.yaml not found within a directory') }
+ end
+
+ context 'with Chart.yaml at root' do
+ before do
+ expect_next_instances_of(Gem::Package::TarReader::Entry, 14) do |entry|
+ expect(entry).to receive(:full_name).exactly(:once) do
+ 'Chart.yaml'
+ end
+ end
+ end
+
+ it { expect { subject }.to raise_error(described_class::ExtractionError, 'Chart.yaml not found within a directory') }
+ end
+
+ context 'with an invalid YAML' do
+ before do
+ expect_next_instance_of(Gem::Package::TarReader::Entry) do |entry|
+ expect(entry).to receive(:read).and_return('{')
+ end
+ end
+
+ it { expect { subject }.to raise_error(described_class::ExtractionError, 'Error while parsing Chart.yaml: (<unknown>): did not find expected node content while parsing a flow node at line 2 column 1') }
+ end
+end
diff --git a/spec/services/packages/nuget/metadata_extraction_service_spec.rb b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
index 39fc0f9e6a1..79428b58bd9 100644
--- a/spec/services/packages/nuget/metadata_extraction_service_spec.rb
+++ b/spec/services/packages/nuget/metadata_extraction_service_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Packages::Nuget::MetadataExtractionService do
- let(:package_file) { create(:nuget_package).package_files.first }
+ let_it_be(:package_file) { create(:nuget_package).package_files.first }
+
let(:service) { described_class.new(package_file.id) }
describe '#execute' do
@@ -28,7 +29,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do
context 'with nuspec file' do
before do
- allow(service).to receive(:nuspec_file).and_return(fixture_file(nuspec_filepath))
+ allow(service).to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath))
end
context 'with dependencies' do
@@ -57,7 +58,7 @@ RSpec.describe Packages::Nuget::MetadataExtractionService do
let_it_be(:nuspec_filepath) { 'packages/nuget/with_metadata.nuspec' }
before do
- allow(service).to receive(:nuspec_file).and_return(fixture_file(nuspec_filepath))
+ allow(service).to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath))
end
it { expect(subject[:license_url]).to eq('https://opensource.org/licenses/MIT') }
diff --git a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
index c1cce46a54c..ffe1a5b7646 100644
--- a/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
+++ b/spec/services/packages/nuget/update_package_from_metadata_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
let(:package_version) { '1.0.0' }
let(:package_file_name) { 'dummyproject.dummypackage.1.0.0.nupkg' }
- RSpec.shared_examples 'raising an' do |error_class|
+ shared_examples 'raising an' do |error_class|
it "raises an #{error_class}" do
expect { subject }.to raise_error(error_class)
end
@@ -21,11 +21,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
describe '#execute' do
subject { service.execute }
- before do
- stub_package_file_object_storage(enabled: true, direct_upload: true)
- end
-
- RSpec.shared_examples 'taking the lease' do
+ shared_examples 'taking the lease' do
before do
allow(service).to receive(:lease_release?).and_return(false)
end
@@ -39,7 +35,7 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
end
end
- RSpec.shared_examples 'not updating the package if the lease is taken' do
+ shared_examples 'not updating the package if the lease is taken' do
context 'without obtaining the exclusive lease' do
let(:lease_key) { "packages:nuget:update_package_from_metadata_service:package:#{package_id}" }
let(:metadata) { { package_name: package_name, package_version: package_version } }
@@ -117,9 +113,10 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
let(:expected_tags) { %w(foo bar test tag1 tag2 tag3 tag4 tag5) }
before do
- allow_any_instance_of(Packages::Nuget::MetadataExtractionService)
- .to receive(:nuspec_file)
- .and_return(fixture_file(nuspec_filepath))
+ allow_next_instance_of(Packages::Nuget::MetadataExtractionService) do |service|
+ allow(service)
+ .to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath))
+ end
end
it 'creates tags' do
@@ -172,9 +169,10 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
let(:package_file_name) { 'test.package.3.5.2.nupkg' }
before do
- allow_any_instance_of(Packages::Nuget::MetadataExtractionService)
- .to receive(:nuspec_file)
- .and_return(fixture_file(nuspec_filepath))
+ allow_next_instance_of(Packages::Nuget::MetadataExtractionService) do |service|
+ allow(service)
+ .to receive(:nuspec_file_content).and_return(fixture_file(nuspec_filepath))
+ end
end
it 'updates package and package file' do
@@ -195,7 +193,9 @@ RSpec.describe Packages::Nuget::UpdatePackageFromMetadataService, :clean_gitlab_
context 'with package file not containing a nuspec file' do
before do
- allow_any_instance_of(Zip::File).to receive(:glob).and_return([])
+ allow_next_instance_of(Zip::File) do |file|
+ allow(file).to receive(:glob).and_return([])
+ end
end
it_behaves_like 'raising an', ::Packages::Nuget::MetadataExtractionService::ExtractionError
diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb
index a79c89a1c35..295abe15bf0 100644
--- a/spec/services/pages/delete_service_spec.rb
+++ b/spec/services/pages/delete_service_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe Pages::DeleteService do
let_it_be(:admin) { create(:admin) }
let(:project) { create(:project, path: "my.project")}
- let!(:domain) { create(:pages_domain, project: project) }
let(:service) { described_class.new(project, admin)}
before do
@@ -14,8 +13,6 @@ RSpec.describe Pages::DeleteService do
end
it 'deletes published pages', :sidekiq_inline do
- expect(project.pages_deployed?).to be(true)
-
expect_next_instance_of(Gitlab::PagesTransfer) do |pages_transfer|
expect(pages_transfer).to receive(:rename_project).and_return true
end
@@ -23,11 +20,9 @@ RSpec.describe Pages::DeleteService do
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
service.execute
-
- expect(project.pages_deployed?).to be(false)
end
- it "doesn't remove anything from the legacy storage", :sidekiq_inline do
+ it "doesn't remove anything from the legacy storage if local_store is disabled", :sidekiq_inline do
allow(Settings.pages.local_store).to receive(:enabled).and_return(false)
expect(project.pages_deployed?).to be(true)
@@ -38,12 +33,20 @@ RSpec.describe Pages::DeleteService do
expect(project.pages_deployed?).to be(false)
end
- it 'deletes all domains', :sidekiq_inline do
- expect(project.pages_domains.count).to eq(1)
+ it 'marks pages as not deployed' do
+ expect do
+ service.execute
+ end.to change { project.reload.pages_deployed? }.from(true).to(false)
+ end
+
+ it 'deletes all domains' do
+ domain = create(:pages_domain, project: project)
+ unrelated_domain = create(:pages_domain)
service.execute
- expect(project.reload.pages_domains.count).to eq(0)
+ expect(PagesDomain.find_by_id(domain.id)).to eq(nil)
+ expect(PagesDomain.find_by_id(unrelated_domain.id)).to be
end
it 'schedules a destruction of pages deployments' do
@@ -61,20 +64,4 @@ RSpec.describe Pages::DeleteService do
service.execute
end.to change { PagesDeployment.count }.by(-1)
end
-
- it 'marks pages as not deployed, deletes domains and schedules worker to remove pages from disk' do
- expect(project.pages_deployed?).to eq(true)
- expect(project.pages_domains.count).to eq(1)
-
- service.execute
-
- expect(project.pages_deployed?).to eq(false)
- expect(project.pages_domains.count).to eq(0)
-
- expect_next_instance_of(Gitlab::PagesTransfer) do |pages_transfer|
- expect(pages_transfer).to receive(:rename_project).and_return true
- end
-
- Sidekiq::Worker.drain_all
- end
end
diff --git a/spec/services/pod_logs/elasticsearch_service_spec.rb b/spec/services/pod_logs/elasticsearch_service_spec.rb
index e233abcd96a..598b162aee4 100644
--- a/spec/services/pod_logs/elasticsearch_service_spec.rb
+++ b/spec/services/pod_logs/elasticsearch_service_spec.rb
@@ -34,11 +34,11 @@ RSpec.describe ::PodLogs::ElasticsearchService do
describe '#get_raw_pods' do
before do
- create(:clusters_applications_elastic_stack, :installed, cluster: cluster)
+ create(:clusters_integrations_elastic_stack, cluster: cluster)
end
it 'returns success with elasticsearch response' do
- allow_any_instance_of(::Clusters::Applications::ElasticStack)
+ allow_any_instance_of(::Clusters::Integrations::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs::Pods)
@@ -53,7 +53,7 @@ RSpec.describe ::PodLogs::ElasticsearchService do
end
it 'returns an error when ES is unreachable' do
- allow_any_instance_of(::Clusters::Applications::ElasticStack)
+ allow_any_instance_of(::Clusters::Integrations::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(nil)
@@ -64,7 +64,7 @@ RSpec.describe ::PodLogs::ElasticsearchService do
end
it 'handles server errors from elasticsearch' do
- allow_any_instance_of(::Clusters::Applications::ElasticStack)
+ allow_any_instance_of(::Clusters::Integrations::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs::Pods)
@@ -247,11 +247,11 @@ RSpec.describe ::PodLogs::ElasticsearchService do
let(:expected_cursor) { '9999934,1572449784442' }
before do
- create(:clusters_applications_elastic_stack, :installed, cluster: cluster)
+ create(:clusters_integrations_elastic_stack, cluster: cluster)
end
it 'returns the logs' do
- allow_any_instance_of(::Clusters::Applications::ElasticStack)
+ allow_any_instance_of(::Clusters::Integrations::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs::Lines)
@@ -267,7 +267,7 @@ RSpec.describe ::PodLogs::ElasticsearchService do
end
it 'returns an error when ES is unreachable' do
- allow_any_instance_of(::Clusters::Applications::ElasticStack)
+ allow_any_instance_of(::Clusters::Integrations::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(nil)
@@ -278,7 +278,7 @@ RSpec.describe ::PodLogs::ElasticsearchService do
end
it 'handles server errors from elasticsearch' do
- allow_any_instance_of(::Clusters::Applications::ElasticStack)
+ allow_any_instance_of(::Clusters::Integrations::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs::Lines)
@@ -292,7 +292,7 @@ RSpec.describe ::PodLogs::ElasticsearchService do
end
it 'handles cursor errors from elasticsearch' do
- allow_any_instance_of(::Clusters::Applications::ElasticStack)
+ allow_any_instance_of(::Clusters::Integrations::ElasticStack)
.to receive(:elasticsearch_client)
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs::Lines)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index cd659bf5e60..ac0b6cc8ef1 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -82,6 +82,34 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
+ describe 'topics' do
+ subject(:project) { create_project(user, opts) }
+
+ context "with 'topics' parameter" do
+ let(:opts) { { topics: 'topics' } }
+
+ it 'keeps them as specified' do
+ expect(project.topic_list).to eq(%w[topics])
+ end
+ end
+
+ context "with 'topic_list' parameter" do
+ let(:opts) { { topic_list: 'topic_list' } }
+
+ it 'keeps them as specified' do
+ expect(project.topic_list).to eq(%w[topic_list])
+ end
+ end
+
+ context "with 'tag_list' parameter (deprecated)" do
+ let(:opts) { { tag_list: 'tag_list' } }
+
+ it 'keeps them as specified' do
+ expect(project.topic_list).to eq(%w[tag_list])
+ end
+ end
+ end
+
context 'user namespace' do
it do
project = create_project(user, opts)
@@ -270,7 +298,7 @@ RSpec.describe Projects::CreateService, '#execute' do
context 'error handling' do
it 'handles invalid options' do
- opts[:default_branch] = 'master'
+ opts[:invalid] = 'option'
expect(create_project(user, opts)).to eq(nil)
end
end
@@ -663,7 +691,7 @@ RSpec.describe Projects::CreateService, '#execute' do
stub_feature_flags(projects_post_creation_worker: false)
end
- context 'Prometheus application is shared via group cluster' do
+ context 'Prometheus integration is shared via group cluster' do
let(:cluster) { create(:cluster, :group, groups: [group]) }
let(:group) do
create(:group).tap do |group|
@@ -672,7 +700,7 @@ RSpec.describe Projects::CreateService, '#execute' do
end
before do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
end
it 'creates PrometheusService record', :aggregate_failures do
@@ -685,11 +713,11 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
- context 'Prometheus application is shared via instance cluster' do
+ context 'Prometheus integration is shared via instance cluster' do
let(:cluster) { create(:cluster, :instance) }
before do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
end
it 'creates PrometheusService record', :aggregate_failures do
@@ -712,7 +740,7 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
- context 'shared Prometheus application is not available' do
+ context 'shared Prometheus integration is not available' do
it 'does not persist PrometheusService record', :aggregate_failures do
project = create_project(user, opts)
@@ -778,7 +806,7 @@ RSpec.describe Projects::CreateService, '#execute' do
end
end
- context 'with specialized_project_authorization_workers' do
+ context 'with specialized project_authorization workers' do
let_it_be(:other_user) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -809,7 +837,7 @@ RSpec.describe Projects::CreateService, '#execute' do
expect(AuthorizedProjectUpdate::ProjectCreateWorker).to(
receive(:perform_async).and_call_original
)
- expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
array_including([user.id], [other_user.id]),
@@ -819,34 +847,6 @@ RSpec.describe Projects::CreateService, '#execute' do
create_project(user, opts)
end
-
- context 'when feature is disabled' do
- before do
- stub_feature_flags(specialized_project_authorization_workers: false)
- end
-
- it 'updates authorization for current_user' do
- project = create_project(user, opts)
-
- expect(
- Ability.allowed?(user, :read_project, project)
- ).to be_truthy
- end
-
- it 'uses AuthorizedProjectsWorker' do
- expect(AuthorizedProjectsWorker).to(
- receive(:bulk_perform_async).with(array_including([user.id], [other_user.id])).and_call_original
- )
- expect(AuthorizedProjectUpdate::ProjectCreateWorker).not_to(
- receive(:perform_async)
- )
- expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).not_to(
- receive(:bulk_perform_in)
- )
-
- create_project(user, opts)
- end
- end
end
def create_project(user, opts)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index ff582279d71..c6b2b1e2b21 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -447,23 +447,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
it_behaves_like 'handles errors thrown during async destroy', "Failed to remove webhooks"
end
-
- context 'when "destroy_webhooks_before_the_project" flag is disabled' do
- before do
- stub_feature_flags(destroy_webhooks_before_the_project: false)
- end
-
- it 'does not call WebHooks::DestroyService' do
- expect(WebHooks::DestroyService).not_to receive(:new)
-
- expect do
- destroy_project(project, user)
- end.to change(WebHook, :count).by(-2)
- .and change(WebHookLog, :count).by(-1)
-
- expect(another_project_web_hook.reload).to be
- end
- end
end
context 'error while destroying', :sidekiq_inline do
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index c249a51fc56..9bc780fe177 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute' do
expect { subject.execute(create(:group)) }.not_to change { project.project_group_links.count }
end
- context 'with specialized_project_authorization_workers' do
+ context 'with specialized project_authorization workers' do
let_it_be(:other_user) { create(:user) }
before do
@@ -54,7 +54,7 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute' do
.with(project.id, group.id, group_access)
.and_call_original
)
- expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
array_including([user.id], [other_user.id]),
@@ -64,25 +64,5 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute' do
subject.execute(group)
end
-
- context 'when feature is disabled' do
- before do
- stub_feature_flags(specialized_project_authorization_project_share_worker: false)
- end
-
- it 'uses AuthorizedProjectsWorker' do
- expect(AuthorizedProjectsWorker).to(
- receive(:bulk_perform_async).with(array_including([user.id], [other_user.id])).and_call_original
- )
- expect(AuthorizedProjectUpdate::ProjectCreateWorker).not_to(
- receive(:perform_async)
- )
- expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).not_to(
- receive(:bulk_perform_in)
- )
-
- subject.execute(group)
- end
- end
end
end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 459b79b2d7d..d60e9a01e54 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -14,12 +14,60 @@ RSpec.describe Projects::GroupLinks::DestroyService, '#execute' do
expect { subject.execute(group_link) }.to change { project.project_group_links.count }.from(1).to(0)
end
- it 'updates authorization' do
- group.add_maintainer(user)
+ context 'project authorizations refresh' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ context 'when the feature flag `use_specialized_worker_for_project_auth_recalculation` is enabled' do
+ before do
+ stub_feature_flags(use_specialized_worker_for_project_auth_recalculation: true)
+ end
+
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
+ expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ .to receive(:perform_async).with(group_link.project.id)
+
+ subject.execute(group_link)
+ end
+
+ it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
+ receive(:bulk_perform_in)
+ .with(1.hour,
+ [[user.id]],
+ batch_delay: 30.seconds, batch_size: 100)
+ )
+
+ subject.execute(group_link)
+ end
- expect { subject.execute(group_link) }.to(
- change { Ability.allowed?(user, :read_project, project) }
- .from(true).to(false))
+ it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
+ expect { subject.execute(group_link) }.to(
+ change { Ability.allowed?(user, :read_project, project) }
+ .from(true).to(false))
+ end
+ end
+
+ context 'when the feature flag `use_specialized_worker_for_project_auth_recalculation` is disabled' do
+ before do
+ stub_feature_flags(use_specialized_worker_for_project_auth_recalculation: false)
+ end
+
+ it 'calls UserProjectAccessChangedService to update project authorizations' do
+ expect_next_instance_of(UserProjectAccessChangedService, [user.id]) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject.execute(group_link)
+ end
+
+ it 'updates project authorizations of users who had access to the project via the group share' do
+ expect { subject.execute(group_link) }.to(
+ change { Ability.allowed?(user, :read_project, project) }
+ .from(true).to(false))
+ end
+ end
end
it 'returns false if group_link is blank' do
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index bfc8225b654..5235c64d451 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -45,9 +45,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
before do
- create(:clusters_applications_prometheus, :installed,
+ create(:clusters_integrations_prometheus,
cluster: prd_cluster, alert_manager_token: token)
- create(:clusters_applications_prometheus, :installed,
+ create(:clusters_integrations_prometheus,
cluster: stg_cluster, alert_manager_token: nil)
end
@@ -62,41 +62,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
end
- context 'with project specific cluster using prometheus application' do
- where(:cluster_enabled, :status, :configured_token, :token_input, :result) do
- true | :installed | token | token | :success
- true | :installed | nil | nil | :success
- true | :updated | token | token | :success
- true | :updating | token | token | :failure
- true | :installed | token | 'x' | :failure
- true | :installed | nil | token | :failure
- true | :installed | token | nil | :failure
- true | nil | token | token | :failure
- false | :installed | token | token | :failure
- end
-
- with_them do
- before do
- cluster.update!(enabled: cluster_enabled)
-
- if status
- create(:clusters_applications_prometheus, status,
- cluster: cluster,
- alert_manager_token: configured_token)
- end
- end
-
- case result = params[:result]
- when :success
- include_examples 'processes one firing and one resolved prometheus alerts'
- when :failure
- it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
- else
- raise "invalid result: #{result.inspect}"
- end
- end
- end
-
context 'with project specific cluster using prometheus integration' do
where(:cluster_enabled, :integration_enabled, :configured_token, :token_input, :result) do
true | true | token | token | :success
diff --git a/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
index 2dc4a56368b..76830396104 100644
--- a/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
+++ b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::ScheduleBulkRepositoryShardMovesService do
it_behaves_like 'moves repository shard in bulk' do
- let_it_be_with_reload(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
+ let_it_be_with_reload(:container) { create(:project, :repository) }
let(:move_service_klass) { Projects::RepositoryStorageMove }
let(:bulk_worker_klass) { ::Projects::ScheduleBulkRepositoryShardMovesWorker }
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 8498b752610..3171abfb36f 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -195,8 +195,6 @@ RSpec.describe Projects::TransferService do
end
it 'does not update storage location' do
- create(:project_repository, project: project)
-
attempt_project_transfer
expect(project.project_repository).to have_attributes(
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index b9e909e8615..e1b22da2e61 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -587,6 +587,31 @@ RSpec.describe Projects::UpdateService do
it_behaves_like 'the transfer was not scheduled'
end
end
+
+ describe 'when updating topics' do
+ let(:project) { create(:project, topic_list: 'topic1, topic2') }
+
+ it 'update using topics' do
+ result = update_project(project, user, { topics: 'topics' })
+
+ expect(result[:status]).to eq(:success)
+ expect(project.topic_list).to eq(%w[topics])
+ end
+
+ it 'update using topic_list' do
+ result = update_project(project, user, { topic_list: 'topic_list' })
+
+ expect(result[:status]).to eq(:success)
+ expect(project.topic_list).to eq(%w[topic_list])
+ end
+
+ it 'update using tag_list (deprecated)' do
+ result = update_project(project, user, { tag_list: 'tag_list' })
+
+ expect(result[:status]).to eq(:success)
+ expect(project.topic_list).to eq(%w[tag_list])
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index f3ad69bae13..4af76bc65ab 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1202,16 +1202,6 @@ RSpec.describe QuickActions::InterpretService do
end
it_behaves_like 'draft command' do
- let(:content) { '/wip' }
- let(:issuable) { merge_request }
- end
-
- it_behaves_like 'undraft command' do
- let(:content) { '/wip' }
- let(:issuable) { merge_request }
- end
-
- it_behaves_like 'draft command' do
let(:content) { '/draft' }
let(:issuable) { merge_request }
end
diff --git a/spec/services/security/ci_configuration/sast_parser_service_spec.rb b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
index 4ebaddcfa4e..4fe99f20879 100644
--- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe Security::CiConfiguration::SastParserService do
let(:configuration) { described_class.new(project).configuration }
let(:secure_analyzers_prefix) { configuration['global'][0] }
let(:sast_excluded_paths) { configuration['global'][1] }
- let(:sast_analyzer_image_tag) { configuration['global'][2] }
let(:sast_pipeline_stage) { configuration['pipeline'][0] }
let(:sast_search_max_depth) { configuration['pipeline'][1] }
let(:bandit) { configuration['analyzers'][0] }
@@ -19,7 +18,6 @@ RSpec.describe Security::CiConfiguration::SastParserService do
it 'parses the configuration for SAST' do
expect(secure_analyzers_prefix['default_value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(sast_excluded_paths['default_value']).to eql('spec, test, tests, tmp')
- expect(sast_analyzer_image_tag['default_value']).to eql('2')
expect(sast_pipeline_stage['default_value']).to eql('test')
expect(sast_search_max_depth['default_value']).to eql('4')
expect(brakeman['enabled']).to be(true)
@@ -32,7 +30,6 @@ RSpec.describe Security::CiConfiguration::SastParserService do
allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_content)
expect(secure_analyzers_prefix['value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers2')
expect(sast_excluded_paths['value']).to eql('spec, executables')
- expect(sast_analyzer_image_tag['value']).to eql('2')
expect(sast_pipeline_stage['value']).to eql('our_custom_security_stage')
expect(sast_search_max_depth['value']).to eql('8')
expect(brakeman['enabled']).to be(false)
@@ -40,15 +37,6 @@ RSpec.describe Security::CiConfiguration::SastParserService do
expect(sast_brakeman_level['value']).to eql('2')
end
- context 'SAST_DEFAULT_ANALYZERS is set' do
- it 'enables analyzers correctly' do
- allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_default_analyzers_content)
-
- expect(brakeman['enabled']).to be(false)
- expect(bandit['enabled']).to be(true)
- end
- end
-
context 'SAST_EXCLUDED_ANALYZERS is set' do
it 'enables analyzers correctly' do
allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_excluded_analyzers_content)
@@ -64,7 +52,23 @@ RSpec.describe Security::CiConfiguration::SastParserService do
allow(project.repository).to receive(:blob_data_at).and_return(nil)
expect(secure_analyzers_prefix['value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(sast_excluded_paths['value']).to eql('spec, test, tests, tmp')
- expect(sast_analyzer_image_tag['value']).to eql('2')
+ expect(sast_pipeline_stage['value']).to eql('test')
+ expect(sast_search_max_depth['value']).to eql('4')
+ expect(brakeman['enabled']).to be(true)
+ expect(sast_brakeman_level['value']).to eql('1')
+ end
+ end
+
+ context 'when .gitlab-ci.yml does not include the sast job' do
+ before do
+ allow(project.repository).to receive(:blob_data_at).and_return(
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ )
+ end
+
+ it 'populates the current values with the default values' do
+ expect(secure_analyzers_prefix['value']).to eql('registry.gitlab.com/gitlab-org/security-products/analyzers')
+ expect(sast_excluded_paths['value']).to eql('spec, test, tests, tmp')
expect(sast_pipeline_stage['value']).to eql('test')
expect(sast_search_max_depth['value']).to eql('4')
expect(brakeman['enabled']).to be(true)
diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb
index 32a09e1afc8..eb6e85eb408 100644
--- a/spec/services/snippets/create_service_spec.rb
+++ b/spec/services/snippets/create_service_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Snippets::CreateService do
let(:extra_opts) { {} }
let(:creator) { admin }
- subject { described_class.new(project, creator, opts).execute }
+ subject { described_class.new(project: project, current_user: creator, params: opts).execute }
let(:snippet) { subject.payload[:snippet] }
diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb
index e737c00ae67..46bc62e11ef 100644
--- a/spec/services/snippets/update_service_spec.rb
+++ b/spec/services/snippets/update_service_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe Snippets::UpdateService do
let(:extra_opts) { {} }
let(:options) { base_opts.merge(extra_opts) }
let(:updater) { user }
- let(:service) { Snippets::UpdateService.new(project, updater, options) }
+ let(:service) { Snippets::UpdateService.new(project: project, current_user: updater, params: options) }
subject { service.execute(snippet) }
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index ae18bc23c17..0eb327ea7f1 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -735,7 +735,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
end
it 'is true with issue tracker not supporting referencing' do
- create(:bugzilla_service, project: project)
+ create(:bugzilla_integration, project: project)
project.reload
expect(service.cross_reference_disallowed?(noteable)).to be_truthy
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index 070782992e7..4723619afd2 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe UserProjectAccessChangedService do
end
it 'permits low-priority operation' do
- expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
+ expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in).with(
described_class::DELAY,
[[1], [2]],
@@ -31,4 +31,37 @@ RSpec.describe UserProjectAccessChangedService do
priority: described_class::LOW_PRIORITY)
end
end
+
+ context 'with load balancing enabled' do
+ let(:service) { UserProjectAccessChangedService.new([1, 2]) }
+
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ .with([[1], [2]])
+ .and_return(10)
+ end
+
+ it 'sticks all the updated users and returns the original result', :aggregate_failures do
+ expect(Gitlab::Database::LoadBalancing::Sticking).to receive(:bulk_stick).with(:user, [1, 2])
+
+ expect(service.execute).to eq(10)
+ end
+
+ it 'avoids N+1 cached queries', :use_sql_query_cache, :request_store do
+ # Run this once to establish a baseline
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ service.execute
+ end
+
+ service = UserProjectAccessChangedService.new([1, 2, 3, 4, 5])
+
+ allow(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait)
+ .with([[1], [2], [3], [4], [5]])
+ .and_return(10)
+
+ expect { service.execute }.not_to exceed_all_query_limit(control_count.count)
+ end
+ end
end
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index 4bbf6a2bcb8..cfafa9eff45 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -84,4 +84,51 @@ RSpec.describe Users::ActivityService do
end
end
end
+
+ context 'with DB Load Balancing', :request_store, :redis, :clean_gitlab_redis_shared_state do
+ include_context 'clear DB Load Balancing configuration'
+
+ let(:user) { create(:user, last_activity_on: last_activity_on) }
+
+ context 'when last activity is in the past' do
+ let(:user) { create(:user, last_activity_on: Date.today - 1.week) }
+
+ context 'database load balancing is configured' do
+ before do
+ # Do not pollute AR for other tests, but rather simulate effect of configure_proxy.
+ allow(ActiveRecord::Base.singleton_class).to receive(:prepend)
+ ::Gitlab::Database::LoadBalancing.configure_proxy
+ allow(ActiveRecord::Base).to receive(:connection).and_return(::Gitlab::Database::LoadBalancing.proxy)
+ end
+
+ let(:service) do
+ service = described_class.new(user)
+
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+
+ service
+ end
+
+ it 'does not stick to primary' do
+ expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_performed_write
+
+ service.execute
+
+ expect(user.last_activity_on).to eq(Date.today)
+ expect(::Gitlab::Database::LoadBalancing::Session.current).to be_performed_write
+ expect(::Gitlab::Database::LoadBalancing::Session.current).not_to be_using_primary
+ end
+ end
+
+ context 'database load balancing is not configured' do
+ let(:service) { described_class.new(user) }
+
+ it 'updates user without error' do
+ service.execute
+
+ expect(user.last_activity_on).to eq(Date.today)
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/users/authorized_build_service_spec.rb b/spec/services/users/authorized_build_service_spec.rb
new file mode 100644
index 00000000000..57a122cbf35
--- /dev/null
+++ b/spec/services/users/authorized_build_service_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::AuthorizedBuildService do
+ describe '#execute' do
+ let_it_be(:current_user) { create(:user) }
+
+ let(:params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
+
+ subject(:user) { described_class.new(current_user, params).execute }
+
+ it_behaves_like 'common user build items'
+ it_behaves_like 'current user not admin build items'
+ end
+end
diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb
index e8786c677d1..98fe6d9b5ba 100644
--- a/spec/services/users/build_service_spec.rb
+++ b/spec/services/users/build_service_spec.rb
@@ -11,148 +11,19 @@ RSpec.describe Users::BuildService do
let(:params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
let(:service) { described_class.new(current_user, params) }
- shared_examples_for 'common build items' do
- it { is_expected.to be_valid }
-
- it 'sets the created_by_id' do
- expect(user.created_by_id).to eq(current_user&.id)
- end
-
- it 'calls UpdateCanonicalEmailService' do
- expect(Users::UpdateCanonicalEmailService).to receive(:new).and_call_original
-
- user
- end
-
- context 'when user_type is provided' do
- context 'when project_bot' do
- before do
- params.merge!({ user_type: :project_bot })
- end
-
- it { expect(user.project_bot?).to be true }
- end
-
- context 'when not a project_bot' do
- before do
- params.merge!({ user_type: :alert_bot })
- end
-
- it { expect(user).to be_human }
- end
- end
- end
-
- shared_examples_for 'current user not admin' do
- context 'with "user_default_external" application setting' do
- where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do
- true | nil | 'fl@example.com' | nil | true
- true | true | 'fl@example.com' | nil | true
- true | false | 'fl@example.com' | nil | true # admin difference
-
- true | nil | 'fl@example.com' | '' | true
- true | true | 'fl@example.com' | '' | true
- true | false | 'fl@example.com' | '' | true # admin difference
-
- true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
- true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference
- true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
-
- true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true
- true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true
- true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference
-
- false | nil | 'fl@example.com' | nil | false
- false | true | 'fl@example.com' | nil | false # admin difference
- false | false | 'fl@example.com' | nil | false
-
- false | nil | 'fl@example.com' | '' | false
- false | true | 'fl@example.com' | '' | false # admin difference
- false | false | 'fl@example.com' | '' | false
-
- false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
- false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference
- false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
-
- false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false
- false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference
- false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false
- end
-
- with_them do
- before do
- stub_application_setting(user_default_external: user_default_external)
- stub_application_setting(user_default_internal_regex: user_default_internal_regex)
-
- params.merge!({ external: external, email: email }.compact)
- end
-
- it 'sets the value of Gitlab::CurrentSettings.user_default_external' do
- expect(user.external).to eq(result)
- end
- end
- end
-
- context 'when "send_user_confirmation_email" application setting is true' do
- before do
- stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true)
- end
-
- it 'does not confirm the user' do
- expect(user).not_to be_confirmed
- end
- end
-
- context 'when "send_user_confirmation_email" application setting is false' do
- before do
- stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true)
- end
-
- it 'confirms the user' do
- expect(user).to be_confirmed
- end
- end
-
- context 'with allowed params' do
- let(:params) do
- {
- email: 1,
- name: 1,
- password: 1,
- password_automatically_set: 1,
- username: 1,
- user_type: 'project_bot'
- }
- end
-
- it 'sets all allowed attributes' do
- expect(User).to receive(:new).with(hash_including(params)).and_call_original
-
- user
- end
- end
- end
-
context 'with nil current_user' do
subject(:user) { service.execute }
- it_behaves_like 'common build items'
- it_behaves_like 'current user not admin'
+ it_behaves_like 'common user build items'
+ it_behaves_like 'current user not admin build items'
end
context 'with non admin current_user' do
let_it_be(:current_user) { create(:user) }
- let(:service) { described_class.new(current_user, params) }
-
- subject(:user) { service.execute(skip_authorization: true) }
-
- it 'raises AccessDeniedError exception when authorization is not skipped' do
- expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError
+ it 'raises AccessDeniedError exception' do
+ expect { described_class.new(current_user, params).execute }.to raise_error Gitlab::Access::AccessDeniedError
end
-
- it_behaves_like 'common build items'
- it_behaves_like 'current user not admin'
end
context 'with an admin current_user' do
@@ -163,7 +34,7 @@ RSpec.describe Users::BuildService do
subject(:user) { service.execute }
- it_behaves_like 'common build items'
+ it_behaves_like 'common user build items'
context 'with allowed params' do
let(:params) do
diff --git a/spec/services/users/update_assigned_open_issue_count_service_spec.rb b/spec/services/users/update_assigned_open_issue_count_service_spec.rb
deleted file mode 100644
index 55fc60a7893..00000000000
--- a/spec/services/users/update_assigned_open_issue_count_service_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Users::UpdateAssignedOpenIssueCountService do
- let_it_be(:user) { create(:user) }
-
- describe '#initialize' do
- context 'incorrect arguments provided' do
- it 'raises an error if there are no target user' do
- expect { described_class.new(target_user: nil) }.to raise_error(ArgumentError, /Please provide a target user/)
- expect { described_class.new(target_user: "nonsense") }.to raise_error(ArgumentError, /Please provide a target user/)
- end
- end
-
- context 'when correct arguments provided' do
- it 'is successful' do
- expect { described_class.new(target_user: user) }.not_to raise_error
- end
- end
- end
-
- describe "#execute", :clean_gitlab_redis_cache do
- let(:fake_update_service) { double }
- let(:fake_issue_count_service) { double }
- let(:provided_value) { nil }
-
- subject { described_class.new(target_user: user).execute }
-
- context 'successful' do
- it 'returns a success response' do
- expect(subject).to be_success
- end
-
- it 'writes the cache with the new value' do
- expect(Rails.cache).to receive(:write).with(['users', user.id, 'assigned_open_issues_count'], 0, expires_in: User::COUNT_CACHE_VALIDITY_PERIOD)
-
- subject
- end
-
- it 'calls the issues finder to get the latest value' do
- expect(IssuesFinder).to receive(:new).with(user, assignee_id: user.id, state: 'opened', non_archived: true).and_return(fake_issue_count_service)
- expect(fake_issue_count_service).to receive(:execute)
-
- subject
- end
- end
- end
-end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index b3fd4e33640..5f53d6f34d8 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -128,11 +128,10 @@ RSpec.describe WebHookService do
end
it 'handles exceptions' do
- exceptions = [
- SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED,
- Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout,
- Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep
+ exceptions = Gitlab::HTTP::HTTP_ERRORS + [
+ Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError
]
+
exceptions.each do |exception_class|
exception = exception_class.new('Exception message')
project_hook.enable!
@@ -175,13 +174,19 @@ RSpec.describe WebHookService do
context 'execution logging' do
let(:hook_log) { project_hook.web_hook_logs.last }
+ def run_service
+ service_instance.execute
+ ::WebHooks::LogExecutionWorker.drain
+ project_hook.reload
+ end
+
context 'with success' do
before do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
end
it 'log successful execution' do
- service_instance.execute
+ run_service
expect(hook_log.trigger).to eq('push_hooks')
expect(hook_log.url).to eq(project_hook.url)
@@ -192,12 +197,16 @@ RSpec.describe WebHookService do
expect(hook_log.internal_error_message).to be_nil
end
+ it 'does not log in the service itself' do
+ expect { service_instance.execute }.not_to change(::WebHookLog, :count)
+ end
+
it 'does not increment the failure count' do
- expect { service_instance.execute }.not_to change(project_hook, :recent_failures)
+ expect { run_service }.not_to change(project_hook, :recent_failures)
end
it 'does not change the disabled_until attribute' do
- expect { service_instance.execute }.not_to change(project_hook, :disabled_until)
+ expect { run_service }.not_to change(project_hook, :disabled_until)
end
context 'when the hook had previously failed' do
@@ -206,7 +215,7 @@ RSpec.describe WebHookService do
end
it 'resets the failure count' do
- expect { service_instance.execute }.to change(project_hook, :recent_failures).to(0)
+ expect { run_service }.to change(project_hook, :recent_failures).to(0)
end
end
end
@@ -217,7 +226,7 @@ RSpec.describe WebHookService do
end
it 'logs failed execution' do
- service_instance.execute
+ run_service
expect(hook_log).to have_attributes(
trigger: eq('push_hooks'),
@@ -231,17 +240,17 @@ RSpec.describe WebHookService do
end
it 'increments the failure count' do
- expect { service_instance.execute }.to change(project_hook, :recent_failures).by(1)
+ expect { run_service }.to change(project_hook, :recent_failures).by(1)
end
it 'does not change the disabled_until attribute' do
- expect { service_instance.execute }.not_to change(project_hook, :disabled_until)
+ expect { run_service }.not_to change(project_hook, :disabled_until)
end
it 'does not allow the failure count to overflow' do
project_hook.update!(recent_failures: 32767)
- expect { service_instance.execute }.not_to change(project_hook, :recent_failures)
+ expect { run_service }.not_to change(project_hook, :recent_failures)
end
context 'when the web_hooks_disable_failed FF is disabled' do
@@ -253,7 +262,7 @@ RSpec.describe WebHookService do
it 'does not allow the failure count to overflow' do
project_hook.update!(recent_failures: 32767)
- expect { service_instance.execute }.not_to change(project_hook, :recent_failures)
+ expect { run_service }.not_to change(project_hook, :recent_failures)
end
end
end
@@ -264,7 +273,7 @@ RSpec.describe WebHookService do
end
it 'log failed execution' do
- service_instance.execute
+ run_service
expect(hook_log.trigger).to eq('push_hooks')
expect(hook_log.url).to eq(project_hook.url)
@@ -276,16 +285,15 @@ RSpec.describe WebHookService do
end
it 'does not increment the failure count' do
- expect { service_instance.execute }.not_to change(project_hook, :recent_failures)
+ expect { run_service }.not_to change(project_hook, :recent_failures)
end
- it 'sets the disabled_until attribute' do
- expect { service_instance.execute }
- .to change(project_hook, :disabled_until).to(project_hook.next_backoff.from_now)
+ it 'backs off' do
+ expect { run_service }.to change(project_hook, :disabled_until)
end
it 'increases the backoff count' do
- expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1)
+ expect { run_service }.to change(project_hook, :backoff_count).by(1)
end
context 'when the previous cool-off was near the maximum' do
@@ -294,11 +302,7 @@ RSpec.describe WebHookService do
end
it 'sets the disabled_until attribute' do
- expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now)
- end
-
- it 'sets the last_backoff attribute' do
- expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1)
+ expect { run_service }.to change(project_hook, :disabled_until).to(1.day.from_now)
end
end
@@ -308,11 +312,7 @@ RSpec.describe WebHookService do
end
it 'sets the disabled_until attribute' do
- expect { service_instance.execute }.to change(project_hook, :disabled_until).to(1.day.from_now)
- end
-
- it 'sets the last_backoff attribute' do
- expect { service_instance.execute }.to change(project_hook, :backoff_count).by(1)
+ expect { run_service }.to change(project_hook, :disabled_until).to(1.day.from_now)
end
end
end
@@ -320,7 +320,7 @@ RSpec.describe WebHookService do
context 'with unsafe response body' do
before do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: "\xBB")
- service_instance.execute
+ run_service
end
it 'log successful execution' do
@@ -375,15 +375,18 @@ RSpec.describe WebHookService do
it 'does not queue a worker and logs an error' do
expect(WebHookWorker).not_to receive(:perform_async)
- payload = {
- message: 'Webhook rate limit exceeded',
- hook_id: project_hook.id,
- hook_type: 'ProjectHook',
- hook_name: 'push_hooks'
- }
-
- expect(Gitlab::AuthLogger).to receive(:error).with(payload)
- expect(Gitlab::AppLogger).to receive(:error).with(payload)
+ expect(Gitlab::AuthLogger).to receive(:error).with(
+ include(
+ message: 'Webhook rate limit exceeded',
+ hook_id: project_hook.id,
+ hook_type: 'ProjectHook',
+ hook_name: 'push_hooks',
+ "correlation_id" => kind_of(String),
+ "meta.project" => project.full_path,
+ "meta.related_class" => 'ProjectHook',
+ "meta.root_namespace" => project.root_namespace.full_path
+ )
+ )
service_instance.async_execute
end
@@ -403,7 +406,6 @@ RSpec.describe WebHookService do
it 'stops queueing workers and logs errors' do
expect(Gitlab::AuthLogger).to receive(:error).twice
- expect(Gitlab::AppLogger).to receive(:error).twice
2.times { service_instance.async_execute }
end
@@ -430,5 +432,19 @@ RSpec.describe WebHookService do
end
end
end
+
+ context 'when hook has custom context attributes' do
+ it 'includes the attributes in the worker context' do
+ expect(WebHookWorker).to receive(:perform_async) do
+ expect(Gitlab::ApplicationContext.current).to include(
+ 'meta.project' => project_hook.project.full_path,
+ 'meta.root_namespace' => project.root_ancestor.path,
+ 'meta.related_class' => 'ProjectHook'
+ )
+ end
+
+ service_instance.async_execute
+ end
+ end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index bd9ba53c04c..9a2eee0edc5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -403,6 +403,15 @@ RSpec.configure do |config|
end
end
+ # Allows stdout to be redirected to reduce noise
+ config.before(:each, :silence_stdout) do
+ $stdout = StringIO.new
+ end
+
+ config.after(:each, :silence_stdout) do
+ $stdout = STDOUT
+ end
+
config.disable_monkey_patching!
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index f9a28c8e40b..e48a7b322ac 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -21,7 +21,8 @@ JS_CONSOLE_FILTER = Regexp.union([
'"[WDS] Hot Module Replacement enabled."',
'"[WDS] Live Reloading enabled."',
'Download the Vue Devtools extension',
- 'Download the Apollo DevTools'
+ 'Download the Apollo DevTools',
+ "Unrecognized feature: 'interest-cohort'"
])
CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
index 60d82f7e92a..f6339d7343c 100644
--- a/spec/support/database_cleaner.rb
+++ b/spec/support/database_cleaner.rb
@@ -35,8 +35,6 @@ RSpec.configure do |config|
puts "Recreating the database"
start = Gitlab::Metrics::System.monotonic_time
- ActiveRecord::AdvisoryLockBase.clear_all_connections!
-
ActiveRecord::Tasks::DatabaseTasks.drop_current
ActiveRecord::Tasks::DatabaseTasks.create_current
ActiveRecord::Tasks::DatabaseTasks.load_schema_current
diff --git a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
index c9ff566e94c..de9735df546 100644
--- a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
+++ b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
@@ -1,13 +1,38 @@
# frozen_string_literal: true
-RSpec.shared_examples 'a correct instrumented metric value' do |options, expected_value|
- let(:time_frame) { options[:time_frame] }
+RSpec.shared_examples 'a correct instrumented metric value' do |params|
+ let(:time_frame) { params[:time_frame] }
+ let(:options) { params[:options] }
+ let(:metric) { described_class.new(time_frame: time_frame, options: options) }
before do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
end
it 'has correct value' do
- expect(described_class.new(time_frame: time_frame).value).to eq(expected_value)
+ expect(metric.value).to eq(expected_value)
end
end
+
+RSpec.shared_examples 'a correct instrumented metric query' do |params|
+ let(:time_frame) { params[:time_frame] }
+ let(:options) { params[:options] }
+ let(:metric) { described_class.new(time_frame: time_frame, options: options) }
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'has correct generate query' do
+ expect(metric.to_sql).to eq(expected_query)
+ end
+end
+
+RSpec.shared_examples 'a correct instrumented metric value and query' do |params|
+ it_behaves_like 'a correct instrumented metric value', params
+ it_behaves_like 'a correct instrumented metric query', params
+end
diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
index d20078c8904..0e021a85ba6 100644
--- a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
@@ -4,7 +4,6 @@ include:
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2"
SAST_EXCLUDED_PATHS: "spec, executables"
- SAST_DEFAULT_ANALYZERS: "bandit, brakeman"
SAST_EXCLUDED_ANALYZERS: "brakeman"
stages:
diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml
deleted file mode 100644
index c4f3c3aace2..00000000000
--- a/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-include:
- - template: SAST.gitlab-ci.yml
-
-variables:
- SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2"
- SAST_EXCLUDED_PATHS: "spec, executables"
- SAST_DEFAULT_ANALYZERS: "bandit, gosec"
-
-stages:
- - our_custom_security_stage
-sast:
- stage: our_custom_security_stage
- variables:
- SEARCH_MAX_DEPTH: 8
- SAST_BRAKEMAN_LEVEL: 2
diff --git a/spec/support/helpers/access_matchers_helpers.rb b/spec/support/helpers/access_matchers_helpers.rb
index 9100f245d36..035653172c1 100644
--- a/spec/support/helpers/access_matchers_helpers.rb
+++ b/spec/support/helpers/access_matchers_helpers.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module AccessMatchersHelpers
+ include Gitlab::Utils::StrongMemoize
+
USER_ACCESSOR_METHOD_NAME = 'user'
def provide_user(role, membership = nil)
@@ -61,11 +63,6 @@ module AccessMatchersHelpers
# (or defined by `method_name`) method generated by `let` definition in example group before it's used by `subject`.
# This override is per concrete example only because the example group class gets re-created for each example.
instance_eval(<<~CODE, __FILE__, __LINE__ + 1)
- if instance_variable_get(:@__#{USER_ACCESSOR_METHOD_NAME}_patched)
- raise ArgumentError, 'An access matcher be_allowed_for/be_denied_for can be used only once per example (`it` block)'
- end
- instance_variable_set(:@__#{USER_ACCESSOR_METHOD_NAME}_patched, true)
-
def #{USER_ACCESSOR_METHOD_NAME}
@#{USER_ACCESSOR_METHOD_NAME} ||= User.find(#{user.id})
end
@@ -81,6 +78,13 @@ module AccessMatchersHelpers
end
end
+ def reset_matcher_environment
+ instance_eval(<<~CODE, __FILE__, __LINE__ + 1)
+ clear_memoization(:#{USER_ACCESSOR_METHOD_NAME})
+ undef #{USER_ACCESSOR_METHOD_NAME} if defined? user
+ CODE
+ end
+
def run_matcher(action, role, membership, owned_objects)
raise_if_non_block_expectation!(action)
@@ -91,5 +95,7 @@ module AccessMatchersHelpers
else
action.call
end
+
+ reset_matcher_environment
end
end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index 5510284b30d..4515b96c79e 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -7,6 +7,11 @@ module CycleAnalyticsHelpers
page.find('[data-testid="dropdown-value-streams"]').click
end
+ def path_nav_stage_names_without_median
+ # Returns the path names with the median value stripped out
+ page.all('.gl-path-button').collect(&:text).map {|name_with_median| name_with_median.split("\n")[0] }
+ end
+
def add_custom_stage_to_form
page.find_button(s_('CreateValueStreamForm|Add another stage')).click
diff --git a/spec/support/helpers/feature_flag_helpers.rb b/spec/support/helpers/feature_flag_helpers.rb
index 93cd915879b..af7a674f3bc 100644
--- a/spec/support/helpers/feature_flag_helpers.rb
+++ b/spec/support/helpers/feature_flag_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module FeatureFlagHelpers
- def create_flag(project, name, active = true, description: nil, version: Operations::FeatureFlag.versions['legacy_flag'])
+ def create_flag(project, name, active = true, description: nil, version: Operations::FeatureFlag.versions['new_version_flag'])
create(:operations_feature_flag, name: name, active: active, version: version,
description: description, project: project)
end
@@ -90,6 +90,5 @@ module FeatureFlagHelpers
def expect_user_to_see_feature_flags_index_page
expect(page).to have_text('Feature Flags')
- expect(page).to have_text('Lists')
end
end
diff --git a/spec/support/helpers/features/top_nav_spec_helpers.rb b/spec/support/helpers/features/top_nav_spec_helpers.rb
new file mode 100644
index 00000000000..ab664ce4283
--- /dev/null
+++ b/spec/support/helpers/features/top_nav_spec_helpers.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.).
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module TopNavSpecHelpers
+ def open_top_nav
+ return unless Feature.enabled?(:combined_menu, default_enabled: :yaml)
+
+ find('.js-top-nav-dropdown-toggle').click
+ end
+
+ def within_top_nav
+ if Feature.enabled?(:combined_menu, default_enabled: :yaml)
+ within('.js-top-nav-dropdown-menu') do
+ yield
+ end
+ else
+ within('.navbar-sub-nav') do
+ yield
+ end
+ end
+ end
+
+ def open_top_nav_projects
+ if Feature.enabled?(:combined_menu, default_enabled: :yaml)
+ open_top_nav
+
+ within_top_nav do
+ click_button('Projects')
+ end
+ else
+ find('#nav-projects-dropdown').click
+ end
+ end
+
+ def open_top_nav_groups
+ return unless Feature.enabled?(:combined_menu, default_enabled: :yaml)
+
+ open_top_nav
+
+ within_top_nav do
+ click_button('Groups')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 2ce4bcfa943..5cfd03ecea8 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -15,7 +15,7 @@ module GitalySetup
default_name = ENV['CI'] ? 'DEBUG' : 'WARN'
level_name = ENV['GITLAB_TESTING_LOG_LEVEL']&.upcase
level = Logger.const_get(level_name || default_name, true) # rubocop: disable Gitlab/ConstGetInheritFalse
- Logger.new(STDOUT, level: level, formatter: ->(_, _, _, msg) { msg })
+ Logger.new($stdout, level: level, formatter: ->(_, _, _, msg) { msg })
end
def tmp_tests_gitaly_dir
@@ -153,7 +153,7 @@ module GitalySetup
end
LOGGER.debug "Checking gitaly-ruby bundle...\n"
- out = ENV['CI'] ? STDOUT : '/dev/null'
+ out = ENV['CI'] ? $stdout : '/dev/null'
abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: File.dirname(gemfile))
end
diff --git a/spec/support/helpers/global_id_deprecation_helpers.rb b/spec/support/helpers/global_id_deprecation_helpers.rb
new file mode 100644
index 00000000000..37ba1420fb3
--- /dev/null
+++ b/spec/support/helpers/global_id_deprecation_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module GlobalIDDeprecationHelpers
+ def stub_global_id_deprecations(*deprecations)
+ old_name_map = deprecations.index_by(&:old_model_name)
+ new_name_map = deprecations.index_by(&:new_model_name)
+ old_graphql_name_map = deprecations.index_by { |d| Types::GlobalIDType.model_name_to_graphql_name(d.old_model_name) }
+
+ stub_const('Gitlab::GlobalId::Deprecations::OLD_NAME_MAP', old_name_map)
+ stub_const('Gitlab::GlobalId::Deprecations::NEW_NAME_MAP', new_name_map)
+ stub_const('Gitlab::GlobalId::Deprecations::OLD_GRAPHQL_NAME_MAP', old_graphql_name_map)
+ end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 5dc6945ec5e..4857fa63114 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
module GraphqlHelpers
+ def self.included(base)
+ base.include(::Gitlab::Graphql::Laziness)
+ end
+
MutationDefinition = Struct.new(:query, :variables)
NoData = Class.new(StandardError)
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 28375c1d51e..8fd8a548011 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -66,6 +66,14 @@ module JavaScriptFixturesHelpers
File.write(full_fixture_path, fixture)
end
+ def parse_html(fixture)
+ if respond_to?(:use_full_html) && public_send(:use_full_html)
+ Nokogiri::HTML::Document.parse(fixture)
+ else
+ Nokogiri::HTML::DocumentFragment.parse(fixture)
+ end
+ end
+
# Private: Prepare a response object for use as a frontend fixture
#
# response - response object to prepare
@@ -76,7 +84,7 @@ module JavaScriptFixturesHelpers
response_mime_type = Mime::Type.lookup(response.media_type)
if response_mime_type.html?
- doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
+ doc = parse_html(fixture)
link_tags = doc.css('link')
link_tags.remove
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index fc3eb976276..cc88a3fc71e 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -77,7 +77,11 @@ module LoginHelpers
# Requires Javascript driver.
def gitlab_disable_admin_mode
- click_on 'Leave Admin Mode'
+ open_top_nav
+
+ within_top_nav do
+ click_on 'Leave Admin Mode'
+ end
end
private
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index 05afbc336da..d18a1d23584 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -2,15 +2,16 @@
module ActiveRecord
class QueryRecorder
- attr_reader :log, :skip_cached, :cached, :data
+ attr_reader :log, :skip_cached, :skip_schema_queries, :cached, :data
UNKNOWN = %w[unknown unknown].freeze
- def initialize(skip_cached: true, log_file: nil, query_recorder_debug: false, &block)
+ def initialize(skip_cached: true, skip_schema_queries: true, log_file: nil, query_recorder_debug: false, &block)
@data = Hash.new { |h, k| h[k] = { count: 0, occurrences: [], backtrace: [], durations: [] } }
@log = []
@cached = []
@skip_cached = skip_cached
+ @skip_schema_queries = skip_schema_queries
@query_recorder_debug = ENV['QUERY_RECORDER_DEBUG'] || query_recorder_debug
@log_file = log_file
record(&block) if block_given?
@@ -79,7 +80,7 @@ module ActiveRecord
if values[:cached] && skip_cached
@cached << values[:sql]
- elsif !values[:name]&.include?("SCHEMA")
+ elsif !skip_schema_queries || !values[:name]&.include?("SCHEMA")
backtrace = @query_recorder_debug ? show_backtrace(values, duration) : nil
@log << values[:sql]
store_sql_by_source(values: values, duration: duration, backtrace: backtrace)
diff --git a/spec/support/helpers/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb
index d8f354a69da..4c0fa9d1b0b 100644
--- a/spec/support/helpers/rake_helpers.rb
+++ b/spec/support/helpers/rake_helpers.rb
@@ -10,11 +10,6 @@ module RakeHelpers
allow(main_object).to receive(:warn_user_is_not_gitlab)
end
- def silence_output
- allow(main_object).to receive(:puts)
- allow(main_object).to receive(:print)
- end
-
def silence_progress_bar
allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double.as_null_object)
end
diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb
index e65cb8c96db..a6a7948d9d9 100644
--- a/spec/support/helpers/reference_parser_helpers.rb
+++ b/spec/support/helpers/reference_parser_helpers.rb
@@ -11,20 +11,20 @@ module ReferenceParserHelpers
end
RSpec.shared_examples 'no project N+1 queries' do
- it 'avoids N+1 queries in #nodes_visible_to_user', :request_store do
+ it 'avoids N+1 queries in #nodes_visible_to_user' do
context = Banzai::RenderContext.new(project, user)
- record_queries = lambda do |links|
- ActiveRecord::QueryRecorder.new do
- described_class.new(context).nodes_visible_to_user(user, links)
- end
+ request = lambda do |links|
+ described_class.new(context).nodes_visible_to_user(user, links)
end
- control = record_queries.call(control_links)
- actual = record_queries.call(actual_links)
+ control = ActiveRecord::QueryRecorder.new { request.call(control_links) }
- expect(actual.count).to be <= control.count
- expect(actual.cached_count).to be <= control.cached_count
+ create(:group_member, group: project.group) if project.group
+ create(:project_member, project: project)
+ create(:project_group_link, project: project)
+
+ expect { request.call(actual_links) }.not_to exceed_query_limit(control)
end
end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index c6176b5bcbc..b1a9aade043 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -99,7 +99,6 @@ module UsageDataHelpers
projects_with_repositories_enabled
projects_with_error_tracking_enabled
projects_with_enabled_alert_integrations
- projects_with_prometheus_alerts
projects_with_tracing_enabled
projects_with_expiration_policy_enabled
projects_with_expiration_policy_disabled
@@ -163,7 +162,6 @@ module UsageDataHelpers
database
prometheus_metrics_enabled
web_ide_clientside_preview_enabled
- ingress_modsecurity_enabled
object_store
topology
).freeze
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index a6b395ad4d5..5fb6af99b79 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -20,11 +20,11 @@ module ImportExport
def setup_reader(reader)
if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson, default_enabled: true)
- allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(false)
- allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(true)
+ allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:exist?).and_return(false)
+ allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(true)
else
- allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(true)
- allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(false)
+ allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:exist?).and_return(true)
+ allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(false)
end
end
diff --git a/spec/support/matchers/be_one_of.rb b/spec/support/matchers/be_one_of.rb
new file mode 100644
index 00000000000..16ee32b67c3
--- /dev/null
+++ b/spec/support/matchers/be_one_of.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :be_one_of do |collection|
+ match do |item|
+ expect(collection).to include(item)
+ end
+
+ failure_message do |item|
+ "expected #{item} to be one of #{collection}"
+ end
+end
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index 8539f202602..eeeb93fa811 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -30,4 +30,12 @@ RSpec.configure do |config|
redis_queues_cleanup!
end
+
+ config.around(:each, :clean_gitlab_redis_trace_chunks) do |example|
+ redis_trace_chunks_cleanup!
+
+ example.run
+
+ redis_trace_chunks_cleanup!
+ end
end
diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb
index 7c571738a01..b8118bf94cc 100644
--- a/spec/support/redis/redis_helpers.rb
+++ b/spec/support/redis/redis_helpers.rb
@@ -17,4 +17,9 @@ module RedisHelpers
def redis_shared_state_cleanup!
Gitlab::Redis::SharedState.with(&:flushall)
end
+
+ # Usage: CI trace chunks
+ def redis_trace_chunks_cleanup!
+ Gitlab::Redis::TraceChunks.with(&:flushall)
+ end
end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index f5f6a69738b..25eab5fd6e4 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -4,9 +4,22 @@ RSpec.shared_examples "redis_shared_examples" do
include StubENV
let(:test_redis_url) { "redis://redishost:#{redis_port}"}
+ let(:config_file_name) { instance_specific_config_file }
+ let(:config_old_format_socket) { "spec/fixtures/config/redis_old_format_socket.yml" }
+ let(:config_new_format_socket) { "spec/fixtures/config/redis_new_format_socket.yml" }
+ let(:old_socket_path) {"/path/to/old/redis.sock" }
+ let(:new_socket_path) {"/path/to/redis.sock" }
+ let(:config_old_format_host) { "spec/fixtures/config/redis_old_format_host.yml" }
+ let(:config_new_format_host) { "spec/fixtures/config/redis_new_format_host.yml" }
+ let(:redis_port) { 6379 }
+ let(:redis_database) { 99 }
+ let(:sentinel_port) { 26379 }
+ let(:config_with_environment_variable_inside) { "spec/fixtures/config/redis_config_with_env.yml"}
+ let(:config_env_variable_url) {"TEST_GITLAB_REDIS_URL"}
+ let(:rails_root) { Dir.mktmpdir('redis_shared_examples') }
before do
- stub_env(environment_config_file_name, Rails.root.join(config_file_name))
+ allow(described_class).to receive(:config_file_name).and_return(Rails.root.join(config_file_name).to_s)
clear_raw_config
end
@@ -14,8 +27,71 @@ RSpec.shared_examples "redis_shared_examples" do
clear_raw_config
end
+ describe '.config_file_name' do
+ subject { described_class.config_file_name }
+
+ before do
+ # 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)
+ FileUtils.mkdir_p(File.join(rails_root, 'config'))
+ end
+
+ after do
+ FileUtils.rm_rf(rails_root)
+ end
+
+ context 'when there is no config file anywhere' do
+ it { expect(subject).to be_nil }
+
+ context 'but resque.yml exists' do
+ before do
+ FileUtils.touch(File.join(rails_root, 'config', 'resque.yml'))
+ 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
+
+ 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 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 'returns a path that exists' do
+ expect(File.file?(subject)).to eq(true)
+ 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
+ end
+ end
+ end
+ end
+ end
+
describe '.params' do
- subject { described_class.params }
+ subject { described_class.new(rails_env).params }
+
+ let(:rails_env) { 'development' }
+ let(:config_file_name) { config_old_format_socket }
it 'withstands mutation' do
params1 = described_class.params
@@ -58,15 +134,27 @@ RSpec.shared_examples "redis_shared_examples" do
context 'with new format' do
let(:config_file_name) { config_new_format_host }
- it 'returns hash with host, port, db, and password' do
- is_expected.to include(host: 'localhost', password: 'mynewpassword', port: redis_port, db: redis_database)
- is_expected.not_to have_key(:url)
+ where(:rails_env, :host) do
+ [
+ %w[development development-host],
+ %w[test test-host],
+ %w[production production-host]
+ ]
+ end
+
+ with_them do
+ it 'returns hash with host, port, db, and password' do
+ is_expected.to include(host: host, password: 'mynewpassword', port: redis_port, db: redis_database)
+ is_expected.not_to have_key(:url)
+ end
end
end
end
end
describe '.url' do
+ let(:config_file_name) { config_old_format_socket }
+
it 'withstands mutation' do
url1 = described_class.url
url2 = described_class.url
@@ -88,6 +176,12 @@ RSpec.shared_examples "redis_shared_examples" do
end
end
+ describe '.version' do
+ it 'returns a version' do
+ expect(described_class.version).to be_present
+ end
+ end
+
describe '._raw_config' do
subject { described_class._raw_config }
@@ -109,6 +203,8 @@ RSpec.shared_examples "redis_shared_examples" do
end
describe '.with' do
+ let(:config_file_name) { config_old_format_socket }
+
before do
clear_pool
end
@@ -140,17 +236,46 @@ RSpec.shared_examples "redis_shared_examples" do
described_class.with { |_redis_shared_example| true }
end
end
+
+ context 'when there is no config at all' do
+ before do
+ # 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
+
+ after do
+ FileUtils.rm_rf(rails_root)
+ end
+
+ it 'can run an empty block' do
+ expect { described_class.with { nil } }.not_to raise_error
+ end
+ end
end
describe '#sentinels' do
- subject { described_class.new(Rails.env).sentinels }
+ subject { described_class.new(rails_env).sentinels }
+
+ let(:rails_env) { 'development' }
context 'when sentinels are defined' do
let(:config_file_name) { config_new_format_host }
- it 'returns an array of hashes with host and port keys' do
- is_expected.to include(host: 'localhost', port: sentinel_port)
- is_expected.to include(host: 'replica2', port: sentinel_port)
+ where(:rails_env, :hosts) do
+ [
+ ['development', %w[development-replica1 development-replica2]],
+ ['test', %w[test-replica1 test-replica2]],
+ ['production', %w[production-replica1 production-replica2]]
+ ]
+ end
+
+ with_them do
+ it 'returns an array of hashes with host and port keys' do
+ is_expected.to include(host: hosts[0], port: sentinel_port)
+ is_expected.to include(host: hosts[1], port: sentinel_port)
+ end
end
end
@@ -184,12 +309,6 @@ RSpec.shared_examples "redis_shared_examples" do
end
describe '#raw_config_hash' do
- it 'returns default redis url when no config file is present' do
- expect(subject).to receive(:fetch_config) { false }
-
- expect(subject.send(:raw_config_hash)).to eq(url: class_redis_url )
- end
-
it 'returns old-style single url config in a hash' do
expect(subject).to receive(:fetch_config) { test_redis_url }
expect(subject.send(:raw_config_hash)).to eq(url: test_redis_url)
diff --git a/spec/support/shared_contexts/changes_access_checks_shared_context.rb b/spec/support/shared_contexts/changes_access_checks_shared_context.rb
new file mode 100644
index 00000000000..ec3727b6d6c
--- /dev/null
+++ b/spec/support/shared_contexts/changes_access_checks_shared_context.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'changes access checks context' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:user_access) { Gitlab::UserAccess.new(user, container: project) }
+ let(:protocol) { 'ssh' }
+ let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
+ let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:ref) { 'refs/heads/master' }
+ let(:changes) do
+ [
+ # Update of existing branch
+ { oldrev: oldrev, newrev: newrev, ref: ref },
+ # Creation of new branch
+ { newrev: newrev, ref: 'refs/heads/something' },
+ # Deletion of branch
+ { oldrev: oldrev, ref: 'refs/heads/deleteme' }
+ ]
+ end
+
+ let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
+ let(:changes_access) do
+ Gitlab::Checks::ChangesAccess.new(
+ changes,
+ project: project,
+ user_access: user_access,
+ protocol: protocol,
+ logger: logger
+ )
+ end
+
+ subject { described_class.new(changes_access) }
+
+ before do
+ project.add_developer(user)
+ end
+end
diff --git a/spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb
new file mode 100644
index 00000000000..5996fcc6593
--- /dev/null
+++ b/spec/support/shared_contexts/features/integrations/group_integrations_shared_context.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'group integration activation' do
+ include_context 'instance and group integration activation'
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ group.add_owner(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ def visit_group_integrations
+ visit group_settings_integrations_path(group)
+ end
+
+ def visit_group_integration(name)
+ visit_group_integrations
+
+ within('#content-body') do
+ click_link(name)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/features/integrations/instance_and_group_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/instance_and_group_integrations_shared_context.rb
new file mode 100644
index 00000000000..58ee341f71f
--- /dev/null
+++ b/spec/support/shared_contexts/features/integrations/instance_and_group_integrations_shared_context.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'instance and group integration activation' do
+ include_context 'integration activation'
+
+ def click_save_integration
+ click_save_changes_button
+ click_save_settings_modal
+ end
+
+ def click_save_changes_button
+ click_button('Save changes')
+ end
+
+ def click_save_settings_modal
+ click_button('Save')
+ end
+end
diff --git a/spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb
new file mode 100644
index 00000000000..3b02db994a3
--- /dev/null
+++ b/spec/support/shared_contexts/features/integrations/instance_integrations_shared_context.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'instance integration activation' do
+ include_context 'instance and group integration activation'
+
+ let_it_be(:user) { create(:user, :admin) }
+
+ before do
+ sign_in(user)
+ gitlab_enable_admin_mode_sign_in(user)
+ end
+
+ def visit_instance_integrations
+ visit integrations_admin_application_settings_path
+ end
+
+ def visit_instance_integration(name)
+ visit_instance_integrations
+
+ within('#content-body') do
+ click_link(name)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index 34c92367efa..e532b42fd1c 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -5,8 +5,8 @@ Integration.available_services_names.each do |service|
include JiraServiceHelper if service == 'jira'
let(:dashed_service) { service.dasherize }
- let(:service_method) { "#{service}_service".to_sym }
- let(:service_klass) { Integration.service_name_to_model(service) }
+ let(:service_method) { Project.integration_association_name(service) }
+ let(:service_klass) { Integration.integration_name_to_model(service) }
let(:service_instance) { service_klass.new }
let(:service_fields) { service_instance.fields }
let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
@@ -70,3 +70,9 @@ Integration.available_services_names.each do |service|
end
end
end
+
+RSpec.shared_context 'integration activation' do
+ def click_active_checkbox
+ find('label', text: 'Active').click
+ end
+end
diff --git a/spec/support/shared_contexts/project_service_jira_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb
index 54bb9fd108e..54bb9fd108e 100644
--- a/spec/support/shared_contexts/project_service_jira_context.rb
+++ b/spec/support/shared_contexts/features/integrations/project_integrations_jira_context.rb
diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
index 0e3540a3e15..b10844320d0 100644
--- a/spec/support/shared_contexts/project_service_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_context 'project service activation' do
+ include_context 'integration activation'
+
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -21,10 +23,6 @@ RSpec.shared_context 'project service activation' do
end
end
- def click_active_checkbox
- find('label', text: 'Active').click
- end
-
def click_save_integration
click_button('Save changes')
end
diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
new file mode 100644
index 00000000000..334b11c9f6e
--- /dev/null
+++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'package details setup' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package) { create(:package, project: project) }
+
+ let(:package_global_id) { global_id_of(package) }
+
+ let(:depth) { 3 }
+ let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
+ let(:package_files) { all_graphql_fields_for('PackageFile') }
+ let(:user) { project.owner }
+ let(:package_details) { graphql_data_at(:package) }
+ let(:metadata_response) { graphql_data_at(:package, :metadata) }
+ let(:first_file) { package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
+ let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
+ let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
+ let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
+
+ let(:query) do
+ graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
+ #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
+ metadata {
+ #{metadata}
+ }
+ packageFiles {
+ nodes {
+ #{package_files}
+ }
+ }
+ FIELDS
+ end
+end
diff --git a/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb b/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb
new file mode 100644
index 00000000000..a61b8e9a074
--- /dev/null
+++ b/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'clear DB Load Balancing configuration' do
+ def clear_load_balancing_configuration
+ proxy = ::Gitlab::Database::LoadBalancing.instance_variable_get(:@proxy)
+ proxy.load_balancer.release_host if proxy
+ ::Gitlab::Database::LoadBalancing.instance_variable_set(:@proxy, nil)
+
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+ end
+
+ around do |example|
+ clear_load_balancing_configuration
+
+ example.run
+
+ clear_load_balancing_configuration
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 4f8e88ae9da..c00b7203af6 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -18,8 +18,8 @@ RSpec.shared_context 'project navbar structure' do
{
nav_item: _('Security & Compliance'),
nav_sub_items: [
- _('Configuration'),
- (_('Audit Events') if Gitlab.ee?)
+ (_('Audit Events') if Gitlab.ee?),
+ _('Configuration')
]
}
end
@@ -71,8 +71,16 @@ RSpec.shared_context 'project navbar structure' do
]
end
+ let(:project_context_nav_item) do
+ {
+ nav_item: "#{project.name[0, 1].upcase} #{project.name}",
+ nav_sub_items: []
+ }
+ end
+
let(:structure) do
[
+ project_context_nav_item,
project_information_nav_item,
{
nav_item: _('Repository'),
@@ -200,8 +208,16 @@ RSpec.shared_context 'group navbar structure' do
]
end
+ let(:group_context_nav_item) do
+ {
+ nav_item: "#{group.name[0, 1].upcase} #{group.name}",
+ nav_sub_items: []
+ }
+ end
+
let(:structure) do
[
+ group_context_nav_item,
group_information_nav_item,
{
nav_item: _('Issues'),
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 35dc709b5d9..d638ffcf8fa 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -26,7 +26,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_reporter_permissions) do
%i[
admin_issue admin_issue_link admin_label admin_issue_board_list create_snippet
- download_code download_wiki_code fork_project metrics_dashboard
+ daily_statistics download_code download_wiki_code fork_project metrics_dashboard
read_build read_commit_status read_confidential_issues
read_container_image read_deployment read_environment read_merge_request
read_metrics_dashboard_annotation read_pipeline read_prometheus
@@ -44,7 +44,7 @@ RSpec.shared_context 'ProjectPolicy context' do
create_commit_status create_container_image create_deployment
create_environment create_merge_request_from
create_metrics_dashboard_annotation create_pipeline create_release
- create_wiki daily_statistics delete_metrics_dashboard_annotation
+ create_wiki delete_metrics_dashboard_annotation
destroy_container_image push_code read_pod_logs read_terraform_state
resolve_note update_build update_commit_status update_container_image
update_deployment update_environment update_merge_request
diff --git a/spec/support/shared_contexts/read_ci_configuration_shared_context.rb b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb
index 04c50171766..f5d70d5ef5a 100644
--- a/spec/support/shared_contexts/read_ci_configuration_shared_context.rb
+++ b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb
@@ -5,10 +5,6 @@ RSpec.shared_context 'read ci configuration for sast enabled project' do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast.yml'))
end
- let_it_be(:gitlab_ci_yml_default_analyzers_content) do
- File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml'))
- end
-
let_it_be(:gitlab_ci_yml_excluded_analyzers_content) do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml'))
end
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
index ac53be1a1cb..c69a987c00d 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
@@ -8,11 +8,11 @@ RSpec.shared_context 'conan api setup' do
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user }
let_it_be(:base_secret) { SecureRandom.base64(64) }
- let_it_be(:job) { create(:ci_build, :running, user: user) }
- let_it_be(:job_token) { job.token }
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, 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/helm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/helm_packages_shared_context.rb
new file mode 100644
index 00000000000..099fdec0cc8
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/helm_packages_shared_context.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'helm api setup' do
+ include WorkhorseHelpers
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
+end
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
index 815108be447..c737091df48 100644
--- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
@@ -11,7 +11,7 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
- let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
+ let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
diff --git a/spec/support/shared_contexts/services/packages/debian/distribution_shared_context.rb b/spec/support/shared_contexts/services/packages/debian/distribution_shared_context.rb
new file mode 100644
index 00000000000..67e2c0629cc
--- /dev/null
+++ b/spec/support/shared_contexts/services/packages/debian/distribution_shared_context.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with published Debian package' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:project_distribution) { create(:debian_project_distribution, container: project, codename: 'unstable', valid_time_duration_seconds: 48.hours.to_i) }
+ let_it_be(:package) { create(:debian_package, project: project, published_in: project_distribution) }
+end
+
+RSpec.shared_context 'with Debian distribution' do |container_type|
+ let_it_be(:container_type) { container_type }
+
+ if container_type == :project
+ let_it_be(:container) { project }
+ let_it_be(:distribution, reload: true) { project_distribution }
+ else
+ let_it_be(:container) { group }
+ let_it_be(:distribution, reload: true) { create(:debian_group_distribution, container: group, codename: 'unstable', valid_time_duration_seconds: 48.hours.to_i) }
+ end
+end
diff --git a/spec/support/shared_contexts/change_access_checks_shared_context.rb b/spec/support/shared_contexts/single_change_access_checks_shared_context.rb
index 4c55990c901..bf90c26047b 100644
--- a/spec/support/shared_contexts/change_access_checks_shared_context.rb
+++ b/spec/support/shared_contexts/single_change_access_checks_shared_context.rb
@@ -12,7 +12,7 @@ RSpec.shared_context 'change access checks context' do
let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
let(:change_access) do
- Gitlab::Checks::ChangeAccess.new(
+ Gitlab::Checks::SingleChangeAccess.new(
changes,
project: project,
user_access: user_access,
diff --git a/spec/support/shared_examples/ci/badge_template_shared_examples.rb b/spec/support/shared_examples/ci/badge_template_shared_examples.rb
new file mode 100644
index 00000000000..94aec33ecc2
--- /dev/null
+++ b/spec/support/shared_examples/ci/badge_template_shared_examples.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a badge template' do |badge_type|
+ describe '#key_text' do
+ it "says #{badge_type} by default" do
+ expect(template.key_text).to eq(badge_type)
+ end
+
+ context 'when custom key_text is defined' do
+ before do
+ allow(badge).to receive(:customization).and_return({ key_text: "custom text" })
+ end
+
+ it 'returns custom value' do
+ expect(template.key_text).to eq("custom text")
+ end
+
+ context 'when its size is larger than the max allowed value' do
+ before do
+ allow(badge).to receive(:customization).and_return({ key_text: 't' * (::Gitlab::Ci::Badge::Template::MAX_KEY_TEXT_SIZE + 1) } )
+ end
+
+ it 'returns default value' do
+ expect(template.key_text).to eq(badge_type)
+ end
+ end
+ end
+ end
+
+ describe '#key_width' do
+ let_it_be(:default_key_width) { ::Gitlab::Ci::Badge::Template::DEFAULT_KEY_WIDTH }
+
+ it 'is fixed by default' do
+ expect(template.key_width).to eq(default_key_width)
+ end
+
+ context 'when custom key_width is defined' do
+ before do
+ allow(badge).to receive(:customization).and_return({ key_width: 101 })
+ end
+
+ it 'returns custom value' do
+ expect(template.key_width).to eq(101)
+ end
+
+ context 'when it is larger than the max allowed value' do
+ before do
+ allow(badge).to receive(:customization).and_return({ key_width: ::Gitlab::Ci::Badge::Template::MAX_KEY_WIDTH + 1 })
+ end
+
+ it 'returns default value' do
+ expect(template.key_width).to eq(default_key_width)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index cfee26a0d6a..9af35c189d0 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -298,7 +298,7 @@ RSpec.shared_examples 'wiki controller actions' do
expect(response.headers['Content-Disposition']).to match(/^inline/)
expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true')
expect(response.cache_control[:public]).to be(false)
- expect(response.cache_control[:extras]).to include('no-store')
+ expect(response.headers['Cache-Control']).to eq('no-store')
end
end
end
@@ -486,7 +486,7 @@ RSpec.shared_examples 'wiki controller actions' do
end.not_to change { wiki.list_pages.size }
expect(response).to render_template('shared/wikis/edit')
- expect(assigns(:error).message).to eq('Could not delete wiki page')
+ expect(assigns(:error)).to eq('Could not delete wiki page')
end
end
end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 808e0be6be2..ff2878f77b4 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -11,6 +11,8 @@ RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name
let(:comment) { 'My comment' }
it 'clicking "Comment" will post a comment' do
+ wait_for_all_requests
+
expect(page).to have_selector toggle_selector
find("#{form_selector} .note-textarea").send_keys(comment)
@@ -29,6 +31,8 @@ RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name
find("#{form_selector} .note-textarea").send_keys(comment)
find(toggle_selector).click
+
+ wait_for_all_requests
end
it 'has a "Comment" item (selected by default) and "Start thread" item' do
diff --git a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
new file mode 100644
index 00000000000..cfa043322db
--- /dev/null
+++ b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'user activates the Mattermost Slash Command integration' do
+ it 'shows a help message' do
+ expect(page).to have_content('Use this service to perform common')
+ end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
+
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ end
+
+ it 'redirects to the integrations page after saving but not activating' do
+ token = ('a'..'z').to_a.join
+
+ fill_in 'service_token', with: token
+ click_active_checkbox
+ click_save_integration
+
+ expect(current_path).to eq(edit_path)
+ expect(page).to have_content('Mattermost slash commands settings saved, but not active.')
+ end
+
+ it 'redirects to the integrations page after activating' do
+ token = ('a'..'z').to_a.join
+
+ fill_in 'service_token', with: token
+ click_save_integration
+
+ expect(current_path).to eq(edit_path)
+ expect(page).to have_content('Mattermost slash commands settings saved and active.')
+ 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 736c353c2aa..c0cfc27ceaf 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,9 +1,12 @@
# frozen_string_literal: true
-RSpec.shared_examples 'issuable invite members experiments' do
+RSpec.shared_examples 'issuable invite members' do
context 'when a privileged user can invite' do
- it 'shows a link for inviting members and launches invite modal' do
+ before do
project.add_maintainer(user)
+ end
+
+ it 'shows a link for inviting members and launches invite modal' do
visit issuable_path
find('.block.assignee .edit-link').click
@@ -23,8 +26,11 @@ RSpec.shared_examples 'issuable invite members experiments' do
end
context 'when user cannot invite members in assignee dropdown' do
- it 'shows author in assignee dropdown and no invite link' do
+ before do
project.add_developer(user)
+ end
+
+ it 'shows author in assignee dropdown and no invite link' do
visit issuable_path
find('.block.assignee .edit-link').click
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 4b94411f009..997500415a9 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -283,6 +283,8 @@ RSpec.shared_examples 'variable list' do
end
def fill_variable(key, value, protected: false, masked: false)
+ wait_for_requests
+
page.within('#add-ci-variable') do
find('[data-qa-selector="ci_variable_key_field"] input').set(key)
find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present?
diff --git a/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb b/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
index 96b05db4cd9..5cbbed1468f 100644
--- a/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
+++ b/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
@@ -24,6 +24,12 @@ RSpec.shared_examples 'assignee NOT username filter' do
end
end
+RSpec.shared_examples 'assignee OR filter' do
+ it 'returns issuables assigned to the given users' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
RSpec.shared_examples 'no assignee filter' do
let(:params) { { assignee_id: 'None' } }
diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
index fc795012ce7..5e15c91cd41 100644
--- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
@@ -6,9 +6,9 @@ RSpec.shared_examples 'a mutation which can mutate a spammable' do
describe "#additional_spam_params" do
it 'passes additional spam params to the service' do
args = [
- anything,
- anything,
- hash_including(
+ project: anything,
+ current_user: anything,
+ params: hash_including(
api: true,
request: instance_of(ActionDispatch::Request),
captcha_response: captcha_response,
diff --git a/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb
index ebba312e895..678bb908343 100644
--- a/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb
@@ -2,44 +2,37 @@
require 'spec_helper'
-RSpec.shared_examples 'a subscribeable graphql resource' do
- let(:project) { resource.project }
- let_it_be(:user) { create(:user) }
-
- subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+RSpec.shared_examples 'a subscribeable not accessible graphql resource' do
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
- specify { expect(described_class).to require_graphql_authorizations(permission_name) }
+ subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, subscribed_state: true) }
- describe '#resolve' do
- let(:subscribe) { true }
- let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] }
-
- subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, subscribed_state: subscribe) }
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+end
- it 'raises an error if the resource is not accessible to the user' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- end
+RSpec.shared_examples 'a subscribeable graphql resource' do
+ let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] }
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ let(:subscribe) { true }
- context 'when the user can update the resource' do
- before do
- resource.project.add_developer(user)
- end
+ subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, subscribed_state: subscribe) }
- it 'subscribes to the resource' do
- expect(mutated_resource).to eq(resource)
- expect(mutated_resource.subscribed?(user, project)).to eq(true)
- expect(subject[:errors]).to be_empty
- end
+ it 'subscribes to the resource' do
+ expect(mutated_resource).to eq(resource)
+ expect(mutated_resource.subscribed?(user, project)).to eq(true)
+ expect(subject[:errors]).to be_empty
+ end
- context 'when passing subscribe as false' do
- let(:subscribe) { false }
+ context 'when passing subscribe as false' do
+ let(:subscribe) { false }
- it 'unsubscribes from the discussion' do
- resource.subscribe(user, project)
+ it 'unsubscribes from the discussion' do
+ resource.subscribe(user, project)
- expect(mutated_resource.subscribed?(user, project)).to eq(false)
- end
- end
+ expect(mutated_resource.subscribed?(user, project)).to eq(false)
+ expect(subject[:errors]).to be_empty
end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
index 9c95d1ff9d9..3760325675a 100644
--- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
@@ -29,6 +29,34 @@ RSpec.shared_examples 'common trace features' do
end
end
+ describe '#read' do
+ context 'gitlab_ci_archived_trace_consistent_reads feature flag enabled' do
+ before do
+ stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project)
+ end
+
+ it 'calls ::Gitlab::Database::LoadBalancing::Sticking.unstick_or_continue_sticking' do
+ expect(::Gitlab::Database::LoadBalancing::Sticking).to receive(:unstick_or_continue_sticking)
+ .with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id)
+ .and_call_original
+
+ trace.read { |stream| stream }
+ end
+ end
+
+ context 'gitlab_ci_archived_trace_consistent_reads feature flag disabled' do
+ before do
+ stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false)
+ end
+
+ it 'does not call ::Gitlab::Database::LoadBalancing::Sticking.unstick_or_continue_sticking' do
+ expect(::Gitlab::Database::LoadBalancing::Sticking).not_to receive(:unstick_or_continue_sticking)
+
+ trace.read { |stream| stream }
+ end
+ end
+ end
+
describe '#extract_coverage' do
let(:regex) { '\(\d+.\d+\%\) covered' }
@@ -253,6 +281,52 @@ RSpec.shared_examples 'common trace features' do
describe '#archive!' do
subject { trace.archive! }
+ context 'when live trace chunks exists' do
+ before do
+ # Build a trace_chunk manually
+ # It is possible to do so with trace.set but only if ci_enable_live_trace FF is enabled
+ #
+ # We need the job to have a trace_chunk because we only use #stick in
+ # the case where trace_chunks exist.
+ stream = Gitlab::Ci::Trace::Stream.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(trace.job)
+ end
+
+ stream.set(+"12\n34")
+ end
+
+ # We check the before setup actually sets up job trace_chunks
+ it 'has job trace_chunks' do
+ expect(trace.job.trace_chunks).to be_present
+ end
+
+ context 'gitlab_ci_archived_trace_consistent_reads feature flag enabled' do
+ before do
+ stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project)
+ end
+
+ it 'calls ::Gitlab::Database::LoadBalancing::Sticking.stick' do
+ expect(::Gitlab::Database::LoadBalancing::Sticking).to receive(:stick)
+ .with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id)
+ .and_call_original
+
+ subject
+ end
+ end
+
+ context 'gitlab_ci_archived_trace_consistent_reads feature flag disabled' do
+ before do
+ stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false)
+ end
+
+ it 'does not call ::Gitlab::Database::LoadBalancing::Sticking.stick' do
+ expect(::Gitlab::Database::LoadBalancing::Sticking).not_to receive(:stick)
+
+ subject
+ end
+ end
+ end
+
context 'when build status is success' do
let!(:build) { create(:ci_build, :success, :trace_live) }
diff --git a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb
new file mode 100644
index 00000000000..5baa6478225
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'tracks assignment and records the subject' do |experiment, subject_type|
+ it 'tracks the assignment', :experiment do
+ expect(experiment(experiment))
+ .to track(:assignment)
+ .with_context(subject_type => subject)
+ .on_next_instance
+
+ action
+ end
+
+ it 'records the subject' do
+ stub_experiments(experiment => :candidate)
+
+ expect(Experiment).to receive(:add_subject).with(experiment.to_s, variant: :experimental, subject: subject)
+
+ action
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index 4a47aad0957..9f3be3e2e06 100644
--- a/spec/support/shared_examples/models/chat_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-RSpec.shared_examples "chat service" do |service_name|
+RSpec.shared_examples "chat integration" do |integration_name|
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe "Validations" do
- context "when service is active" do
+ context "when integration is active" do
before do
subject.active = true
end
@@ -16,7 +16,7 @@ RSpec.shared_examples "chat service" do |service_name|
it_behaves_like "issue tracker service URL attribute", :webhook
end
- context "when service is inactive" do
+ context "when integration is inactive" do
before do
subject.active = false
end
@@ -47,12 +47,12 @@ RSpec.shared_examples "chat service" do |service_name|
WebMock.stub_request(:post, webhook_url)
end
- shared_examples "triggered #{service_name} service" do |branches_to_be_notified: nil|
+ shared_examples "triggered #{integration_name} integration" do |branches_to_be_notified: nil|
before do
subject.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
end
- it "calls #{service_name} API" do
+ it "calls #{integration_name} API" do
result = subject.execute(sample_data)
expect(result).to be(true)
@@ -63,12 +63,12 @@ RSpec.shared_examples "chat service" do |service_name|
end
end
- shared_examples "untriggered #{service_name} service" do |branches_to_be_notified: nil|
+ shared_examples "untriggered #{integration_name} integration" do |branches_to_be_notified: nil|
before do
subject.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified
end
- it "does not call #{service_name} API" do
+ it "does not call #{integration_name} API" do
result = subject.execute(sample_data)
expect(result).to be(false)
@@ -81,7 +81,7 @@ RSpec.shared_examples "chat service" do |service_name|
Gitlab::DataBuilder::Push.build_sample(project, user)
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
it "specifies the webhook when it is configured", if: defined?(client) do
expect(client).to receive(:new).with(client_arguments).and_return(double(:chat_service).as_null_object)
@@ -95,19 +95,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
@@ -121,19 +121,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "protected"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
@@ -143,19 +143,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
end
@@ -168,7 +168,7 @@ RSpec.shared_examples "chat service" do |service_name|
service.hook_data(issue, "open")
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
context "with merge events" do
@@ -191,7 +191,7 @@ RSpec.shared_examples "chat service" do |service_name|
project.add_developer(user)
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
context "with wiki page events" do
@@ -207,7 +207,7 @@ RSpec.shared_examples "chat service" do |service_name|
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") }
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
context "with note events" do
@@ -222,7 +222,7 @@ RSpec.shared_examples "chat service" do |service_name|
note: "a comment on a commit")
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
context "with merge request comment" do
@@ -230,7 +230,7 @@ RSpec.shared_examples "chat service" do |service_name|
create(:note_on_merge_request, project: project, note: "merge request note")
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
context "with issue comment" do
@@ -238,7 +238,7 @@ RSpec.shared_examples "chat service" do |service_name|
create(:note_on_issue, project: project, note: "issue note")
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
context "with snippet comment" do
@@ -246,7 +246,7 @@ RSpec.shared_examples "chat service" do |service_name|
create(:note_on_project_snippet, project: project, note: "snippet note")
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
end
@@ -262,14 +262,14 @@ RSpec.shared_examples "chat service" do |service_name|
context "with failed pipeline" do
let(:status) { "failed" }
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
context "with succeeded pipeline" do
let(:status) { "success" }
context "with default notify_only_broken_pipelines" do
- it "does not call #{service_name} API" do
+ it "does not call #{integration_name} API" do
result = subject.execute(sample_data)
expect(result).to be_falsy
@@ -281,7 +281,7 @@ RSpec.shared_examples "chat service" do |service_name|
subject.notify_only_broken_pipelines = false
end
- it_behaves_like "triggered #{service_name} service"
+ it_behaves_like "triggered #{integration_name} integration"
end
end
@@ -291,19 +291,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
@@ -317,19 +317,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "protected"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
@@ -339,19 +339,19 @@ RSpec.shared_examples "chat service" do |service_name|
end
context "when only default branch are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default"
end
context "when only protected branches are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "protected"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "protected"
end
context "when default and protected branches are to be notified" do
- it_behaves_like "untriggered #{service_name} service", branches_to_be_notified: "default_and_protected"
+ it_behaves_like "untriggered #{integration_name} integration", branches_to_be_notified: "default_and_protected"
end
context "when all branches are to be notified" do
- it_behaves_like "triggered #{service_name} service", branches_to_be_notified: "all"
+ it_behaves_like "triggered #{integration_name} integration", branches_to_be_notified: "all"
end
end
end
diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
index 09b7d1be704..66448aca2c5 100644
--- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
+RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name|
include StubRequests
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com' }
def execute_with_options(options)
- receive(:new).with(webhook_url, options.merge(http_client: SlackMattermost::Notifier::HTTPClient))
+ receive(:new).with(webhook_url, options.merge(http_client: Integrations::SlackMattermostNotifier::HTTPClient))
.and_return(double(:slack_service).as_null_object)
end
@@ -81,7 +81,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
shared_examples 'calls the service API with the event message' do |event_message|
specify do
- expect_next_instance_of(Slack::Messenger) do |messenger|
+ expect_next_instance_of(::Slack::Messenger) do |messenger|
expect(messenger).to receive(:ping).with(event_message, anything).and_call_original
end
@@ -95,7 +95,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { username: 'slack_username' } }
it 'uses the username as an option' do
- expect(Slack::Messenger).to execute_with_options(username: 'slack_username')
+ expect(::Slack::Messenger).to execute_with_options(username: 'slack_username')
execute_service
end
@@ -110,7 +110,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { push_channel: 'random' } }
it 'uses the right channel for push event' do
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
@@ -128,6 +128,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
context 'issue events' do
let_it_be(:issue) { create(:issue) }
+
let(:data) { issue.to_hook_data(user) }
it_behaves_like 'calls the service API with the event message', /Issue (.*?) opened by/
@@ -136,7 +137,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { issue_channel: 'random' } }
it 'uses the right channel for issue event' do
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
@@ -147,7 +148,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to issue channel' do
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
@@ -156,7 +157,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } }
it 'uses the confidential issue channel when it is defined' do
- expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['confidential'])
execute_service
end
@@ -167,6 +168,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
context 'merge request events' do
let_it_be(:merge_request) { create(:merge_request) }
+
let(:data) { merge_request.to_hook_data(user) }
it_behaves_like 'calls the service API with the event message', /opened merge request/
@@ -175,7 +177,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { merge_request_channel: 'random' } }
it 'uses the right channel for merge request event' do
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
@@ -184,15 +186,16 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
context 'wiki page events' do
let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') }
+
let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
- it_behaves_like 'calls the service API with the event message', / created (.*?)wikis\/(.*?)|wiki page> in/
+ it_behaves_like 'calls the service API with the event message', %r{ created (.*?)wikis/(.*?)|wiki page> in}
context 'with event channel' do
let(:chat_service_params) { { wiki_page_channel: 'random' } }
it 'uses the right channel for wiki event' do
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
@@ -201,6 +204,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
context 'deployment events' do
let_it_be(:deployment) { create(:deployment) }
+
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) }
it_behaves_like 'calls the service API with the event message', /Deploy to (.*?) created/
@@ -208,6 +212,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
context 'note event' do
let_it_be(:issue_note) { create(:note_on_issue, project: project, note: "issue note") }
+
let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
it_behaves_like 'calls the service API with the event message', /commented on issue/
@@ -216,7 +221,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { note_channel: 'random' } }
it 'uses the right channel' do
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
@@ -227,7 +232,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to note channel' do
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['random'])
execute_service
end
@@ -236,7 +241,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:chat_service_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } }
it 'uses confidential channel' do
- expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
+ expect(::Slack::Messenger).to execute_with_options(channel: ['confidential'])
execute_service
end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index 68142e667a4..39121b73bc5 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -86,45 +86,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.")
end
end
-
- describe "#uniqueness_of_title" do
- context "per project" do
- it "does not accept the same title in a project twice" do
- new_timebox = timebox.dup
- expect(new_timebox).not_to be_valid
- end
-
- it "accepts the same title in another project" do
- project = create(:project)
- new_timebox = timebox.dup
- new_timebox.project = project
-
- expect(new_timebox).to be_valid
- end
- end
-
- context "per group" do
- let(:timebox) { create(timebox_type, *timebox_args, group: group) }
-
- before do
- project.update!(group: group)
- end
-
- it "does not accept the same title in a group twice" do
- new_timebox = described_class.new(group: group, title: timebox.title)
-
- expect(new_timebox).not_to be_valid
- end
-
- it "does not accept the same title of a child project timebox" do
- create(timebox_type, *timebox_args, project: group.projects.first)
-
- new_timebox = described_class.new(group: group, title: timebox.title)
-
- expect(new_timebox).not_to be_valid
- end
- end
- end
end
describe "Associations" do
diff --git a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
index 49729afce61..128999d02fa 100644
--- a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'chat slash commands service' do
+RSpec.shared_examples Integrations::BaseSlashCommands do
describe "Associations" do
it { is_expected.to respond_to :token }
it { is_expected.to have_many :chat_names }
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index 2392658e584..04630484964 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -66,7 +66,7 @@ RSpec.shared_examples 'a mentionable' do
expect(subject.gfm_reference).to eq(backref_text)
end
- it "extracts references from its reference property" do
+ it "extracts references from its reference property", :clean_gitlab_redis_cache do
# De-duplicate and omit itself
refs = subject.referenced_mentionables
expect(refs.size).to eq(6)
@@ -98,7 +98,7 @@ RSpec.shared_examples 'a mentionable' do
end
end
- it 'creates cross-reference notes' do
+ it 'creates cross-reference notes', :clean_gitlab_redis_cache do
mentioned_objects = [mentioned_issue, mentioned_mr, mentioned_commit,
ext_issue, ext_mr, ext_commit]
diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
index e6b16d5881d..f08ee820463 100644
--- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
+++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
@@ -142,6 +142,14 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
end
end
+ describe '.with_architecture' do
+ subject { described_class.with_architecture(architecture1_2) }
+
+ it do
+ expect(subject.to_a).to contain_exactly(component_file_other_architecture)
+ end
+ end
+
describe '.with_architecture_name' do
subject { described_class.with_architecture_name(architecture1_2.name) }
@@ -166,12 +174,12 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
end
end
- describe '.created_before' do
- let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 4.hours.ago) }
- let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 3.hours.ago) }
- let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 1.hour.ago) }
+ describe '.updated_before' do
+ let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, updated_at: 4.hours.ago) }
+ let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, updated_at: 3.hours.ago) }
+ let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, updated_at: 1.hour.ago) }
- subject { described_class.created_before(2.hours.ago) }
+ subject { described_class.updated_before(2.hours.ago) }
it do
expect(subject.to_a).to contain_exactly(component_file1, component_file2)
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_key_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_key_shared_examples.rb
new file mode 100644
index 00000000000..26794c83736
--- /dev/null
+++ b/spec/support/shared_examples/models/packages/debian/distribution_key_shared_examples.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'Debian Distribution Key' do |container|
+ let_it_be_with_refind(:distribution_key) { create("debian_#{container}_distribution_key") } # rubocop:disable Rails/SaveBang
+
+ subject { distribution_key }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:key) }
+ end
+
+ describe 'validations' do
+ describe "#distribution" do
+ it { is_expected.to validate_presence_of(:distribution) }
+ end
+
+ describe '#private_key' do
+ it { is_expected.to validate_presence_of(:private_key) }
+
+ it { is_expected.to allow_value("-----BEGIN PGP PRIVATE KEY BLOCK-----\n...").for(:private_key) }
+ it { is_expected.not_to allow_value('A').for(:private_key).with_message('must be ASCII armored') }
+ end
+
+ describe '#passphrase' do
+ it { is_expected.to validate_presence_of(:passphrase) }
+
+ it { is_expected.to allow_value('P@$$w0rd').for(:passphrase) }
+ it { is_expected.to allow_value('A' * 255).for(:passphrase) }
+ it { is_expected.not_to allow_value('A' * 256).for(:passphrase) }
+ end
+
+ describe '#public_key' do
+ it { is_expected.to validate_presence_of(:public_key) }
+
+ it { is_expected.to allow_value("-----BEGIN PGP PUBLIC KEY BLOCK-----\n...").for(:public_key) }
+ it { is_expected.not_to allow_value('A').for(:public_key).with_message('must be ASCII armored') }
+ end
+
+ describe '#fingerprint' do
+ it { is_expected.to validate_presence_of(:passphrase) }
+
+ it { is_expected.to allow_value('abc').for(:passphrase) }
+ it { is_expected.to allow_value('A' * 255).for(:passphrase) }
+ it { is_expected.not_to allow_value('A' * 256).for(:passphrase) }
+ 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 8693d6868e9..5459d17b1df 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
@@ -17,6 +17,7 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it { is_expected.to belong_to(container) }
it { is_expected.to belong_to(:creator).class_name('User') }
+ it { is_expected.to have_one(:key).class_name("Packages::Debian::#{container.capitalize}DistributionKey").with_foreign_key(:distribution_id).inverse_of(:distribution) }
it { is_expected.to have_many(:components).class_name("Packages::Debian::#{container.capitalize}Component").inverse_of(:distribution) }
it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) }
end
diff --git a/spec/support/shared_examples/namespaces/linear_traversal_examples.rb b/spec/support/shared_examples/namespaces/linear_traversal_examples.rb
new file mode 100644
index 00000000000..2fd90c36953
--- /dev/null
+++ b/spec/support/shared_examples/namespaces/linear_traversal_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Traversal examples common to linear and recursive methods are in
+# spec/support/shared_examples/namespaces/traversal_examples.rb
+
+RSpec.shared_examples 'linear namespace traversal' do
+ context 'when use_traversal_ids feature flag is enabled' do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+ end
+
+ context 'scopes' do
+ describe '.as_ids' do
+ let_it_be(:namespace1) { create(:group) }
+ let_it_be(:namespace2) { create(:group) }
+
+ subject { Namespace.where(id: [namespace1, namespace2]).as_ids.pluck(:id) }
+
+ it { is_expected.to contain_exactly(namespace1.id, namespace2.id) }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb
index 77a1705627e..ccc64c80fd4 100644
--- a/spec/support/shared_examples/namespaces/traversal_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_examples.rb
@@ -17,6 +17,28 @@ RSpec.shared_examples 'namespace traversal' do
end
end
+ describe '#root_ancestor' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:nested_group) { create(:group, parent: group) }
+ let_it_be(:deep_nested_group) { create(:group, parent: nested_group) }
+
+ it 'returns the correct root ancestor' do
+ expect(group.root_ancestor).to eq(group)
+ expect(nested_group.root_ancestor).to eq(group)
+ expect(deep_nested_group.root_ancestor).to eq(group)
+ end
+
+ describe '#recursive_root_ancestor' do
+ let(:groups) { [group, nested_group, deep_nested_group] }
+
+ it "is equivalent to #recursive_root_ancestor" do
+ groups.each do |group|
+ expect(group.root_ancestor).to eq(group.recursive_root_ancestor)
+ end
+ end
+ end
+ end
+
describe '#self_and_hierarchy' do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
@@ -122,4 +144,20 @@ RSpec.shared_examples 'namespace traversal' do
it_behaves_like 'recursive version', :self_and_descendants
end
end
+
+ describe '#self_and_descendant_ids' do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+
+ subject { group.self_and_descendant_ids.pluck(:id) }
+
+ it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id) }
+
+ describe '#recursive_self_and_descendant_ids' do
+ let(:groups) { [group, nested_group, deep_nested_group] }
+
+ it_behaves_like 'recursive version', :self_and_descendant_ids
+ end
+ end
end
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
index c938c6432fe..20606ae942d 100644
--- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -294,16 +294,6 @@ RSpec.shared_examples 'rejects invalid upload_url params' do
end
end
-RSpec.shared_examples 'successful response when using Unicorn' do
- context 'on Unicorn', :unicorn do
- it 'returns successfully' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-end
-
RSpec.shared_examples 'recipe snapshot endpoint' do
subject { get api(url), headers: headers }
@@ -372,7 +362,6 @@ RSpec.shared_examples 'recipe upload_urls endpoint' do
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects invalid upload_url params'
- it_behaves_like 'successful response when using Unicorn'
it 'returns a set of upload urls for the files requested' do
subject
@@ -434,7 +423,6 @@ RSpec.shared_examples 'package upload_urls endpoint' do
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects invalid upload_url params'
- it_behaves_like 'successful response when using Unicorn'
it 'returns a set of upload urls for the files requested' do
expected_response = {
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 dfd19167dcd..0530aa8c760 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
@@ -12,7 +12,35 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
let_it_be(:user, freeze: true) { create(:user) }
let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) }
- let(:distribution) { 'bullseye' }
+ let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", container: private_container, codename: 'existing-codename') }
+ let_it_be(:private_component, freeze: true) { create("debian_#{container_type}_component", distribution: private_distribution, name: 'existing-component') }
+ let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') }
+ let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') }
+
+ let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", container: public_container, codename: 'existing-codename') }
+ let_it_be(:public_component, freeze: true) { create("debian_#{container_type}_component", distribution: public_distribution, name: 'existing-component') }
+ let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') }
+ let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') }
+
+ if container_type == :group
+ let_it_be(:private_project) { create(:project, :private, group: private_container) }
+ let_it_be(:public_project) { create(:project, :public, group: public_container) }
+ let_it_be(:private_project_distribution) { create(:debian_project_distribution, container: private_project, codename: 'existing-codename') }
+ let_it_be(:public_project_distribution) { create(:debian_project_distribution, container: public_project, codename: 'existing-codename') }
+ else
+ let_it_be(:private_project) { private_container }
+ let_it_be(:public_project) { public_container }
+ let_it_be(:private_project_distribution) { private_distribution }
+ let_it_be(:public_project_distribution) { public_distribution }
+ end
+
+ let_it_be(:private_package) { create(:debian_package, project: private_project, published_in: private_project_distribution) }
+ let_it_be(:public_package) { create(:debian_package, project: public_project, published_in: public_project_distribution) }
+
+ let(:visibility_level) { :public }
+
+ let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] }
+
let(:component) { 'main' }
let(:architecture) { 'amd64' }
let(:source_package) { 'sample' }
@@ -97,7 +125,7 @@ RSpec.shared_examples 'Debian repository GET request' do |status, body = nil|
expect(response).to have_gitlab_http_status(status)
unless body.nil?
- expect(response.body).to eq(body)
+ expect(response.body).to match(body)
end
end
end
@@ -107,16 +135,25 @@ RSpec.shared_examples 'Debian repository upload request' do |status, body = nil|
if status == :created
it 'creates package files', :aggregate_failures do
- pending "Debian package creation not implemented"
+ expect(::Packages::Debian::FindOrCreateIncomingService).to receive(:new).with(container, user).and_call_original
+ expect(::Packages::Debian::CreatePackageFileService).to receive(:new).with(be_a(Packages::Package), be_an(Hash)).and_call_original
+
+ if file_name.end_with? '.changes'
+ expect(::Packages::Debian::ProcessChangesWorker).to receive(:perform_async)
+ else
+ expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async)
+ end
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)
expect(response).to have_gitlab_http_status(status)
expect(response.media_type).to eq('text/plain')
unless body.nil?
- expect(response.body).to eq(body)
+ expect(response.body).to match(body)
end
end
it_behaves_like 'a package tracking event', described_class.name, 'push_package'
@@ -127,7 +164,7 @@ RSpec.shared_examples 'Debian repository upload request' do |status, body = nil|
expect(response).to have_gitlab_http_status(status)
unless body.nil?
- expect(response.body).to eq(body)
+ expect(response.body).to match(body)
end
end
end
@@ -173,18 +210,112 @@ RSpec.shared_examples 'Debian repository upload authorize request' do |status, b
expect(response).to have_gitlab_http_status(status)
unless body.nil?
- expect(response.body).to eq(body)
+ expect(response.body).to match(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian repository POST distribution request' do |status, body|
+ and_body = body.nil? ? '' : ' and expected body'
+
+ if status == :created
+ it 'creates distribution', :aggregate_failures do
+ expect(::Packages::Debian::CreateDistributionService).to receive(:new).with(container, user, api_params).and_call_original
+
+ expect { subject }
+ .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(1)
+ .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(1)
+ .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(2)
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ else
+ it "returns #{status}#{and_body}", :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian repository PUT distribution request' do |status, body|
+ and_body = body.nil? ? '' : ' and expected body'
+
+ 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
+
+ expect { subject }
+ .to not_change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }
+ .and not_change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }
+ .and not_change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ else
+ it "returns #{status}#{and_body}", :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to match(body)
end
end
end
end
-RSpec.shared_examples 'rejects Debian access with unknown container id' do
+RSpec.shared_examples 'Debian repository DELETE distribution request' do |status, body|
+ and_body = body.nil? ? '' : ' and expected body'
+
+ if status == :success
+ it 'updates distribution', :aggregate_failures do
+ expect { subject }
+ .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(-1)
+ .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(-1)
+ .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(-2)
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ else
+ it "returns #{status}#{and_body}", :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to match(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects Debian access with unknown container id' do |hidden_status|
context 'with an unknown container' do
let(:container) { double(id: non_existing_record_id) }
context 'as anonymous' do
- it_behaves_like 'Debian repository GET request', :unauthorized, nil
+ it_behaves_like 'Debian repository GET request', hidden_status, nil
end
context 'as authenticated user' do
@@ -195,19 +326,25 @@ RSpec.shared_examples 'rejects Debian access with unknown container id' do
end
end
-RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, success_status, success_body|
+RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true|
+ hidden_status = if authenticate_non_public
+ :unauthorized
+ else
+ :not_found
+ end
+
context 'with valid container' do
using RSpec::Parameterized::TableSyntax
where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
:public | :developer | true | true | success_status | success_body
:public | :guest | true | true | success_status | success_body
- :public | :developer | true | false | success_status | success_body
- :public | :guest | true | false | success_status | success_body
+ :public | :developer | true | false | :unauthorized | nil
+ :public | :guest | true | false | :unauthorized | nil
:public | :developer | false | true | success_status | success_body
:public | :guest | false | true | success_status | success_body
- :public | :developer | false | false | success_status | success_body
- :public | :guest | false | false | success_status | success_body
+ :public | :developer | false | false | :unauthorized | nil
+ :public | :guest | false | false | :unauthorized | nil
:public | :anonymous | false | true | success_status | success_body
:private | :developer | true | true | success_status | success_body
:private | :guest | true | true | :forbidden | nil
@@ -217,7 +354,7 @@ RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, su
:private | :guest | false | true | :not_found | nil
:private | :developer | false | false | :unauthorized | nil
:private | :guest | false | false | :unauthorized | nil
- :private | :anonymous | false | true | :unauthorized | nil
+ :private | :anonymous | false | true | hidden_status | nil
end
with_them do
@@ -227,10 +364,16 @@ RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, su
end
end
- it_behaves_like 'rejects Debian access with unknown container id'
+ it_behaves_like 'rejects Debian access with unknown container id', hidden_status
end
-RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, success_status, success_body|
+RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true|
+ hidden_status = if authenticate_non_public
+ :unauthorized
+ else
+ :not_found
+ end
+
context 'with valid container' do
using RSpec::Parameterized::TableSyntax
@@ -252,7 +395,50 @@ RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, s
:private | :guest | false | true | :not_found | nil
:private | :developer | false | false | :unauthorized | nil
:private | :guest | false | false | :unauthorized | nil
- :private | :anonymous | false | true | :unauthorized | nil
+ :private | :anonymous | false | true | hidden_status | nil
+ end
+
+ with_them do
+ include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do
+ it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown container id', hidden_status
+end
+
+RSpec.shared_examples 'Debian repository maintainer write endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true|
+ hidden_status = if authenticate_non_public
+ :unauthorized
+ else
+ :not_found
+ end
+
+ context 'with valid container' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
+ :public | :maintainer | true | true | success_status | success_body
+ :public | :developer | true | true | :forbidden | nil
+ :public | :guest | true | true | :forbidden | nil
+ :public | :maintainer | true | false | :unauthorized | nil
+ :public | :guest | true | false | :unauthorized | nil
+ :public | :maintainer | false | true | :forbidden | nil
+ :public | :guest | false | true | :forbidden | nil
+ :public | :maintainer | false | false | :unauthorized | nil
+ :public | :guest | false | false | :unauthorized | nil
+ :public | :anonymous | false | true | :unauthorized | nil
+ :private | :maintainer | true | true | success_status | success_body
+ :private | :developer | true | true | :forbidden | nil
+ :private | :guest | true | true | :forbidden | nil
+ :private | :maintainer | true | false | :unauthorized | nil
+ :private | :guest | true | false | :unauthorized | nil
+ :private | :maintainer | false | true | :not_found | nil
+ :private | :guest | false | true | :not_found | nil
+ :private | :maintainer | false | false | :unauthorized | nil
+ :private | :guest | false | false | :unauthorized | nil
+ :private | :anonymous | false | true | hidden_status | nil
end
with_them do
@@ -262,5 +448,5 @@ RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, s
end
end
- it_behaves_like 'rejects Debian access with unknown container id'
+ it_behaves_like 'rejects Debian access with unknown container id', hidden_status
end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
new file mode 100644
index 00000000000..41a61ba5fd7
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a package detail' do
+ 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
+end
+
+RSpec.shared_examples 'a package with files' do
+ it 'has the right amount of files' do
+ expect(package_files_response.length).to be(package.package_files.length)
+ end
+
+ it 'has the basic package files data' do
+ expect(first_file_response).to include(
+ 'id' => global_id_of(first_file),
+ 'fileName' => first_file.file_name,
+ 'size' => first_file.size.to_s,
+ 'downloadPath' => first_file.download_path,
+ 'fileSha1' => first_file.file_sha1,
+ 'fileMd5' => first_file.file_md5,
+ 'fileSha256' => first_file.file_sha256
+ )
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
new file mode 100644
index 00000000000..585c4fb8a4e
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rejects helm packages access' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it_behaves_like 'returning response status', status
+
+ if status == :unauthorized
+ it 'has the correct response header' do
+ subject
+
+ expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'process helm download content request' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it_behaves_like 'returning response status', status
+
+ it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package'
+
+ it 'returns a valid package archive' do
+ subject
+
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects helm access with unknown project id' do
+ context 'with an unknown project' do
+ let(:project) { OpenStruct.new(id: 1234567890) }
+
+ context 'as anonymous' do
+ it_behaves_like 'rejects helm packages access', :anonymous, :unauthorized
+ end
+
+ context 'as authenticated user' do
+ subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
+
+ it_behaves_like 'rejects helm packages access', :anonymous, :not_found
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index eb86b7c37d5..42c29084d7b 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -146,6 +146,6 @@ RSpec.shared_examples 'a package tracking event' do |category, action|
it "creates a gitlab tracking event #{action}", :snowplow do
expect { subject }.to change { Packages::Event.count }.by(1)
- expect_snowplow_event(category: category, action: action)
+ expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context)
end
end
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index bbcf856350d..8a351226123 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -110,6 +110,7 @@ RSpec.shared_examples 'PyPI package versions' do |user_type, status, add_member
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it 'returns the package listing' do
@@ -127,6 +128,7 @@ RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it 'returns the package listing' do
@@ -144,24 +146,185 @@ RSpec.shared_examples 'process PyPI api request' do |user_type, status, add_memb
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it_behaves_like 'returning response status', status
end
end
+RSpec.shared_examples 'unknown PyPI scope id' do
+ context 'as anonymous' do
+ it_behaves_like 'process PyPI api request', :anonymous, :not_found
+ end
+
+ context 'as authenticated user' do
+ subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
+
+ it_behaves_like 'process PyPI api request', :anonymous, :not_found
+ end
+end
+
RSpec.shared_examples 'rejects PyPI access with unknown project id' do
context 'with an unknown project' do
let(:project) { OpenStruct.new(id: 1234567890) }
- context 'as anonymous' do
- it_behaves_like 'process PyPI api request', :anonymous, :not_found
+ it_behaves_like 'unknown PyPI scope id'
+ end
+end
+
+RSpec.shared_examples 'rejects PyPI access with unknown group id' do
+ context 'with an unknown project' do
+ let(:group) { OpenStruct.new(id: 1234567890) }
+
+ it_behaves_like 'unknown PyPI scope id'
+ end
+end
+
+RSpec.shared_examples 'pypi simple API endpoint' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | true | 'PyPI package versions' | :success
+ :public | :guest | true | true | 'PyPI package versions' | :success
+ :public | :developer | true | false | 'PyPI package versions' | :success
+ :public | :guest | true | false | 'PyPI package versions' | :success
+ :public | :developer | false | true | 'PyPI package versions' | :success
+ :public | :guest | false | true | 'PyPI package versions' | :success
+ :public | :developer | false | false | 'PyPI package versions' | :success
+ :public | :guest | false | false | 'PyPI package versions' | :success
+ :public | :anonymous | false | true | 'PyPI package versions' | :success
+ :private | :developer | true | true | 'PyPI package versions' | :success
+ :private | :guest | true | true | 'process PyPI api request' | :forbidden
+ :private | :developer | true | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | true | false | 'process PyPI api request' | :unauthorized
+ :private | :developer | false | true | 'process PyPI api request' | :not_found
+ :private | :guest | false | true | 'process PyPI api request' | :not_found
+ :private | :developer | false | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | false | false | 'process PyPI api request' | :unauthorized
+ :private | :anonymous | false | true | 'process PyPI api request' | :unauthorized
end
- context 'as authenticated user' do
- subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- it_behaves_like 'process PyPI api request', :anonymous, :not_found
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ context 'with a normalized package name' do
+ let_it_be(:package) { create(:pypi_package, project: project, name: 'my.package') }
+
+ let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" }
+ let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
+
+ it_behaves_like 'PyPI package versions', :developer, :success
+ end
+end
+
+RSpec.shared_examples 'pypi file download endpoint' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token) do
+ :public | :developer | true | true
+ :public | :guest | true | true
+ :public | :developer | true | false
+ :public | :guest | true | false
+ :public | :developer | false | true
+ :public | :guest | false | true
+ :public | :developer | false | false
+ :public | :guest | false | false
+ :public | :anonymous | false | true
+ :private | :developer | true | true
+ :private | :guest | true | true
+ :private | :developer | true | false
+ :private | :guest | true | false
+ :private | :developer | false | true
+ :private | :guest | false | true
+ :private | :developer | false | false
+ :private | :guest | false | false
+ :private | :anonymous | false | true
end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ end
+
+ it_behaves_like 'PyPI package download', params[:user_role], :success, params[:member]
+ end
+ end
+
+ context 'with deploy token headers' do
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) }
+
+ context 'valid token' do
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid token' do
+ let(:headers) { basic_auth_header('foo', 'bar') }
+
+ it_behaves_like 'returning response status', :success
+ end
+ end
+
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
+
+ context 'valid token' do
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ context 'invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token) }
+
+ it_behaves_like 'returning response status', :success
+ end
+ end
+end
+
+RSpec.shared_examples 'a pypi user namespace endpoint' do
+ using RSpec::Parameterized::TableSyntax
+
+ # only group namespaces are supported at this time
+ where(:visibility_level, :user_role, :expected_status) do
+ :public | :owner | :not_found
+ :private | :owner | :not_found
+ :public | :external | :not_found
+ :private | :external | :not_found
+ :public | :anonymous | :not_found
+ :private | :anonymous | :not_found
+ end
+
+ with_them do
+ let_it_be_with_reload(:group) { create(:namespace) }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
+
+ before do
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ group.update_column(:owner_id, user.id) if user_role == :owner
+ end
+
+ it_behaves_like 'returning response status', params[:expected_status]
end
end
diff --git a/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb
index 675b6c5cef6..2ac78131e08 100644
--- a/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'resource_label_events API' do |parent_type, eventable_typ
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user)
expect(json_response).to be_an Array
- expect(json_response).to eq []
+ expect(json_response).to be_empty
end
end
end
diff --git a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb
index 826139635ed..af13e3fc14d 100644
--- a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb
@@ -4,6 +4,6 @@ RSpec.shared_examples 'a gitlab tracking event' do |category, action|
it "creates a gitlab tracking event #{action}", :snowplow do
subject
- expect_snowplow_event(category: category, action: action)
+ expect_snowplow_event(category: category, action: action, **snowplow_standard_context_params)
end
end
diff --git a/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb
deleted file mode 100644
index 466300017d9..00000000000
--- a/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'parse cluster applications artifact' do |release_name|
- let(:application_class) { Clusters::Cluster::APPLICATIONS[release_name] }
- let(:cluster_application) { cluster.public_send("application_#{release_name}") }
- let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
- let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
-
- context 'release is missing' do
- let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_missing.json.gz" }
-
- context 'application does not exist' do
- it 'does not create or destroy an application' do
- expect do
- described_class.new(job, user).execute(artifact)
- end.not_to change(application_class, :count)
- end
- end
-
- context 'application exists' do
- before do
- create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster)
- end
-
- it 'marks the application as uninstalled' do
- described_class.new(job, user).execute(artifact)
-
- cluster_application.reload
- expect(cluster_application).to be_uninstalled
- end
- end
- end
-
- context 'release is deployed' do
- let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_deployed.json.gz" }
-
- context 'application does not exist' do
- it 'creates an application and marks it as installed' do
- expect do
- described_class.new(job, user).execute(artifact)
- end.to change(application_class, :count)
-
- expect(cluster_application).to be_persisted
- expect(cluster_application).to be_externally_installed
- end
- end
-
- context 'application exists' do
- before do
- create("clusters_applications_#{release_name}".to_sym, :errored, cluster: cluster)
- end
-
- it 'marks the application as installed' do
- described_class.new(job, user).execute(artifact)
-
- expect(cluster_application).to be_externally_installed
- end
- end
- end
-
- context 'release is failed' do
- let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_failed.json.gz" }
-
- context 'application does not exist' do
- it 'creates an application and marks it as errored' do
- expect do
- described_class.new(job, user).execute(artifact)
- end.to change(application_class, :count)
-
- expect(cluster_application).to be_persisted
- expect(cluster_application).to be_errored
- expect(cluster_application.status_reason).to eq('Helm release failed to install')
- end
- end
-
- context 'application exists' do
- before do
- create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster)
- end
-
- it 'marks the application as errored' do
- described_class.new(job, user).execute(artifact)
-
- expect(cluster_application).to be_errored
- expect(cluster_application.status_reason).to eq('Helm release failed to install')
- end
- end
- end
-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
new file mode 100644
index 00000000000..9ffeba1b1d0
--- /dev/null
+++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Generate Debian Distribution and component files' do
+ let_it_be(:component_main) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') }
+ let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') }
+
+ let_it_be(:architecture_all) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
+ let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
+ let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
+
+ let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated
+ let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed
+ let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation
+ let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation
+ let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation
+ let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago
+
+ def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content)
+ component_file = distribution
+ .component_files
+ .with_component_name(component_name)
+ .with_file_type(component_file_type)
+ .with_architecture_name(architecture_name)
+ .order_updated_asc
+ .last
+
+ expect(component_file).not_to be_nil
+ expect(component_file.updated_at).to eq(release_date)
+
+ unless expected_content.nil?
+ component_file.file.use_file do |file_path|
+ expect(File.read(file_path)).to eq(expected_content)
+ end
+ end
+ end
+
+ it 'generates Debian distribution and component files', :aggregate_failures do
+ current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456)
+
+ travel_to(current_time) do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ initial_count = 6
+ destroyed_count = 2
+ # updated_count = 1
+ created_count = 5
+
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::PackageFile.count }
+ .and change { distribution.reload.updated_at }.to(current_time.round)
+ .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count)
+ .and change { component_file1.reload.updated_at }.to(current_time.round)
+
+ debs = package.package_files.with_debian_file_type(:deb).preload_debian_file_metadata.to_a
+ pool_prefix = "pool/unstable/#{project.id}/p/#{package.name}"
+ expected_main_amd64_content = <<~EOF
+ Package: libsample0
+ Source: #{package.name}
+ Version: #{package.version}
+ Installed-Size: 7
+ Maintainer: #{debs[0].debian_fields['Maintainer']}
+ Architecture: amd64
+ Description: Some mostly empty lib
+ Used in GitLab tests.
+ .
+ Testing another paragraph.
+ Multi-Arch: same
+ Homepage: #{debs[0].debian_fields['Homepage']}
+ Section: libs
+ Priority: optional
+ Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb
+ Size: 409600
+ MD5sum: #{debs[0].file_md5}
+ SHA256: #{debs[0].file_sha256}
+
+ Package: sample-dev
+ Source: #{package.name} (#{package.version})
+ Version: 1.2.3~binary
+ Installed-Size: 7
+ Maintainer: #{debs[1].debian_fields['Maintainer']}
+ Architecture: amd64
+ Depends: libsample0 (= 1.2.3~binary)
+ Description: Some mostly empty development files
+ Used in GitLab tests.
+ .
+ Testing another paragraph.
+ Multi-Arch: same
+ Homepage: #{debs[1].debian_fields['Homepage']}
+ Section: libdevel
+ Priority: optional
+ Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb
+ Size: 409600
+ MD5sum: #{debs[1].file_md5}
+ SHA256: #{debs[1].file_sha256}
+ EOF
+
+ check_component_file(current_time.round, 'main', :packages, 'all', nil)
+ check_component_file(current_time.round, 'main', :packages, 'amd64', expected_main_amd64_content)
+ check_component_file(current_time.round, 'main', :packages, 'arm64', nil)
+
+ check_component_file(current_time.round, 'contrib', :packages, 'all', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'arm64', 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
+
+ expected_release_content = <<~EOF
+ Codename: unstable
+ Date: Sat, 25 Jan 2020 15:17:18 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ Architectures: all amd64 arm64
+ Components: contrib main
+ MD5Sum:
+ #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages
+ #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages
+ SHA256:
+ #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
+ #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
+ EOF
+
+ distribution.file.use_file do |file_path|
+ expect(File.read(file_path)).to eq(expected_release_content)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Generate minimal Debian Distribution' do
+ it 'generates minimal distribution', :aggregate_failures do
+ travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::PackageFile.count }
+ .and not_change { distribution.component_files.reset.count }
+
+ expected_release_content = <<~EOF
+ Codename: unstable
+ Date: Sat, 25 Jan 2020 15:17:18 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ MD5Sum:
+ SHA256:
+ EOF
+
+ distribution.file.use_file do |file_path|
+ expect(File.read(file_path)).to eq(expected_release_content)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/users/build_service_shared_examples.rb b/spec/support/shared_examples/services/users/build_service_shared_examples.rb
new file mode 100644
index 00000000000..6a8695e1786
--- /dev/null
+++ b/spec/support/shared_examples/services/users/build_service_shared_examples.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'common user build items' do
+ it { is_expected.to be_valid }
+
+ it 'sets the created_by_id' do
+ expect(user.created_by_id).to eq(current_user&.id)
+ end
+
+ it 'calls UpdateCanonicalEmailService' do
+ expect(Users::UpdateCanonicalEmailService).to receive(:new).and_call_original
+
+ user
+ end
+
+ context 'when user_type is provided' do
+ context 'when project_bot' do
+ before do
+ params.merge!({ user_type: :project_bot })
+ end
+
+ it { expect(user.project_bot?).to be true }
+ end
+
+ context 'when not a project_bot' do
+ before do
+ params.merge!({ user_type: :alert_bot })
+ end
+
+ it { expect(user).to be_human }
+ end
+ end
+end
+
+RSpec.shared_examples_for 'current user not admin build items' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with "user_default_external" application setting' do
+ where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do
+ true | nil | 'fl@example.com' | nil | true
+ true | true | 'fl@example.com' | nil | true
+ true | false | 'fl@example.com' | nil | true # admin difference
+
+ true | nil | 'fl@example.com' | '' | true
+ true | true | 'fl@example.com' | '' | true
+ true | false | 'fl@example.com' | '' | true # admin difference
+
+ true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
+ true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference
+ true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
+
+ true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true
+ true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true
+ true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference
+
+ false | nil | 'fl@example.com' | nil | false
+ false | true | 'fl@example.com' | nil | false # admin difference
+ false | false | 'fl@example.com' | nil | false
+
+ false | nil | 'fl@example.com' | '' | false
+ false | true | 'fl@example.com' | '' | false # admin difference
+ false | false | 'fl@example.com' | '' | false
+
+ false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
+ false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference
+ false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false
+
+ false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false
+ false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference
+ false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false
+ end
+
+ with_them do
+ before do
+ stub_application_setting(user_default_external: user_default_external)
+ stub_application_setting(user_default_internal_regex: user_default_internal_regex)
+
+ params.merge!({ external: external, email: email }.compact)
+ end
+
+ it 'sets the value of Gitlab::CurrentSettings.user_default_external' do
+ expect(user.external).to eq(result)
+ end
+ end
+ end
+
+ context 'when "send_user_confirmation_email" application setting is true' do
+ before do
+ stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true)
+ end
+
+ it 'does not confirm the user' do
+ expect(user).not_to be_confirmed
+ end
+ end
+
+ context 'when "send_user_confirmation_email" application setting is false' do
+ before do
+ stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true)
+ end
+
+ it 'confirms the user' do
+ expect(user).to be_confirmed
+ end
+ end
+
+ context 'with allowed params' do
+ let(:params) do
+ {
+ email: 1,
+ name: 1,
+ password: 1,
+ password_automatically_set: 1,
+ username: 1,
+ user_type: 'project_bot'
+ }
+ end
+
+ it 'sets all allowed attributes' do
+ expect(User).to receive(:new).with(hash_including(params)).and_call_original
+
+ user
+ end
+ end
+end
diff --git a/spec/support/shared_examples/uncached_response_shared_examples.rb b/spec/support/shared_examples/uncached_response_shared_examples.rb
deleted file mode 100644
index 3997017ff35..00000000000
--- a/spec/support/shared_examples/uncached_response_shared_examples.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-#
-# Pairs with lib/gitlab/no_cache_headers.rb
-#
-
-RSpec.shared_examples 'uncached response' do
- it 'defines an uncached header response' do
- expect(response.headers["Cache-Control"]).to include("no-store", "no-cache")
- expect(response.headers["Pragma"]).to eq("no-cache")
- expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
- end
-end
diff --git a/spec/support/unicorn.rb b/spec/support/unicorn.rb
deleted file mode 100644
index 0b01fc9e26c..00000000000
--- a/spec/support/unicorn.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-REQUEST_CLASSES = [
- ::Grape::Request,
- ::Rack::Request
-].freeze
-
-def request_body_class
- return ::Unicorn::TeeInput if defined?(::Unicorn)
-
- Class.new(StringIO) do
- def string
- raise NotImplementedError, '#string is only valid under Puma which uses StringIO, use #read instead'
- end
- end
-end
-
-RSpec.configure do |config|
- config.before(:each, :unicorn) do
- REQUEST_CLASSES.each do |request_class|
- allow_any_instance_of(request_class)
- .to receive(:body).and_wrap_original do |m, *args|
- request_body_class.new(m.call(*args).read)
- end
- end
- end
-end
diff --git a/spec/tasks/admin_mode_spec.rb b/spec/tasks/admin_mode_spec.rb
index 9dd35650ab6..116d47aa503 100644
--- a/spec/tasks/admin_mode_spec.rb
+++ b/spec/tasks/admin_mode_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'admin mode on tasks' do
+RSpec.describe 'admin mode on tasks', :silence_stdout do
before do
allow(::Gitlab::Runtime).to receive(:test_suite?).and_return(false)
allow(::Gitlab::Runtime).to receive(:rake?).and_return(true)
diff --git a/spec/tasks/cache/clear/redis_spec.rb b/spec/tasks/cache/clear/redis_spec.rb
index d2de068f254..64ed83c649b 100644
--- a/spec/tasks/cache/clear/redis_spec.rb
+++ b/spec/tasks/cache/clear/redis_spec.rb
@@ -2,11 +2,15 @@
require 'rake_helper'
-RSpec.describe 'clearing redis cache' do
+RSpec.describe 'clearing redis cache', :clean_gitlab_redis_cache, :silence_stdout do
before do
Rake.application.rake_require 'tasks/cache'
end
+ shared_examples 'clears the cache' do
+ it { expect { run_rake_task('cache:clear:redis') }.to change { redis_keys.size }.by(-1) }
+ end
+
describe 'clearing pipeline status cache' do
let(:pipeline_status) do
project = create(:project, :repository)
@@ -20,5 +24,38 @@ RSpec.describe 'clearing redis cache' do
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'
+ end
+
+ describe 'clearing set caches' do
+ context 'repository set' do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ let(:cache) { Gitlab::RepositorySetCache.new(repository) }
+
+ before do
+ pending "Enable as part of https://gitlab.com/gitlab-org/gitlab/-/issues/331319"
+
+ cache.write(:foo, [:bar])
+ end
+
+ it_behaves_like 'clears the cache'
+ end
+
+ context 'reactive cache set' do
+ let(:cache) { Gitlab::ReactiveCacheSetCache.new }
+
+ before do
+ cache.write(:foo, :bar)
+ end
+
+ it_behaves_like 'clears the cache'
+ end
+ end
+
+ def redis_keys
+ Gitlab::Redis::Cache.with { |redis| redis.scan(0, match: "*") }.last
end
end
diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb
index f5a382989ea..34899c84888 100644
--- a/spec/tasks/config_lint_spec.rb
+++ b/spec/tasks/config_lint_spec.rb
@@ -3,7 +3,7 @@
require 'rake_helper'
Rake.application.rake_require 'tasks/config_lint'
-RSpec.describe ConfigLint do
+RSpec.describe ConfigLint, :silence_stdout do
let(:files) { ['lib/support/fake.sh'] }
it 'errors out if any bash scripts have errors' do
@@ -15,7 +15,7 @@ RSpec.describe ConfigLint do
end
end
-RSpec.describe 'config_lint rake task' do
+RSpec.describe 'config_lint rake task', :silence_stdout do
before do
# Prevent `system` from actually being called
allow(Kernel).to receive(:system).and_return(true)
diff --git a/spec/tasks/gettext_rake_spec.rb b/spec/tasks/gettext_rake_spec.rb
index a535ac92a75..29caa363f7b 100644
--- a/spec/tasks/gettext_rake_spec.rb
+++ b/spec/tasks/gettext_rake_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require "rake_helper"
+require 'rake_helper'
-RSpec.describe 'gettext' do
+RSpec.describe 'gettext', :silence_stdout do
let(:locale_path) { Rails.root.join('tmp/gettext_spec') }
let(:pot_file_path) { File.join(locale_path, 'gitlab.pot') }
diff --git a/spec/tasks/gitlab/artifacts/check_rake_spec.rb b/spec/tasks/gitlab/artifacts/check_rake_spec.rb
index d1d02ab9bc5..e0303170755 100644
--- a/spec/tasks/gitlab/artifacts/check_rake_spec.rb
+++ b/spec/tasks/gitlab/artifacts/check_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:artifacts rake tasks' do
+RSpec.describe 'gitlab:artifacts rake tasks', :silence_stdout do
describe 'check' do
let!(:artifact) { create(:ci_job_artifact, :archive, :correct_checksum) }
diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
index 94a8da9478a..25a3723fbaa 100644
--- a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:artifacts namespace rake task' do
+RSpec.describe 'gitlab:artifacts namespace rake task', :silence_stdout do
before(:context) do
Rake.application.rake_require 'tasks/gitlab/artifacts/migrate'
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 8963164ac53..e5a210bb344 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
-require 'rake'
+require 'rake_helper'
RSpec.describe 'gitlab:app namespace rake task', :delete do
let(:enable_registry) { true }
@@ -24,14 +23,10 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
before(:all) do
Rake.application.rake_require 'active_record/railties/databases'
- Rake.application.rake_require 'tasks/gitlab/helpers'
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'
Rake.application.rake_require 'tasks/gitlab/db'
Rake.application.rake_require 'tasks/cache'
-
- # empty task as env is already loaded
- Rake::Task.define_task :environment
end
before do
@@ -39,6 +34,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
FileUtils.rm(tars_glob, force: true)
FileUtils.rm(backup_files, force: true)
FileUtils.rm_rf(backup_directories, secure: true)
+ FileUtils.mkdir_p('tmp/tests/public/uploads')
reenable_backup_sub_tasks
stub_container_registry_config(enabled: enable_registry)
end
@@ -47,12 +43,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
FileUtils.rm(tars_glob, force: true)
FileUtils.rm(backup_files, force: true)
FileUtils.rm_rf(backup_directories, secure: true)
- end
-
- def run_rake_task(task_name)
- FileUtils.mkdir_p('tmp/tests/public/uploads')
- Rake::Task[task_name].reenable
- Rake.application.invoke_task task_name
+ FileUtils.rm_rf('tmp/tests/public/uploads', secure: true)
end
def reenable_backup_sub_tasks
@@ -92,11 +83,11 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'invokes restoration on match' do
- expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
end
it 'prints timestamps on messages' do
- expect { run_rake_task('gitlab:backup:restore') }.to output(/.*\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\s[-+]\d{4}\s--\s.*/).to_stdout
+ expect { run_rake_task('gitlab:backup:restore') }.to output(/.*\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\s[-+]\d{4}\s--\s.*/).to_stdout_from_any_process
end
end
end
@@ -105,14 +96,16 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
before do
# We only need a backup of the repositories for this test
stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
+
+ create(:project, :repository)
end
it 'removes stale data' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
excluded_project = create(:project, :repository, name: 'mepmep')
- expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
raw_repo = excluded_project.repository.raw
@@ -124,9 +117,10 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
context 'when the backup is restored' do
let!(:included_project) { create(:project, :repository) }
+ let!(:original_checksum) { included_project.repository.checksum }
before do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
backup_tar = Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')).last
allow(Dir).to receive(:glob).and_return([backup_tar])
@@ -153,11 +147,12 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'restores the data' do
- expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
raw_repo = included_project.repository.raw
expect(raw_repo.empty?).to be(false)
+ expect(included_project.repository.checksum).to eq(original_checksum)
end
end
end
@@ -169,8 +164,9 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
allow(ActiveRecord::Base.connection).to receive(:reconnect!)
end
+ let!(:project) { create(:project, :repository) }
+
describe 'backup creation and deletion using custom_hooks' do
- let(:project) { create(:project, :repository) }
let(:user_backup_path) { "repositories/#{project.disk_path}" }
before do
@@ -184,7 +180,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
context 'project uses custom_hooks and successfully creates backup' do
it 'creates custom_hooks.tar and project bundle' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{backup_tar}})
@@ -195,8 +191,8 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'restores files correctly' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
- expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
project.repository.path
@@ -210,7 +206,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
it 'prints a progress message to stdout' do
task_list.each do |task|
- expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout
+ expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout_from_any_process
end
end
end
@@ -219,7 +215,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
context 'tar creation' do
context 'archive file permissions' do
it 'sets correct permissions on the tar file' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
expect(File.exist?(backup_tar)).to be_truthy
expect(File::Stat.new(backup_tar).mode.to_s(8)).to eq('100600')
@@ -231,7 +227,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'uses the custom permissions' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
expect(File::Stat.new(backup_tar).mode.to_s(8)).to eq('100651')
end
@@ -239,7 +235,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'sets correct permissions on the tar contents' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
@@ -258,7 +254,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'deletes temp directories' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
temp_dirs = Dir.glob(
File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}')
@@ -271,7 +267,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
let(:enable_registry) { false }
it 'does not create registry.tar.gz' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{backup_tar}}
@@ -301,10 +297,8 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
shared_examples 'includes repositories in all repository storages' do
specify :aggregate_failures do
project_a = create(:project, :repository)
- project_a.track_project_repository
project_snippet_a = create(:project_snippet, :repository, project: project_a, author: project_a.owner)
project_b = create(:project, :repository, repository_storage: second_storage_name)
- project_b.track_project_repository
project_snippet_b = create(:project_snippet, :repository, project: project_b, author: project_b.owner)
project_snippet_b.snippet_repository.update!(shard: project_b.project_repository.shard)
create(:wiki_page, container: project_a)
@@ -313,7 +307,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
move_repository_to_secondary(project_b)
move_repository_to_secondary(project_snippet_b)
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{backup_tar} repositories}
@@ -379,7 +373,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
.and_call_original
end
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
end
it 'passes through concurrency environment variables' do
@@ -392,7 +386,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
.and_call_original
end
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
end
end
end
@@ -401,10 +395,12 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
describe "Skipping items" do
before do
stub_env('SKIP', 'repositories,uploads')
+
+ create(:project, :repository)
end
it "does not contain skipped item" do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
tar_contents, _exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
@@ -421,7 +417,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'does not invoke repositories restore' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
allow(Rake::Task['gitlab:shell:setup'])
.to receive(:invoke).and_return(true)
@@ -436,17 +432,19 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
- expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout_from_any_process
end
end
describe 'skipping tar archive creation' do
before do
stub_env('SKIP', 'tar')
+
+ create(:project, :repository)
end
it 'created files with backup content and no tar archive' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
dir_contents = Dir.children(Gitlab.config.backup.path)
@@ -465,7 +463,7 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
end
it 'those component files can be restored from' do
- expect { run_rake_task("gitlab:backup:create") }.to output.to_stdout
+ expect { run_rake_task("gitlab:backup:create") }.to output.to_stdout_from_any_process
allow(Rake::Task['gitlab:shell:setup'])
.to receive(:invoke).and_return(true)
@@ -480,13 +478,13 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
- expect { run_rake_task("gitlab:backup:restore") }.to output.to_stdout
+ expect { run_rake_task("gitlab:backup:restore") }.to output.to_stdout_from_any_process
end
end
describe "Human Readable Backup Name" do
it 'name has human readable time' do
- expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
+ expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout_from_any_process
expect(backup_tar).to match(/\d+_\d{4}_\d{2}_\d{2}_\d+\.\d+\.\d+.*_gitlab_backup.tar$/)
end
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
index 7956b5c57e6..aee03059120 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' do
+RSpec.describe 'check.rake', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/check'
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
index 08d8651dcef..16c907ca87c 100644
--- a/spec/tasks/gitlab/cleanup_rake_spec.rb
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:cleanup rake tasks' do
+RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/cleanup'
end
diff --git a/spec/tasks/gitlab/container_registry_rake_spec.rb b/spec/tasks/gitlab/container_registry_rake_spec.rb
index b83ff567126..f19e93fc6cb 100644
--- a/spec/tasks/gitlab/container_registry_rake_spec.rb
+++ b/spec/tasks/gitlab/container_registry_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:container_registry namespace rake tasks' do
+RSpec.describe 'gitlab:container_registry namespace rake tasks', :silence_stdout do
let_it_be(:api_url) { 'http://registry.gitlab' }
before :all do
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index c4623061944..08ca6c32b49 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' do
+RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
before :all do
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
@@ -306,7 +306,7 @@ RSpec.describe 'gitlab:db namespace rake task' do
let(:all_migrations) { [double('migration1', version: 1), pending_migration] }
let(:pending_migration) { double('migration2', version: 2) }
let(:filename) { Gitlab::Database::Migrations::Instrumentation::STATS_FILENAME }
- let!(:directory) { Dir.mktmpdir }
+ let(:result_dir) { Dir.mktmpdir }
let(:observations) { %w[some data] }
before do
@@ -316,19 +316,17 @@ RSpec.describe 'gitlab:db namespace rake task' do
allow(instrumentation).to receive(:observe).and_yield
- allow(Dir).to receive(:mkdir)
- allow(File).to receive(:exist?).with(directory).and_return(false)
- stub_const('Gitlab::Database::Migrations::Instrumentation::RESULT_DIR', directory)
+ stub_const('Gitlab::Database::Migrations::Instrumentation::RESULT_DIR', result_dir)
end
after do
- FileUtils.rm_rf([directory])
+ FileUtils.rm_rf(result_dir)
end
- it 'fails when the directory already exists' do
- expect(File).to receive(:exist?).with(directory).and_return(true)
+ it 'creates result directory when one does not exist' do
+ FileUtils.rm_rf(result_dir)
- expect { subject }.to raise_error(/Directory exists/)
+ expect { subject }.to change { Dir.exist?(result_dir) }.from(false).to(true)
end
it 'instruments the pending migration' do
@@ -346,7 +344,7 @@ RSpec.describe 'gitlab:db namespace rake task' do
it 'writes observations out to JSON file' do
subject
- expect(File.read(File.join(directory, filename))).to eq(observations.to_json)
+ expect(File.read(File.join(result_dir, filename))).to eq(observations.to_json)
end
end
diff --git a/spec/tasks/gitlab/external_diffs_rake_spec.rb b/spec/tasks/gitlab/external_diffs_rake_spec.rb
index 66e555734b3..86242de4b90 100644
--- a/spec/tasks/gitlab/external_diffs_rake_spec.rb
+++ b/spec/tasks/gitlab/external_diffs_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:external_diffs rake tasks' do
+RSpec.describe 'gitlab:external_diffs rake tasks', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/external_diffs'
end
diff --git a/spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb b/spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb
index a8effef2d7b..67bf512c6da 100644
--- a/spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb
+++ b/spec/tasks/gitlab/generate_sample_prometheus_data_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:generate_sample_prometheus_data rake task' do
+RSpec.describe 'gitlab:generate_sample_prometheus_data rake task', :silence_stdout do
let(:cluster) { create(:cluster, :provided_by_user, :project) }
let(:environment) { create(:environment, project: cluster.project) }
let(:sample_query_file) { File.join(Rails.root, Metrics::SampleMetricsService::DIRECTORY, 'test_query_result.yml') }
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index 50ec2632d83..aab927a472e 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:git rake tasks' do
+RSpec.describe 'gitlab:git rake tasks', :silence_stdout do
let(:base_path) { 'tmp/tests/default_storage' }
let!(:project) { create(:project, :repository) }
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 1b38580f484..5adea832995 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:gitaly namespace rake task' do
+RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
before :all do
Rake.application.rake_require 'tasks/gitlab/gitaly'
end
diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb
index 941f3429017..19ed43723e2 100644
--- a/spec/tasks/gitlab/info_rake_spec.rb
+++ b/spec/tasks/gitlab/info_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:env:info' do
+RSpec.describe 'gitlab:env:info', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/info'
diff --git a/spec/tasks/gitlab/ldap_rake_spec.rb b/spec/tasks/gitlab/ldap_rake_spec.rb
index 5286cd98944..693bb7826a3 100644
--- a/spec/tasks/gitlab/ldap_rake_spec.rb
+++ b/spec/tasks/gitlab/ldap_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:ldap:rename_provider rake task' do
+RSpec.describe 'gitlab:ldap:rename_provider rake task', :silence_stdout do
it 'completes without error' do
Rake.application.rake_require 'tasks/gitlab/ldap'
stub_warn_user_is_not_gitlab
@@ -93,8 +93,8 @@ RSpec.describe 'gitlab:ldap:secret rake tasks' do
describe 'write' do
before do
- allow(STDIN).to receive(:tty?).and_return(false)
- allow(STDIN).to receive(:read).and_return('testvalue')
+ allow($stdin).to receive(:tty?).and_return(false)
+ allow($stdin).to receive(:read).and_return('testvalue')
end
it 'creates encrypted file from stdin' do
diff --git a/spec/tasks/gitlab/lfs/check_rake_spec.rb b/spec/tasks/gitlab/lfs/check_rake_spec.rb
index fd1b6d010e4..ce0076826c4 100644
--- a/spec/tasks/gitlab/lfs/check_rake_spec.rb
+++ b/spec/tasks/gitlab/lfs/check_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:lfs rake tasks' do
+RSpec.describe 'gitlab:lfs rake tasks', :silence_stdout do
describe 'check' do
let!(:lfs_object) { create(:lfs_object, :with_file, :correct_oid) }
diff --git a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
index a59da615b94..3b571507bac 100644
--- a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:lfs namespace rake task' do
+RSpec.describe 'gitlab:lfs namespace rake task', :silence_stdout do
before :all do
Rake.application.rake_require 'tasks/gitlab/lfs/migrate'
end
diff --git a/spec/tasks/gitlab/packages/composer_rake_spec.rb b/spec/tasks/gitlab/packages/composer_rake_spec.rb
index d54e1b02599..78013714de5 100644
--- a/spec/tasks/gitlab/packages/composer_rake_spec.rb
+++ b/spec/tasks/gitlab/packages/composer_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:packages:build_composer_cache namespace rake task' do
+RSpec.describe 'gitlab:packages:build_composer_cache namespace rake task', :silence_stdout do
let_it_be(:package_name) { 'sample-project' }
let_it_be(:package_name2) { 'sample-project2' }
let_it_be(:json) { { 'name' => package_name } }
diff --git a/spec/tasks/gitlab/packages/events_rake_spec.rb b/spec/tasks/gitlab/packages/events_rake_spec.rb
index d6aa6deac1e..87f4db360ca 100644
--- a/spec/tasks/gitlab/packages/events_rake_spec.rb
+++ b/spec/tasks/gitlab/packages/events_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:packages:events namespace rake task' do
+RSpec.describe 'gitlab:packages:events namespace rake task', :silence_stdout do
before :all do
Rake.application.rake_require 'tasks/gitlab/packages/events'
end
diff --git a/spec/tasks/gitlab/packages/migrate_rake_spec.rb b/spec/tasks/gitlab/packages/migrate_rake_spec.rb
index 618ff215c74..bf34034ee57 100644
--- a/spec/tasks/gitlab/packages/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/packages/migrate_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:packages namespace rake task' do
+RSpec.describe 'gitlab:packages namespace rake task', :silence_stdout do
before :all do
Rake.application.rake_require 'tasks/gitlab/packages/migrate'
end
diff --git a/spec/tasks/gitlab/pages_rake_spec.rb b/spec/tasks/gitlab/pages_rake_spec.rb
index 664899c361b..d4bfcafa7b4 100644
--- a/spec/tasks/gitlab/pages_rake_spec.rb
+++ b/spec/tasks/gitlab/pages_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:pages' do
+RSpec.describe 'gitlab:pages', :silence_stdout do
before(:context) do
Rake.application.rake_require 'tasks/gitlab/pages'
end
diff --git a/spec/tasks/gitlab/password_rake_spec.rb b/spec/tasks/gitlab/password_rake_spec.rb
index d5320f3b4af..65bba836024 100644
--- a/spec/tasks/gitlab/password_rake_spec.rb
+++ b/spec/tasks/gitlab/password_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:password rake tasks' do
+RSpec.describe 'gitlab:password rake tasks', :silence_stdout do
let_it_be(:user_1) { create(:user, username: 'foobar', password: 'initial_password') }
def stub_username(username)
diff --git a/spec/tasks/gitlab/praefect_rake_spec.rb b/spec/tasks/gitlab/praefect_rake_spec.rb
index c67dba110c7..85e655ed72c 100644
--- a/spec/tasks/gitlab/praefect_rake_spec.rb
+++ b/spec/tasks/gitlab/praefect_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:praefect:replicas' do
+RSpec.describe 'gitlab:praefect:replicas', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/praefect'
end
diff --git a/spec/tasks/gitlab/seed/group_seed_rake_spec.rb b/spec/tasks/gitlab/seed/group_seed_rake_spec.rb
index 0b69615eebc..2f57e875f5f 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' do
+RSpec.describe 'gitlab:seed:group_seed rake task', :silence_stdout do
let(:username) { 'group_seed' }
let!(:user) { create(:user, username: username) }
let(:task_params) { [2, username] }
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index a929daddb67..52a9738fb51 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:shell rake tasks' do
+RSpec.describe 'gitlab:shell rake tasks', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/shell'
diff --git a/spec/tasks/gitlab/sidekiq_rake_spec.rb b/spec/tasks/gitlab/sidekiq_rake_spec.rb
index 61a8aecfa61..75f904389e2 100644
--- a/spec/tasks/gitlab/sidekiq_rake_spec.rb
+++ b/spec/tasks/gitlab/sidekiq_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'sidekiq.rake', :aggregate_failures do
+RSpec.describe 'sidekiq.rake', :aggregate_failures, :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/sidekiq'
diff --git a/spec/tasks/gitlab/snippets_rake_spec.rb b/spec/tasks/gitlab/snippets_rake_spec.rb
index f21922e14b8..d40b784b3a0 100644
--- a/spec/tasks/gitlab/snippets_rake_spec.rb
+++ b/spec/tasks/gitlab/snippets_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:snippets namespace rake task' do
+RSpec.describe 'gitlab:snippets namespace rake task', :silence_stdout do
let_it_be(:user) { create(:user)}
let_it_be(:migrated) { create(:personal_snippet, :repository, author: user) }
let(:non_migrated) { create_list(:personal_snippet, 3, author: user) }
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index abd44adbdcc..fefcd05af3b 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:*' do
+RSpec.describe 'rake gitlab:storage:*', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/storage'
@@ -88,6 +88,27 @@ RSpec.describe 'rake gitlab:storage:*' do
end
end
+ shared_examples 'wait until database is ready' do
+ it 'checks if the database is ready once' do
+ expect(Gitlab::Database).to receive(:exists?).once
+
+ run_rake_task(task)
+ end
+
+ context 'handles custom env vars' do
+ before do
+ stub_env('MAX_DATABASE_CONNECTION_CHECKS' => 3)
+ stub_env('MAX_DATABASE_CONNECTION_INTERVAL' => 0.1)
+ end
+
+ it 'tries for 3 times, polling every 0.1 seconds' do
+ expect(Gitlab::Database).to receive(:exists?).exactly(3).times.and_return(false)
+
+ run_rake_task(task)
+ end
+ end
+ end
+
describe 'gitlab:storage:migrate_to_hashed' do
let(:task) { 'gitlab:storage:migrate_to_hashed' }
@@ -198,6 +219,10 @@ RSpec.describe 'rake gitlab:storage:*' do
let(:task) { 'gitlab:storage:legacy_projects' }
let(:create_collection) { create_list(:project, 3, :legacy_storage) }
end
+
+ it_behaves_like 'wait until database is ready' do
+ let(:task) { 'gitlab:storage:legacy_projects' }
+ end
end
describe 'gitlab:storage:list_legacy_projects' do
@@ -227,6 +252,10 @@ RSpec.describe 'rake gitlab:storage:*' do
let(:project) { create(:project, storage_version: 1) }
let(:create_collection) { create_list(:upload, 3, model: project) }
end
+
+ it_behaves_like 'wait until database is ready' do
+ let(:task) { 'gitlab:storage:legacy_attachments' }
+ end
end
describe 'gitlab:storage:list_legacy_attachments' do
diff --git a/spec/tasks/gitlab/terraform/migrate_rake_spec.rb b/spec/tasks/gitlab/terraform/migrate_rake_spec.rb
index 4188521df8e..8d911010a2e 100644
--- a/spec/tasks/gitlab/terraform/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/terraform/migrate_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:terraform_states' do
+RSpec.describe 'gitlab:terraform_states', :silence_stdout do
let_it_be(:version) { create(:terraform_state_version) }
let(:logger) { instance_double(Logger) }
@@ -13,7 +13,7 @@ RSpec.describe 'gitlab:terraform_states' do
end
before do
- allow(Logger).to receive(:new).with(STDOUT).and_return(logger)
+ allow(Logger).to receive(:new).with($stdout).and_return(logger)
end
describe 'gitlab:terraform_states:migrate' do
diff --git a/spec/tasks/gitlab/update_templates_rake_spec.rb b/spec/tasks/gitlab/update_templates_rake_spec.rb
index 25151a2f3ae..7eccdf22a1f 100644
--- a/spec/tasks/gitlab/update_templates_rake_spec.rb
+++ b/spec/tasks/gitlab/update_templates_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:update_project_templates rake task' do
+RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout do
let!(:tmpdir) { Dir.mktmpdir }
before do
diff --git a/spec/tasks/gitlab/uploads/check_rake_spec.rb b/spec/tasks/gitlab/uploads/check_rake_spec.rb
index 4c6f16dbcf9..b3efe33c549 100644
--- a/spec/tasks/gitlab/uploads/check_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/check_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:uploads rake tasks' do
+RSpec.describe 'gitlab:uploads rake tasks', :silence_stdout do
describe 'check' do
let!(:upload) { create(:upload, path: Rails.root.join('spec/fixtures/banana_sample.gif')) }
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index 7f0f5c6767d..e293271ca67 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:uploads:migrate and migrate_to_local rake tasks' do
+RSpec.describe 'gitlab:uploads:migrate and migrate_to_local rake tasks', :silence_stdout do
let(:model_class) { nil }
let(:uploader_class) { nil }
let(:mounted_as) { nil }
diff --git a/spec/tasks/gitlab/usage_data_rake_spec.rb b/spec/tasks/gitlab/usage_data_rake_spec.rb
index 84269568b8b..acaf9b5729b 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' do
+RSpec.describe 'gitlab:usage data take tasks', :silence_stdout do
include UsageDataHelpers
before do
diff --git a/spec/tasks/gitlab/user_management_rake_spec.rb b/spec/tasks/gitlab/user_management_rake_spec.rb
index 958055780d0..b13b004aaa4 100644
--- a/spec/tasks/gitlab/user_management_rake_spec.rb
+++ b/spec/tasks/gitlab/user_management_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:user_management tasks' do
+RSpec.describe 'gitlab:user_management tasks', :silence_stdout do
before do
Rake.application.rake_require 'tasks/gitlab/user_management'
end
diff --git a/spec/tasks/gitlab/web_hook_rake_spec.rb b/spec/tasks/gitlab/web_hook_rake_spec.rb
index 9f373e3a20a..2c582dc78f8 100644
--- a/spec/tasks/gitlab/web_hook_rake_spec.rb
+++ b/spec/tasks/gitlab/web_hook_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:web_hook namespace rake tasks' do
+RSpec.describe 'gitlab:web_hook namespace rake tasks', :silence_stdout do
let_it_be(:group, refind: true) { create(:group) }
let_it_be(:project1, reload: true) { create(:project, namespace: group) }
let_it_be(:project2, reload: true) { create(:project, namespace: group) }
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
index 0757f6ca015..6b5985a2a8a 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' do
+RSpec.describe 'gitlab:workhorse namespace rake task', :silence_stdout do
before :all do
Rake.application.rake_require 'tasks/gitlab/workhorse'
end
diff --git a/spec/tasks/gitlab/x509/update_rake_spec.rb b/spec/tasks/gitlab/x509/update_rake_spec.rb
index b166e73935a..dca4f07cda7 100644
--- a/spec/tasks/gitlab/x509/update_rake_spec.rb
+++ b/spec/tasks/gitlab/x509/update_rake_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'gitlab:x509 namespace rake task' do
+RSpec.describe 'gitlab:x509 namespace rake task', :silence_stdout do
before :all do
Rake.application.rake_require 'tasks/gitlab/x509/update'
end
diff --git a/spec/tasks/migrate/schema_check_rake_spec.rb b/spec/tasks/migrate/schema_check_rake_spec.rb
index aebb3a6551f..1b60b63ad84 100644
--- a/spec/tasks/migrate/schema_check_rake_spec.rb
+++ b/spec/tasks/migrate/schema_check_rake_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'rake'
-RSpec.describe 'schema_version_check rake task' do
+RSpec.describe 'schema_version_check rake task', :silence_stdout do
include StubENV
before :all do
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 13fcd37b426..3f7271d4be1 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -2,7 +2,7 @@
require 'rake_helper'
-RSpec.describe 'tokens rake tasks' do
+RSpec.describe 'tokens rake tasks', :silence_stdout do
let!(:user) { create(:user) }
before do
diff --git a/spec/tooling/danger/changelog_spec.rb b/spec/tooling/danger/changelog_spec.rb
index 7637d894265..5777186cc28 100644
--- a/spec/tooling/danger/changelog_spec.rb
+++ b/spec/tooling/danger/changelog_spec.rb
@@ -18,136 +18,52 @@ RSpec.describe Tooling::Danger::Changelog do
allow(changelog).to receive(:project_helper).and_return(fake_project_helper)
end
- describe '#check_changelog_trailer' do
- subject { changelog.check_changelog_trailer(commit) }
-
- context "when commit doesn't include a changelog trailer" do
- let(:commit) { double('commit', message: "Hello world") }
-
- it { is_expected.to be_nil }
- end
-
- context "when commit include a changelog trailer with no category" do
- let(:commit) { double('commit', message: "Hello world\n\nChangelog:") }
+ describe '#check_changelog_commit_categories' do
+ context 'when all changelog commits are correct' do
+ it 'does not produce any messages' do
+ commit = double(:commit, message: "foo\nChangelog: fixed")
- it { is_expected.to be_nil }
- end
+ allow(changelog).to receive(:changelog_commits).and_return([commit])
- context "when commit include a changelog trailer with an unknown category" do
- let(:commit) { double('commit', message: "Hello world\n\nChangelog: foo", sha: "abc123") }
-
- it { is_expected.to have_attributes(errors: ["Commit #{commit.sha} uses an invalid changelog category: foo"]) }
- end
-
- described_class::CATEGORIES.each do |category|
- context "when commit include a changelog trailer with category set to '#{category}'" do
- let(:commit) { double('commit', message: "Hello world\n\nChangelog: #{category}", sha: "abc123") }
+ expect(changelog).not_to receive(:fail)
- it { is_expected.to have_attributes(errors: []) }
+ changelog.check_changelog_commit_categories
end
end
- end
-
- describe '#check_changelog_yaml' do
- let(:changelog_path) { 'ee/changelogs/unreleased/entry.yml' }
- let(:changes) { changes_class.new([change_class.new(changelog_path, :added, :changelog)]) }
- let(:yaml_title) { 'Fix changelog Dangerfile to convert MR IID to a string before comparison' }
- let(:yaml_merge_request) { 60899 }
- let(:mr_iid) { '60899' }
- let(:yaml_type) { 'fixed' }
- let(:yaml) do
- <<~YAML
- ---
- title: #{yaml_title}
- merge_request: #{yaml_merge_request}
- author:
- type: #{yaml_type}
- YAML
- end
-
- before do
- allow(changelog).to receive(:present?).and_return(true)
- allow(changelog).to receive(:changelog_path).and_return(changelog_path)
- allow(changelog).to receive(:read_file).with(changelog_path).and_return(yaml)
- allow(fake_helper).to receive(:security_mr?).and_return(false)
- allow(fake_helper).to receive(:mr_iid).and_return(mr_iid)
- allow(fake_helper).to receive(:cherry_pick_mr?).and_return(false)
- allow(fake_helper).to receive(:stable_branch?).and_return(false)
- allow(fake_helper).to receive(:html_link).with(changelog_path).and_return(changelog_path)
- end
-
- subject { changelog.check_changelog_yaml }
-
- context "when changelog is not present" do
- before do
- allow(changelog).to receive(:present?).and_return(false)
- end
- it { is_expected.to have_attributes(errors: [], warnings: [], markdowns: [], messages: []) }
- end
+ context 'when a commit has an incorrect trailer' do
+ it 'adds a message' do
+ commit = double(:commit, message: "foo\nChangelog: foo", sha: '123')
- context "when YAML is invalid" do
- let(:yaml) { '{ foo bar]' }
+ allow(changelog).to receive(:changelog_commits).and_return([commit])
- it { is_expected.to have_attributes(errors: ["#{changelog_path} isn't valid YAML! #{described_class::SEE_DOC}"]) }
- end
+ expect(changelog).to receive(:fail)
- context "when a StandardError is raised" do
- before do
- allow(changelog).to receive(:read_file).and_raise(StandardError, "Fail!")
+ changelog.check_changelog_commit_categories
end
-
- it { is_expected.to have_attributes(warnings: ["There was a problem trying to check the Changelog. Exception: StandardError - Fail!"]) }
- end
-
- context "when YAML title is nil" do
- let(:yaml_title) { '' }
-
- it { is_expected.to have_attributes(errors: ["`title` should be set, in #{changelog_path}! #{described_class::SEE_DOC}"]) }
- end
-
- context "when YAML type is nil" do
- let(:yaml_type) { '' }
-
- it { is_expected.to have_attributes(errors: ["`type` should be set, in #{changelog_path}! #{described_class::SEE_DOC}"]) }
end
+ end
- context "when on a security MR" do
- let(:yaml_merge_request) { '' }
+ describe '#check_changelog_trailer' do
+ subject { changelog.check_changelog_trailer(commit) }
- before do
- allow(fake_helper).to receive(:security_mr?).and_return(true)
- end
+ context "when commit include a changelog trailer with an unknown category" do
+ let(:commit) { double('commit', message: "Hello world\n\nChangelog: foo", sha: "abc123") }
- it { is_expected.to have_attributes(errors: [], warnings: [], markdowns: [], messages: []) }
+ it { is_expected.to have_attributes(errors: ["Commit #{commit.sha} uses an invalid changelog category: foo"]) }
end
- context "when MR IID is empty" do
- before do
- allow(fake_helper).to receive(:mr_iid).and_return("")
- end
+ context 'when a commit uses the wrong casing for a trailer' do
+ let(:commit) { double('commit', message: "Hello world\n\nchangelog: foo", sha: "abc123") }
- it { is_expected.to have_attributes(errors: [], warnings: [], markdowns: [], messages: []) }
+ it { is_expected.to have_attributes(errors: ["The changelog trailer for commit #{commit.sha} must be `Changelog` (starting with a capital C), not `changelog`"]) }
end
- context "when YAML MR IID is empty" do
- let(:yaml_merge_request) { '' }
-
- context "and YAML includes a merge_request: line" do
- it { is_expected.to have_attributes(markdowns: [{ msg: format(described_class::SUGGEST_MR_COMMENT, mr_iid: fake_helper.mr_iid), file: changelog_path, line: 3 }]) }
- end
-
- context "and YAML does not include a merge_request: line" do
- let(:yaml) do
- <<~YAML
- ---
- title: #{yaml_title}
- author:
- type: #{yaml_type}
- YAML
- end
+ described_class::CATEGORIES.each do |category|
+ context "when commit include a changelog trailer with category set to '#{category}'" do
+ let(:commit) { double('commit', message: "Hello world\n\nChangelog: #{category}", sha: "abc123") }
- it { is_expected.to have_attributes(messages: ["Consider setting `merge_request` to #{mr_iid} in #{changelog_path}. #{described_class::SEE_DOC}"]) }
+ it { is_expected.to have_attributes(errors: []) }
end
end
end
@@ -177,13 +93,26 @@ RSpec.describe Tooling::Danger::Changelog do
let(:ee_change) { change_class.new('ee/app/models/foo.rb', :added, :backend) }
context "and a non-EE changelog, and changelog not required" do
- let(:changelog_change) { change_class.new('changelogs/unreleased/entry.yml', :added, :changelog) }
-
before do
allow(changelog).to receive(:required?).and_return(false)
+ allow(changelog).to receive(:ee_changelog?).and_return(false)
end
- it { is_expected.to have_attributes(warnings: ["This MR has a Changelog file outside `ee/`, but code changes in `ee/`. Consider moving the Changelog file into `ee/`."]) }
+ it { is_expected.to have_attributes(warnings: ["This MR changes code in `ee/`, but its Changelog commit is missing the [`EE: true` trailer](https://docs.gitlab.com/ee/development/changelog.html#gitlab-enterprise-changes). Consider adding it to your Changelog commits."]) }
+ end
+
+ context "and a EE changelog" do
+ before do
+ allow(changelog).to receive(:ee_changelog?).and_return(true)
+ end
+
+ it { is_expected.to have_attributes(errors: [], warnings: [], markdowns: [], messages: []) }
+
+ context "and there are DB changes" do
+ let(:foss_change) { change_class.new('db/migrate/foo.rb', :added, :migration) }
+
+ it { is_expected.to have_attributes(warnings: ["This MR has a Changelog commit with the `EE: true` trailer, but there are database changes which [requires](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry) the Changelog commit to not have the `EE: true` trailer. Consider removing the `EE: true` trailer from your commits."]) }
+ end
end
end
@@ -191,15 +120,19 @@ RSpec.describe Tooling::Danger::Changelog do
let(:foss_change) { change_class.new('app/models/foo.rb', :added, :backend) }
context "and a non-EE changelog" do
- let(:changelog_change) { change_class.new('changelogs/unreleased/entry.yml', :added, :changelog) }
+ before do
+ allow(changelog).to receive(:ee_changelog?).and_return(false)
+ end
it { is_expected.to have_attributes(errors: [], warnings: [], markdowns: [], messages: []) }
end
context "and a EE changelog" do
- let(:changelog_change) { change_class.new('ee/changelogs/unreleased/entry.yml', :added, :changelog) }
+ before do
+ allow(changelog).to receive(:ee_changelog?).and_return(true)
+ end
- it { is_expected.to have_attributes(warnings: ["This MR has a Changelog file in `ee/`, but no code changes in `ee/`. Consider moving the Changelog file outside `ee/`."]) }
+ it { is_expected.to have_attributes(warnings: ["This MR has a Changelog commit for EE, but no code changes in `ee/`. Consider removing the `EE: true` trailer from your commits."]) }
end
end
end
@@ -207,20 +140,26 @@ RSpec.describe Tooling::Danger::Changelog do
describe '#required_reasons' do
subject { changelog.required_reasons }
+ context "added files contain a migration" do
+ let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
+
+ it { is_expected.to include(:db_changes) }
+ end
+
context "removed files contains a feature flag" do
let(:changes) { changes_class.new([change_class.new('foo', :deleted, :feature_flag)]) }
it { is_expected.to include(:feature_flag_removed) }
end
- context "removed files do not contain a feature flag" do
- let(:changes) { changes_class.new([change_class.new('foo', :deleted, :backend)]) }
+ context "added files do not contain a migration" do
+ let(:changes) { changes_class.new([change_class.new('foo', :added, :frontend)]) }
it { is_expected.to be_empty }
end
- context "added files contain a migration" do
- let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
+ context "removed files do not contain a feature flag" do
+ let(:changes) { changes_class.new([change_class.new('foo', :deleted, :backend)]) }
it { is_expected.to be_empty }
end
@@ -229,20 +168,26 @@ RSpec.describe Tooling::Danger::Changelog do
describe '#required?' do
subject { changelog.required? }
+ context 'added files contain a migration' do
+ let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
+
+ it { is_expected.to be_truthy }
+ end
+
context "removed files contains a feature flag" do
let(:changes) { changes_class.new([change_class.new('foo', :deleted, :feature_flag)]) }
it { is_expected.to be_truthy }
end
- context "removed files do not contain a feature flag" do
- let(:changes) { changes_class.new([change_class.new('foo', :deleted, :backend)]) }
+ context 'added files do not contain a migration' do
+ let(:changes) { changes_class.new([change_class.new('foo', :added, :frontend)]) }
it { is_expected.to be_falsey }
end
- context "added files contain a migration" do
- let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
+ context "removed files do not contain a feature flag" do
+ let(:changes) { changes_class.new([change_class.new('foo', :deleted, :backend)]) }
it { is_expected.to be_falsey }
end
@@ -301,50 +246,63 @@ RSpec.describe Tooling::Danger::Changelog do
end
describe '#present?' do
- subject { changelog.present? }
+ it 'returns true when a Changelog commit is present' do
+ allow(changelog)
+ .to receive(:valid_changelog_commits)
+ .and_return([double(:commit)])
- context 'added files contain a changelog' do
- let(:changes) { changes_class.new([change_class.new('foo', :added, :changelog)]) }
-
- it { is_expected.to be_truthy }
+ expect(changelog).to be_present
end
- context 'added files do not contain a changelog' do
- let(:changes) { changes_class.new([change_class.new('foo', :added, :backend)]) }
+ it 'returns false when a Changelog commit is missing' do
+ allow(changelog).to receive(:valid_changelog_commits).and_return([])
- it { is_expected.to be_falsy }
+ expect(changelog).not_to be_present
end
end
- describe '#ee_changelog?' do
- subject { changelog.ee_changelog? }
+ describe '#changelog_commits' do
+ it 'returns the commits that include a Changelog trailer' do
+ commit1 = double(:commit, message: "foo\nChangelog: fixed")
+ commit2 = double(:commit, message: "bar\nChangelog: kittens")
+ commit3 = double(:commit, message: 'testing')
+ git = double(:git)
- context 'is ee changelog' do
- let(:changes) { changes_class.new([change_class.new('ee/changelogs/unreleased/entry.yml', :added, :changelog)]) }
+ allow(changelog).to receive(:git).and_return(git)
+ allow(git).to receive(:commits).and_return([commit1, commit2, commit3])
- it { is_expected.to be_truthy }
+ expect(changelog.changelog_commits).to eq([commit1, commit2])
end
+ end
+
+ describe '#valid_changelog_commits' do
+ it 'returns the commits with a valid Changelog trailer' do
+ commit1 = double(:commit, message: "foo\nChangelog: fixed")
+ commit2 = double(:commit, message: "bar\nChangelog: kittens")
- context 'is not ee changelog' do
- let(:changes) { changes_class.new([change_class.new('changelogs/unreleased/entry.yml', :added, :changelog)]) }
+ allow(changelog)
+ .to receive(:changelog_commits)
+ .and_return([commit1, commit2])
- it { is_expected.to be_falsy }
+ expect(changelog.valid_changelog_commits).to eq([commit1])
end
end
- describe '#changelog_path' do
- subject { changelog.changelog_path }
+ describe '#ee_changelog?' do
+ it 'returns true when an EE changelog commit is present' do
+ commit = double(:commit, message: "foo\nEE: true")
- context 'added files contain a changelog' do
- let(:changes) { changes_class.new([change_class.new('foo', :added, :changelog)]) }
+ allow(changelog).to receive(:changelog_commits).and_return([commit])
- it { is_expected.to eq('foo') }
+ expect(changelog.ee_changelog?).to eq(true)
end
- context 'added files do not contain a changelog' do
- let(:changes) { changes_class.new([change_class.new('foo', :added, :backend)]) }
+ it 'returns false when an EE changelog commit is missing' do
+ commit = double(:commit, message: 'foo')
- it { is_expected.to be_nil }
+ allow(changelog).to receive(:changelog_commits).and_return([commit])
+
+ expect(changelog.ee_changelog?).to eq(false)
end
end
@@ -355,8 +313,8 @@ RSpec.describe Tooling::Danger::Changelog do
shared_examples 'changelog modified text' do |key|
specify do
expect(subject).to include('CHANGELOG.md was edited')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
+ expect(subject).to include('`Changelog` trailer')
+ expect(subject).to include('`EE: true`')
end
end
@@ -386,7 +344,7 @@ RSpec.describe Tooling::Danger::Changelog do
specify do
expect(subject).to include('CHANGELOG.md was edited')
- expect(subject).not_to include('bin/changelog')
+ expect(subject).not_to include('`Changelog` trailer')
end
end
end
@@ -405,8 +363,21 @@ RSpec.describe Tooling::Danger::Changelog do
specify do
expect(subject).to have_key(key)
expect(subject[key]).to include('CHANGELOG missing')
- expect(subject[key]).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject[key]).not_to include('--ee')
+ expect(subject[key]).to include('`Changelog` trailer')
+ end
+ end
+
+ context 'with a new migration file' do
+ let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
+
+ context "when title is not changed from sanitization", :aggregate_failures do
+ it_behaves_like 'changelog required text', :db_changes
+ end
+
+ context "when title needs sanitization", :aggregate_failures do
+ let(:mr_title) { 'DRAFT: Fake Title' }
+
+ it_behaves_like 'changelog required text', :db_changes
end
end
@@ -426,8 +397,21 @@ RSpec.describe Tooling::Danger::Changelog do
specify do
expect(subject).to have_key(key)
expect(subject[key]).to include('CHANGELOG missing')
- expect(subject[key]).not_to include('bin/changelog')
- expect(subject[key]).not_to include('--ee')
+ expect(subject[key]).not_to include('`Changelog` trailer')
+ end
+ end
+
+ context 'with a new migration file' do
+ let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
+
+ context "when title is not changed from sanitization", :aggregate_failures do
+ it_behaves_like 'changelog required text', :db_changes
+ end
+
+ context "when title needs sanitization", :aggregate_failures do
+ let(:mr_title) { 'DRAFT: Fake Title' }
+
+ it_behaves_like 'changelog required text', :db_changes
end
end
@@ -446,8 +430,8 @@ RSpec.describe Tooling::Danger::Changelog do
shared_examples 'changelog optional text' do |key|
specify do
expect(subject).to include('CHANGELOG missing')
- expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
- expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
+ expect(subject).to include('`Changelog` trailer')
+ expect(subject).to include('EE: true')
end
end
@@ -477,7 +461,6 @@ RSpec.describe Tooling::Danger::Changelog do
specify do
expect(subject).to include('CHANGELOG missing')
- expect(subject).not_to include('bin/changelog')
end
end
end
diff --git a/spec/tooling/danger/product_intelligence_spec.rb b/spec/tooling/danger/product_intelligence_spec.rb
new file mode 100644
index 00000000000..17ef67e64fe
--- /dev/null
+++ b/spec/tooling/danger/product_intelligence_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'gitlab-dangerfiles'
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../tooling/danger/product_intelligence'
+require_relative '../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::ProductIntelligence do
+ include_context "with dangerfile"
+
+ subject(:product_intelligence) { fake_danger.new(helper: fake_helper) }
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+ let(:changed_files) { ['metrics/counts_7d/test_metric.yml', 'doc/development/usage_ping/dictionary.md'] }
+ let(:changed_lines) { ['+tier: ee'] }
+
+ before do
+ allow(fake_helper).to receive(:all_changed_files).and_return(changed_files)
+ allow(fake_helper).to receive(:changed_lines).and_return(changed_lines)
+ end
+
+ describe '#need_dictionary_changes?' do
+ subject { product_intelligence.need_dictionary_changes? }
+
+ context 'when changed files do not contain dictionary changes' do
+ let(:changed_files) { ['config/metrics/counts_7d/test_metric.yml'] }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when changed files already contains dictionary changes' do
+ let(:changed_files) { ['doc/development/usage_ping/dictionary.md'] }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#missing_labels' do
+ subject { product_intelligence.missing_labels }
+
+ let(:ci_env) { true }
+
+ before do
+ allow(fake_helper).to receive(:mr_has_labels?).and_return(false)
+ allow(fake_helper).to receive(:ci?).and_return(ci_env)
+ end
+
+ context 'with ci? false' do
+ let(:ci_env) { false }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with ci? true' do
+ let(:expected_labels) { ['product intelligence', 'product intelligence::review pending'] }
+
+ it { is_expected.to match_array(expected_labels) }
+ end
+
+ context 'with product intelligence label' do
+ let(:expected_labels) { ['product intelligence::review pending'] }
+
+ before do
+ allow(fake_helper).to receive(:mr_has_labels?).with('product intelligence').and_return(true)
+ end
+
+ it { is_expected.to match_array(expected_labels) }
+ end
+
+ context 'with product intelligence::review pending' do
+ before do
+ allow(fake_helper).to receive(:mr_has_labels?).and_return(true)
+ end
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '#matching_changed_files' do
+ subject { product_intelligence.matching_changed_files }
+
+ let(:changed_files) do
+ [
+ 'dashboard/todos_controller.rb',
+ 'components/welcome.vue',
+ 'admin/groups/_form.html.haml'
+ ]
+ end
+
+ context 'with snowplow files changed' do
+ context 'when vue file changed' do
+ let(:changed_lines) { ['+data-track-event'] }
+
+ it { is_expected.to match_array(['components/welcome.vue']) }
+ end
+
+ context 'when haml file changed' do
+ let(:changed_lines) { ['+ data: { track_label:'] }
+
+ it { is_expected.to match_array(['admin/groups/_form.html.haml']) }
+ end
+
+ context 'when ruby file changed' do
+ let(:changed_lines) { ['+ Gitlab::Tracking.event'] }
+ let(:changed_files) { ['dashboard/todos_controller.rb', 'admin/groups/_form.html.haml'] }
+
+ it { is_expected.to match_array(['dashboard/todos_controller.rb']) }
+ end
+ end
+
+ context 'with dictionary file not changed' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'with metrics files changed' do
+ let(:changed_files) { ['config/metrics/counts_7d/test_metric.yml', 'ee/config/metrics/counts_7d/ee_metric.yml'] }
+
+ it { is_expected.to match_array(changed_files) }
+ end
+
+ context 'with metrics files not changed' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'with tracking files changed' do
+ let(:changed_files) do
+ [
+ 'lib/gitlab/tracking.rb',
+ 'spec/lib/gitlab/tracking_spec.rb',
+ 'app/helpers/tracking_helper.rb'
+ ]
+ end
+
+ it { is_expected.to match_array(changed_files) }
+ end
+
+ context 'with usage_data files changed' do
+ let(:changed_files) do
+ [
+ 'doc/api/usage_data.md',
+ 'ee/lib/ee/gitlab/usage_data.rb',
+ 'spec/lib/gitlab/usage_data_spec.rb'
+ ]
+ end
+
+ it { is_expected.to match_array(changed_files) }
+ end
+ end
+end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 1d2ea0f5ba3..7474709d255 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -112,10 +112,10 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'FOO_VERSION' | [:backend]
'Dangerfile' | [:engineering_productivity]
- 'danger/commit_messages/Dangerfile' | [:engineering_productivity]
- 'ee/danger/commit_messages/Dangerfile' | [:engineering_productivity]
- 'danger/commit_messages/' | [:engineering_productivity]
- 'ee/danger/commit_messages/' | [:engineering_productivity]
+ 'danger/bundle_size/Dangerfile' | [:engineering_productivity]
+ 'ee/danger/bundle_size/Dangerfile' | [:engineering_productivity]
+ 'danger/bundle_size/' | [:engineering_productivity]
+ 'ee/danger/bundle_size/' | [:engineering_productivity]
'.gitlab-ci.yml' | [:engineering_productivity]
'.gitlab/ci/cng.gitlab-ci.yml' | [:engineering_productivity]
'.gitlab/ci/ee-specific-checks.gitlab-ci.yml' | [:engineering_productivity]
@@ -139,18 +139,18 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'db/post_migrate/foo' | [:database, :migration]
'ee/db/geo/migrate/foo' | [:database, :migration]
'ee/db/geo/post_migrate/foo' | [:database, :migration]
- 'app/models/project_authorization.rb' | [:database]
- 'app/services/users/refresh_authorized_projects_service.rb' | [:database]
- 'app/services/authorized_project_update/find_records_due_for_refresh_service.rb' | [:database]
- 'lib/gitlab/background_migration.rb' | [:database]
- 'lib/gitlab/background_migration/foo' | [:database]
- 'ee/lib/gitlab/background_migration/foo' | [:database]
- 'lib/gitlab/database.rb' | [:database]
- 'lib/gitlab/database/foo' | [:database]
- 'ee/lib/gitlab/database/foo' | [:database]
- 'lib/gitlab/github_import.rb' | [:database]
- 'lib/gitlab/github_import/foo' | [:database]
- 'lib/gitlab/sql/foo' | [:database]
+ 'app/models/project_authorization.rb' | [:database, :backend]
+ 'app/services/users/refresh_authorized_projects_service.rb' | [:database, :backend]
+ 'app/services/authorized_project_update/find_records_due_for_refresh_service.rb' | [:database, :backend]
+ 'lib/gitlab/background_migration.rb' | [:database, :backend]
+ 'lib/gitlab/background_migration/foo' | [:database, :backend]
+ 'ee/lib/gitlab/background_migration/foo' | [:database, :backend]
+ 'lib/gitlab/database.rb' | [:database, :backend]
+ 'lib/gitlab/database/foo' | [:database, :backend]
+ 'ee/lib/gitlab/database/foo' | [:database, :backend]
+ 'lib/gitlab/github_import.rb' | [:database, :backend]
+ 'lib/gitlab/github_import/foo' | [:database, :backend]
+ 'lib/gitlab/sql/foo' | [:database, :backend]
'rubocop/cop/migration/foo' | [:database]
'db/fixtures/foo.rb' | [:backend]
@@ -162,8 +162,6 @@ RSpec.describe Tooling::Danger::ProjectHelper do
'workhorse/main.go' | [:workhorse]
'workhorse/internal/upload/upload.go' | [:workhorse]
- 'changelogs/foo' | [:none]
- 'ee/changelogs/foo' | [:none]
'locale/gitlab.pot' | [:none]
'FOO' | [:unknown]
@@ -220,7 +218,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
- expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, commit_messages, database, datateam, documentation, duplicate_yarn_dependencies, eslint, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
+ expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, database, datateam, documentation, duplicate_yarn_dependencies, eslint, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
end
end
diff --git a/spec/lib/gitlab/graphql/docs/renderer_spec.rb b/spec/tooling/graphql/docs/renderer_spec.rb
index 14db51deb88..50ebb754ca4 100644
--- a/spec/lib/gitlab/graphql/docs/renderer_spec.rb
+++ b/spec/tooling/graphql/docs/renderer_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require 'spec_helper'
+require_relative '../../../../tooling/graphql/docs/renderer'
-RSpec.describe Gitlab::Graphql::Docs::Renderer do
+RSpec.describe Tooling::Graphql::Docs::Renderer do
describe '#contents' do
shared_examples 'renders correctly as GraphQL documentation' do
it 'contains the expected section' do
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
end
end
- let(:template) { Rails.root.join('lib/gitlab/graphql/docs/templates/default.md.haml') }
+ let(:template) { Rails.root.join('tooling/graphql/docs/templates/default.md.haml') }
let(:field_description) { 'List of objects.' }
let(:type) { ::GraphQL::INT_TYPE }
@@ -358,6 +358,9 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
value 'BAR',
description: 'A description of BAR.',
deprecated: { reason: 'This is deprecated', milestone: '1.10' }
+ value 'BOOP',
+ description: 'A description of BOOP.',
+ deprecated: { reason: :renamed, replacement: 'MyEnum.BAR', milestone: '1.10' }
end
Class.new(Types::BaseObject) do
@@ -375,8 +378,9 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
| Value | Description |
| ----- | ----------- |
- | <a id="myenumbar"></a>`BAR` **{warning-solid}** | **Deprecated:** This is deprecated. Deprecated in 1.10. |
+ | <a id="myenumbar"></a>`BAR` **{warning-solid}** | **Deprecated** in 1.10. This is deprecated. |
| <a id="myenumbaz"></a>`BAZ` | A description of BAZ. |
+ | <a id="myenumboop"></a>`BOOP` **{warning-solid}** | **Deprecated** in 1.10. This was renamed. Use: [`MyEnum.BAR`](#myenumbar). |
DOC
end
diff --git a/spec/views/admin/users/_user.html.haml_spec.rb b/spec/views/admin/users/_user.html.haml_spec.rb
deleted file mode 100644
index aed05e4ea9b..00000000000
--- a/spec/views/admin/users/_user.html.haml_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'admin/users/_user.html.haml' do
- before do
- allow(view).to receive(:user).and_return(user)
- end
-
- context 'internal users' do
- context 'when showing a `Ghost User`' do
- let(:user) { create(:user, :ghost) }
-
- it 'does not render action buttons' do
- render
-
- expect(rendered).not_to have_selector('.table-action-buttons')
- end
- end
-
- context 'when showing a `Bot User`' do
- let(:user) { create(:user, user_type: :alert_bot) }
-
- it 'does not render action buttons' do
- render
-
- expect(rendered).not_to have_selector('.table-action-buttons')
- end
- end
-
- context 'when showing a `Migration User`' do
- let(:user) { create(:user, user_type: :migration_bot) }
-
- it 'does not render action buttons' do
- render
-
- expect(rendered).not_to have_selector('.table-action-buttons')
- end
- end
- end
-
- context 'when showing an external user' do
- let(:user) { create(:user) }
-
- it 'renders action buttons' do
- render
-
- expect(rendered).to have_selector('.table-action-buttons')
- end
- end
-end
diff --git a/spec/views/devise/shared/_signup_box.html.haml_spec.rb b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
index b73e32fa765..6efb2730964 100644
--- a/spec/views/devise/shared/_signup_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signup_box.html.haml_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'devise/shared/_signup_box' do
allow(view).to receive(:url).and_return('_url_')
allow(view).to receive(:terms_path).and_return('_terms_path_')
allow(view).to receive(:button_text).and_return('_button_text_')
- allow(view).to receive(:suggestion_path).and_return('_suggestion_path_')
+ allow(view).to receive(:signup_username_data_attributes).and_return({})
stub_template 'devise/shared/_error_messages.html.haml' => ''
end
diff --git a/spec/views/layouts/header/_new_dropdown.haml_spec.rb b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
index bf81ab577f7..319e7b55fc3 100644
--- a/spec/views/layouts/header/_new_dropdown.haml_spec.rb
+++ b/spec/views/layouts/header/_new_dropdown.haml_spec.rb
@@ -52,7 +52,6 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end
it 'has a "New project" link' do
- render('layouts/header/new_repo_experiment')
render
expect(rendered).to have_link('New project', href: new_project_path(namespace_id: group.id))
@@ -164,7 +163,6 @@ RSpec.describe 'layouts/header/_new_dropdown' do
end
it 'has a "New project" link' do
- render('layouts/header/new_repo_experiment')
render
expect(rendered).to have_link('New project', href: new_project_path)
@@ -182,13 +180,13 @@ RSpec.describe 'layouts/header/_new_dropdown' do
expect(rendered).to have_link('New snippet', href: new_snippet_path)
end
- context 'when the user is not allowed to create snippets' do
+ context 'when the user is not allowed to do anything' do
let(:user) { create(:user, :external) }
- it 'has no "New snippet" link' do
- render
-
- expect(rendered).not_to have_link('New snippet', href: new_snippet_path)
+ it 'is nil' do
+ # We have to us `view.render` because `render` causes issues
+ # https://github.com/rails/rails/issues/41320
+ expect(view.render("layouts/header/new_dropdown")).to be_nil
end
end
end
diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
index d96052d4c03..50390964e1b 100644
--- a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
@@ -12,11 +12,19 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
it_behaves_like 'has nav sidebar'
it_behaves_like 'sidebar includes snowplow attributes', 'render', 'groups_side_navigation', 'groups_side_navigation'
- describe 'Group information' do
+ describe 'Group context menu' do
it 'has a link to the group path' do
render
- expect(rendered).to have_link('Group information', href: group_path(group))
+ expect(rendered).to have_link(group.name, href: group_path(group))
+ end
+ end
+
+ describe 'Group information' do
+ it 'has a link to the group activity path' do
+ render
+
+ expect(rendered).to have_link('Group information', href: activity_group_path(group))
end
it 'does not have a link to the details menu item' do
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index 7cb49f635af..c775574091e 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -19,21 +19,32 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it_behaves_like 'has nav sidebar'
- describe 'Project information' do
+ describe 'Project context' do
it 'has a link to the project path' do
render
- expect(rendered).to have_link('Project information', href: project_path(project), class: %w(shortcuts-project rspec-project-link))
+ expect(rendered).to have_link(project.name, href: project_path(project), class: %w(shortcuts-project rspec-project-link))
+ expect(rendered).to have_selector("[aria-label=\"#{project.name}\"]")
+ end
+ end
+
+ describe 'Project information' do
+ it 'has a link to the project activity path' do
+ render
+
+ expect(rendered).to have_link('Project information', href: activity_project_path(project), class: %w(shortcuts-project-information))
expect(rendered).to have_selector('[aria-label="Project information"]')
end
context 'when feature flag :sidebar_refactor is disabled' do
- it 'has a link to the project path' do
+ before do
stub_feature_flags(sidebar_refactor: false)
+ end
+ it 'has a link to the project path' do
render
- expect(rendered).to have_link('Project overview', href: project_path(project), class: %w(shortcuts-project rspec-project-link))
+ expect(rendered).to have_link('Project overview', href: project_path(project), class: %w(shortcuts-project))
expect(rendered).to have_selector('[aria-label="Project overview"]')
end
end
@@ -89,7 +100,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'has a link to the labels path' do
render
- expect(page.at_css('.shortcuts-project').parent.css('[aria-label="Labels"]')).not_to be_empty
+ expect(page.at_css('.shortcuts-project-information').parent.css('[aria-label="Labels"]')).not_to be_empty
expect(rendered).to have_link('Labels', href: project_labels_path(project))
end
@@ -110,7 +121,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'has a link to the members page' do
render
- expect(page.at_css('.shortcuts-project').parent.css('[aria-label="Members"]')).not_to be_empty
+ expect(page.at_css('.shortcuts-project-information').parent.css('[aria-label="Members"]')).not_to be_empty
expect(rendered).to have_link('Members', href: project_project_members_path(project))
end
@@ -277,7 +288,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
let(:external_issue_tracker_url) { 'http://test.com' }
let!(:external_issue_tracker) do
- create(:custom_issue_tracker_service, active: external_issue_tracker_active, project: project, project_url: external_issue_tracker_url)
+ create(:custom_issue_tracker_integration, active: external_issue_tracker_active, project: project, project_url: external_issue_tracker_url)
end
context 'when external issue tracker is configured and active' do
@@ -994,7 +1005,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
describe 'Confluence' do
- let!(:service) { create(:confluence_service, project: project, active: active) }
+ let!(:service) { create(:confluence_integration, project: project, active: active) }
before do
render
@@ -1327,4 +1338,22 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
it_behaves_like 'sidebar includes snowplow attributes', 'render', 'projects_side_navigation', 'projects_side_navigation'
+
+ describe 'Collapsed menu items' do
+ it 'does not render the collapsed top menu as a link' do
+ render
+
+ expect(rendered).not_to have_selector('.sidebar-sub-level-items > li.fly-out-top-item > a')
+ end
+
+ context 'when feature flag :sidebar_refactor is disabled' do
+ it 'renders the collapsed top menu as a link' do
+ stub_feature_flags(sidebar_refactor: false)
+
+ render
+
+ expect(rendered).to have_selector('.sidebar-sub-level-items > li.fly-out-top-item > a')
+ end
+ end
+ end
end
diff --git a/spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb b/spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb
index bf5cb6fb25d..5120998ded6 100644
--- a/spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb
+++ b/spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb
@@ -23,16 +23,4 @@ RSpec.describe 'clusters/clusters/gcp/_form' do
expect(rendered).to have_selector("input[id='cluster_provider_gcp_attributes_cloud_run']")
end
end
-
- context 'with cloud run feature flag disabled' do
- before do
- stub_feature_flags(create_cloud_run_clusters: false)
- end
-
- it 'does not have a cloud run checkbox' do
- render
-
- expect(rendered).not_to have_selector("input[id='cluster_provider_gcp_attributes_cloud_run']")
- end
- end
end
diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb
index dd7b1bd94a9..7fa95507f75 100644
--- a/spec/views/projects/empty.html.haml_spec.rb
+++ b/spec/views/projects/empty.html.haml_spec.rb
@@ -46,47 +46,36 @@ RSpec.describe 'projects/empty' do
end
end
- describe 'invite_members_empty_project_version_a experiment' do
+ context 'with invite button on empty projects' do
let(:can_import_members) { true }
before do
allow(view).to receive(:can_import_members?).and_return(can_import_members)
end
- shared_examples_for 'no invite member info' do
- it 'does not show invite member info' do
- render
+ it 'shows invite members info', :aggregate_failures do
+ render
- expect(rendered).not_to have_content('Invite your team')
- end
+ expect(rendered).to have_selector('[data-track-event=render]')
+ expect(rendered).to have_selector('[data-track-label=invite_members_empty_project]')
+ expect(rendered).to have_content('Invite your team')
+ expect(rendered).to have_content('Add members to this project and start collaborating with your team.')
+ expect(rendered).to have_selector('.js-invite-members-trigger')
+ expect(rendered).to have_selector('.js-invite-members-modal')
+ expect(rendered).to have_selector('[data-label=invite_members_empty_project]')
+ expect(rendered).to have_selector('[data-event=click_button]')
end
- context 'when experiment is enabled' do
- it 'shows invite members info', :aggregate_failures do
- render
-
- expect(rendered).to have_selector('[data-track-event=render]')
- expect(rendered).to have_selector('[data-track-label=invite_members_empty_project]', count: 2)
- expect(rendered).to have_content('Invite your team')
- expect(rendered).to have_content('Add members to this project and start collaborating with your team.')
- expect(rendered).to have_link('Invite members', href: project_project_members_path(project, sort: :access_level_desc))
- expect(rendered).to have_selector('[data-track-event=click_button]')
- end
+ context 'when user does not have permissions to invite members' do
+ let(:can_import_members) { false }
- context 'when user does not have permissions to invite members' do
- let(:can_import_members) { false }
-
- it_behaves_like 'no invite member info'
- end
- end
+ it 'does not show invite member info', :aggregate_failures do
+ render
- context 'when experiment is not enabled' do
- before do
- allow(view).to receive(:experiment_enabled?)
- .with(:invite_members_empty_project_version_a).and_return(false)
+ expect(rendered).not_to have_content('Invite your team')
+ expect(rendered).not_to have_selector('.js-invite-members-trigger')
+ expect(rendered).not_to have_selector('.js-invite-members-modal')
end
-
- it_behaves_like 'no invite member info'
end
end
end
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
index 4ca64eb3129..f063e73dae4 100644
--- a/spec/views/projects/services/_form.haml_spec.rb
+++ b/spec/views/projects/services/_form.haml_spec.rb
@@ -11,19 +11,18 @@ RSpec.describe 'projects/services/_form' do
allow(controller).to receive(:current_user).and_return(user)
- allow(view).to receive_messages(current_user: user,
- can?: true,
- current_application_settings: Gitlab::CurrentSettings.current_application_settings,
- request: double(referrer: '/services'))
+ allow(view).to receive_messages(
+ current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings,
+ integration: project.redmine_service,
+ request: double(referer: '/services')
+ )
end
context 'commit_events and merge_request_events' do
- before do
- assign(:service, project.redmine_service)
- end
-
it 'display merge_request_events and commit_events descriptions' do
- allow(RedmineService).to receive(:supported_events).and_return(%w(commit merge_request))
+ allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request))
render
diff --git a/spec/views/projects/services/edit.html.haml_spec.rb b/spec/views/projects/services/edit.html.haml_spec.rb
index 785af6a5344..a5460adbd2c 100644
--- a/spec/views/projects/services/edit.html.haml_spec.rb
+++ b/spec/views/projects/services/edit.html.haml_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
RSpec.describe 'projects/services/edit' do
- let(:service) { create(:drone_ci_service, project: project) }
+ let(:integration) { create(:drone_ci_integration, project: project) }
let(:project) { create(:project) }
before do
assign :project, project
- assign :service, service
+ assign :integration, integration
end
it do
@@ -17,7 +17,7 @@ RSpec.describe 'projects/services/edit' do
expect(rendered).not_to have_text('Recent Deliveries')
end
- context 'service using WebHooks' do
+ context 'integration using WebHooks' do
before do
assign(:web_hook_logs, [])
end
diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb
index ab868eb78b8..43c064e1a2b 100644
--- a/spec/views/projects/settings/operations/show.html.haml_spec.rb
+++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'projects/settings/operations/show' do
it 'renders the Operations Settings page' do
render
- expect(rendered).to have_content _('Alert integrations')
+ expect(rendered).to have_content _('Alerts')
expect(rendered).to have_content _('Display alerts from all configured monitoring tools.')
end
end
@@ -59,7 +59,7 @@ RSpec.describe 'projects/settings/operations/show' do
expect(rendered).to have_content _('Prometheus')
expect(rendered).to have_content _('Link Prometheus monitoring to GitLab.')
- expect(rendered).to have_content _('To enable the installation of Prometheus on your clusters, deactivate the manual configuration.')
+ expect(rendered).to have_content _('To use a Prometheus installed on a cluster, deactivate the manual configuration.')
end
end
@@ -77,41 +77,11 @@ RSpec.describe 'projects/settings/operations/show' do
end
describe 'Operations > Tracing' do
- context 'with project.tracing_external_url' do
- it 'links to project.tracing_external_url' do
- render
-
- expect(rendered).to have_link('Tracing', href: tracing_setting.external_url)
- end
-
- context 'with malicious external_url' do
- let(:malicious_tracing_url) { "https://replaceme.com/'><script>alert(document.cookie)</script>" }
- let(:cleaned_url) { "https://replaceme.com/'>" }
-
- before do
- tracing_setting.update_column(:external_url, malicious_tracing_url)
- end
-
- it 'sanitizes external_url' do
- render
-
- expect(tracing_setting.external_url).to eq(malicious_tracing_url)
- expect(rendered).to have_link('Tracing', href: cleaned_url)
- end
- end
- end
-
- context 'without project.tracing_external_url' do
- let(:tracing_setting) { build(:project_tracing_setting, project: project) }
-
- before do
- tracing_setting.external_url = nil
- end
-
- it 'links to Tracing page' do
+ context 'Settings page ' do
+ it 'renders the Tracing Settings page' do
render
- expect(rendered).to have_link('Tracing', href: project_tracing_path(project))
+ expect(rendered).to have_content _('Embed an image of your existing Jaeger server in GitLab.')
end
end
end
diff --git a/spec/views/shared/nav/_sidebar.html.haml_spec.rb b/spec/views/shared/nav/_sidebar.html.haml_spec.rb
index cf9452ba68c..2eeebdff7a8 100644
--- a/spec/views/shared/nav/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/nav/_sidebar.html.haml_spec.rb
@@ -25,13 +25,11 @@ RSpec.describe 'shared/nav/_sidebar.html.haml' do
context 'when sidebar does not have a scope menu' do
let(:scope_menu_view_path) { 'shared/nav/' }
let(:scope_menu_view_name) { 'scope_menu.html.haml' }
- let(:scope_menu_view) { "#{scope_menu_view_path}#{scope_menu_view_name}" }
let(:scope_menu_partial) { "#{scope_menu_view_path}_#{scope_menu_view_name}" }
let(:content) { 'Custom test content' }
context 'when sidebar has a custom scope menu partial defined' do
it 'renders the custom partial' do
- allow(sidebar).to receive(:render_raw_scope_menu_partial).and_return(scope_menu_view)
allow(view).to receive(:scope_menu).and_return(nil)
stub_template(scope_menu_partial => content)
diff --git a/spec/workers/analytics/instance_statistics/count_job_trigger_worker_spec.rb b/spec/workers/analytics/instance_statistics/count_job_trigger_worker_spec.rb
deleted file mode 100644
index da0cbe37400..00000000000
--- a/spec/workers/analytics/instance_statistics/count_job_trigger_worker_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Analytics::InstanceStatistics::CountJobTriggerWorker do
- it_behaves_like 'an idempotent worker'
-
- context 'triggers a job for each measurement identifiers' do
- let(:expected_count) { Analytics::UsageTrends::Measurement.identifier_query_mapping.keys.size }
-
- it 'triggers CounterJobWorker jobs' do
- subject.perform
-
- expect(Analytics::UsageTrends::CounterJobWorker.jobs.count).to eq(expected_count)
- end
- end
-end
diff --git a/spec/workers/analytics/instance_statistics/counter_job_worker_spec.rb b/spec/workers/analytics/instance_statistics/counter_job_worker_spec.rb
deleted file mode 100644
index 4994fec44ab..00000000000
--- a/spec/workers/analytics/instance_statistics/counter_job_worker_spec.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Analytics::InstanceStatistics::CounterJobWorker do
- let_it_be(:user_1) { create(:user) }
- let_it_be(:user_2) { create(:user) }
-
- let(:users_measurement_identifier) { ::Analytics::UsageTrends::Measurement.identifiers.fetch(:users) }
- let(:recorded_at) { Time.zone.now }
- let(:job_args) { [users_measurement_identifier, user_1.id, user_2.id, recorded_at] }
-
- before do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
- end
-
- include_examples 'an idempotent worker' do
- it 'counts a scope and stores the result' do
- subject
-
- measurement = Analytics::UsageTrends::Measurement.users.first
- expect(measurement.recorded_at).to be_like_time(recorded_at)
- expect(measurement.identifier).to eq('users')
- expect(measurement.count).to eq(2)
- end
- end
-
- context 'when no records are in the database' do
- let(:users_measurement_identifier) { ::Analytics::UsageTrends::Measurement.identifiers.fetch(:groups) }
-
- subject { described_class.new.perform(users_measurement_identifier, nil, nil, recorded_at) }
-
- it 'sets 0 as the count' do
- subject
-
- measurement = Analytics::UsageTrends::Measurement.groups.first
- expect(measurement.recorded_at).to be_like_time(recorded_at)
- expect(measurement.identifier).to eq('groups')
- expect(measurement.count).to eq(0)
- end
- end
-
- it 'does not raise error when inserting duplicated measurement' do
- subject
-
- expect { subject }.not_to raise_error
- end
-
- it 'does not insert anything when BatchCount returns error' do
- allow(Gitlab::Database::BatchCount).to receive(:batch_count).and_return(Gitlab::Database::BatchCounter::FALLBACK)
-
- expect { subject }.not_to change { Analytics::UsageTrends::Measurement.count }
- end
-
- context 'when pipelines_succeeded identifier is passed' do
- let_it_be(:pipeline) { create(:ci_pipeline, :success) }
-
- let(:successful_pipelines_measurement_identifier) { ::Analytics::UsageTrends::Measurement.identifiers.fetch(:pipelines_succeeded) }
- let(:job_args) { [successful_pipelines_measurement_identifier, pipeline.id, pipeline.id, recorded_at] }
-
- it 'counts successful pipelines' do
- subject
-
- measurement = Analytics::UsageTrends::Measurement.pipelines_succeeded.first
- expect(measurement.recorded_at).to be_like_time(recorded_at)
- expect(measurement.identifier).to eq('pipelines_succeeded')
- expect(measurement.count).to eq(1)
- end
- end
-end
diff --git a/spec/workers/authorized_project_update/project_recalculate_worker_spec.rb b/spec/workers/authorized_project_update/project_recalculate_worker_spec.rb
new file mode 100644
index 00000000000..403793a15e2
--- /dev/null
+++ b/spec/workers/authorized_project_update/project_recalculate_worker_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuthorizedProjectUpdate::ProjectRecalculateWorker do
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:project) { create(:project) }
+
+ subject(:worker) { described_class.new }
+
+ it 'is labeled as high urgency' do
+ expect(described_class.get_urgency).to eq(:high)
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { project.id }
+
+ it 'does not change authorizations when run twice' do
+ user = create(:user)
+ project.add_developer(user)
+
+ user.project_authorizations.delete_all
+
+ expect { worker.perform(project.id) }.to change { project.project_authorizations.reload.size }.by(1)
+ expect { worker.perform(project.id) }.not_to change { project.project_authorizations.reload.size }
+ end
+ end
+
+ describe '#perform' do
+ it 'does not fail if the project does not exist' do
+ expect do
+ worker.perform(non_existing_record_id)
+ end.not_to raise_error
+ end
+
+ it 'calls AuthorizedProjectUpdate::ProjectRecalculateService' do
+ expect_next_instance_of(AuthorizedProjectUpdate::ProjectRecalculateService, project) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ worker.perform(project.id)
+ end
+
+ context 'exclusive lease' do
+ let(:lock_key) { "#{described_class.name.underscore}/#{project.root_namespace.id}" }
+ let(:timeout) { 10.seconds }
+
+ context 'when exclusive lease has not been taken' do
+ it 'obtains a new exclusive lease' do
+ expect_to_obtain_exclusive_lease(lock_key, timeout: timeout)
+
+ worker.perform(project.id)
+ end
+ end
+
+ context 'when exclusive lease has already been taken' do
+ before do
+ stub_exclusive_lease_taken(lock_key, timeout: timeout)
+ end
+
+ it 'raises an error' do
+ expect { worker.perform(project.id) }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb
new file mode 100644
index 00000000000..cdf2cb493b0
--- /dev/null
+++ b/spec/workers/authorized_project_update/user_refresh_from_replica_worker_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AuthorizedProjectUpdate::UserRefreshFromReplicaWorker do
+ it 'is labeled as low urgency' do
+ expect(described_class.get_urgency).to eq(:low)
+ end
+
+ it_behaves_like "refreshes user's project authorizations"
+end
diff --git a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
index 832d5afd957..7c0c4d5bab4 100644
--- a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
+++ b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:user) { project.namespace.owner }
let(:start_user_id) { user.id }
let(:end_user_id) { start_user_id }
@@ -11,56 +12,35 @@ RSpec.describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker do
it_behaves_like 'worker with data consistency',
described_class,
- feature_flag: :delayed_consistency_for_user_refresh_over_range_worker,
data_consistency: :delayed
describe '#perform' do
- context 'when the feature flag `periodic_project_authorization_update_via_replica` is enabled' do
- before do
- stub_feature_flags(periodic_project_authorization_update_via_replica: true)
- end
-
- context 'checks if project authorization update is required' do
- it 'checks if a project_authorization refresh is needed for each of the users' do
- User.where(id: start_user_id..end_user_id).each do |user|
- expect(AuthorizedProjectUpdate::FindRecordsDueForRefreshService).to(
- receive(:new).with(user).and_call_original)
- end
-
- execute_worker
- end
- end
-
- context 'when there are project authorization records due for either removal or addition for a specific user' do
- before do
- user.project_authorizations.delete_all
+ context 'checks if project authorization update is required' do
+ it 'checks if a project_authorization refresh is needed for each of the users' do
+ User.where(id: start_user_id..end_user_id).each do |user|
+ expect(AuthorizedProjectUpdate::FindRecordsDueForRefreshService).to(
+ receive(:new).with(user).and_call_original)
end
- it 'enqueues a new project authorization update job for the user' do
- expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to receive(:perform_async).with(user.id)
+ execute_worker
+ end
+ end
- execute_worker
- end
+ context 'when there are project authorization records due for either removal or addition for a specific user' do
+ before do
+ user.project_authorizations.delete_all
end
- context 'when there are no additions or removals to be made to project authorizations for a specific user' do
- it 'does not enqueue a new project authorization update job for the user' do
- expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).not_to receive(:perform_async)
+ it 'enqueues a new project authorization update job for the user' do
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to receive(:perform_async).with(user.id)
- execute_worker
- end
+ execute_worker
end
end
- context 'when the feature flag `periodic_project_authorization_update_via_replica` is disabled' do
- before do
- stub_feature_flags(periodic_project_authorization_update_via_replica: false)
- end
-
- it 'calls AuthorizedProjectUpdate::RecalculateForUserRangeService' do
- expect_next_instance_of(AuthorizedProjectUpdate::RecalculateForUserRangeService, start_user_id, end_user_id) do |service|
- expect(service).to receive(:execute)
- end
+ context 'when there are no additions or removals to be made to project authorizations for a specific user' do
+ it 'does not enqueue a new project authorization update job for the user' do
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).not_to receive(:perform_async)
execute_worker
end
diff --git a/spec/workers/build_hooks_worker_spec.rb b/spec/workers/build_hooks_worker_spec.rb
index 8395d8fb0e7..5f7e7e5fb00 100644
--- a/spec/workers/build_hooks_worker_spec.rb
+++ b/spec/workers/build_hooks_worker_spec.rb
@@ -24,18 +24,8 @@ RSpec.describe BuildHooksWorker do
end
describe '.perform_async' do
- context 'when delayed_perform_for_build_hooks_worker feature flag is disabled' do
- before do
- stub_feature_flags(delayed_perform_for_build_hooks_worker: false)
- end
-
- it 'does not call perform_in' do
- expect(described_class).not_to receive(:perform_in)
- end
- end
-
- it 'delays scheduling a job by calling perform_in' do
- expect(described_class).to receive(:perform_in).with(described_class::DATA_CONSISTENCY_DELAY.second, 123)
+ it 'delays scheduling a job by calling perform_in with default delay' do
+ expect(described_class).to receive(:perform_in).with(ApplicationWorker::DEFAULT_DELAY_INTERVAL.second, 123)
described_class.perform_async(123)
end
@@ -43,6 +33,5 @@ RSpec.describe BuildHooksWorker do
it_behaves_like 'worker with data consistency',
described_class,
- feature_flag: :load_balancing_for_build_hooks_worker,
data_consistency: :delayed
end
diff --git a/spec/workers/build_queue_worker_spec.rb b/spec/workers/build_queue_worker_spec.rb
new file mode 100644
index 00000000000..5f8510abf23
--- /dev/null
+++ b/spec/workers/build_queue_worker_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BuildQueueWorker do
+ describe '#perform' do
+ context 'when build exists' do
+ let!(:build) { create(:ci_build) }
+
+ it 'ticks runner queue value' do
+ expect_next_instance_of(Ci::UpdateBuildQueueService) do |instance|
+ expect(instance).to receive(:tick).with(build)
+ end
+
+ described_class.new.perform(build.id)
+ end
+ end
+
+ context 'when build does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+
+ it_behaves_like 'worker with data consistency',
+ described_class,
+ feature_flag: :load_balancing_for_build_queue_worker,
+ data_consistency: :sticky
+end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index 9119394f250..205bf23f36d 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -22,6 +22,16 @@ RSpec.describe BulkImportWorker do
end
end
+ context 'when bulk import is failed' do
+ it 'does nothing' do
+ bulk_import = create(:bulk_import, :failed)
+
+ expect(described_class).not_to receive(:perform_in)
+
+ subject.perform(bulk_import.id)
+ end
+ end
+
context 'when all entities are processed' do
it 'marks bulk import as finished' do
bulk_import = create(:bulk_import, :started)
@@ -34,6 +44,18 @@ RSpec.describe BulkImportWorker do
end
end
+ context 'when all entities are failed' do
+ it 'marks bulk import as failed' do
+ bulk_import = create(:bulk_import, :started)
+ create(:bulk_import_entity, :failed, bulk_import: bulk_import)
+ create(:bulk_import_entity, :failed, bulk_import: bulk_import)
+
+ subject.perform(bulk_import.id)
+
+ expect(bulk_import.reload.failed?).to eq(true)
+ end
+ end
+
context 'when maximum allowed number of import entities in progress' do
it 'reenqueues itself' do
bulk_import = create(:bulk_import, :started)
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index f7838279212..8d528011752 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
it 'requests relations export' do
expected = "/groups/foo%2Fbar/export_relations"
- expect_next_instance_of(BulkImports::Clients::Http) do |client|
+ expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
expect(client).to receive(:post).with(expected).twice
end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index 27151177634..972a4158194 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -8,10 +8,16 @@ RSpec.describe BulkImports::PipelineWorker do
def initialize(_); end
def run; end
+
+ def self.ndjson_pipeline?
+ false
+ end
end
end
- let_it_be(:entity) { create(:bulk_import_entity) }
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) }
before do
stub_const('FakePipeline', pipeline_class)
@@ -27,6 +33,7 @@ RSpec.describe BulkImports::PipelineWorker do
expect(BulkImports::Stage)
.to receive(:pipeline_exists?)
.with('FakePipeline')
+ .twice
.and_return(true)
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
@@ -122,4 +129,117 @@ RSpec.describe BulkImports::PipelineWorker do
expect(pipeline_tracker.jid).to eq('jid')
end
end
+
+ context 'when ndjson pipeline' do
+ let(:ndjson_pipeline) do
+ Class.new do
+ def initialize(_); end
+
+ def run; end
+
+ def self.ndjson_pipeline?
+ true
+ end
+
+ def self.relation
+ 'test'
+ end
+ end
+ end
+
+ let(:pipeline_tracker) do
+ create(
+ :bulk_import_tracker,
+ entity: entity,
+ pipeline_name: 'NdjsonPipeline'
+ )
+ end
+
+ before do
+ stub_const('NdjsonPipeline', ndjson_pipeline)
+ allow(BulkImports::Stage)
+ .to receive(:pipeline_exists?)
+ .with('NdjsonPipeline')
+ .and_return(true)
+ end
+
+ it 'runs the pipeline successfully' do
+ allow_next_instance_of(BulkImports::ExportStatus) do |status|
+ allow(status).to receive(:started?).and_return(false)
+ allow(status).to receive(:failed?).and_return(false)
+ end
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.status_name).to eq(:finished)
+ end
+
+ context 'when export status is started' do
+ it 'reenqueues pipeline worker' do
+ allow_next_instance_of(BulkImports::ExportStatus) do |status|
+ allow(status).to receive(:started?).and_return(true)
+ allow(status).to receive(:failed?).and_return(false)
+ end
+
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(
+ described_class::NDJSON_PIPELINE_PERFORM_DELAY,
+ pipeline_tracker.id,
+ pipeline_tracker.stage,
+ entity.id
+ )
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+ 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)
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:error)
+ .with(
+ worker: described_class.name,
+ pipeline_name: 'NdjsonPipeline',
+ entity_id: entity.id,
+ message: 'Pipeline timeout'
+ )
+ end
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.status_name).to eq(:failed)
+
+ entity.update!(created_at: old_created_at)
+ end
+ end
+
+ context 'when export status is failed' do
+ it 'marks as failed and logs the error' do
+ allow_next_instance_of(BulkImports::ExportStatus) do |status|
+ allow(status).to receive(:failed?).and_return(true)
+ allow(status).to receive(:error).and_return('Error!')
+ end
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger)
+ .to receive(:error)
+ .with(
+ worker: described_class.name,
+ pipeline_name: 'NdjsonPipeline',
+ entity_id: entity.id,
+ message: 'Error!'
+ )
+ end
+
+ subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
+
+ expect(pipeline_tracker.reload.status_name).to eq(:failed)
+ end
+ end
+ end
end
diff --git a/spec/workers/ci/initial_pipeline_process_worker_spec.rb b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
index 5db9287fe96..5fb8671fd5c 100644
--- a/spec/workers/ci/initial_pipeline_process_worker_spec.rb
+++ b/spec/workers/ci/initial_pipeline_process_worker_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe Ci::InitialPipelineProcessWorker do
describe '#perform' do
- let_it_be(:pipeline) { create(:ci_pipeline, :with_job, status: :created) }
+ let_it_be_with_reload(:pipeline) do
+ create(:ci_pipeline, :with_job, status: :created)
+ end
include_examples 'an idempotent worker' do
let(:job_args) { pipeline.id }
diff --git a/spec/workers/clusters/applications/activate_service_worker_spec.rb b/spec/workers/clusters/applications/activate_service_worker_spec.rb
index c157c57888e..7b05b76bebc 100644
--- a/spec/workers/clusters/applications/activate_service_worker_spec.rb
+++ b/spec/workers/clusters/applications/activate_service_worker_spec.rb
@@ -8,13 +8,13 @@ RSpec.describe Clusters::Applications::ActivateServiceWorker, '#perform' do
let(:service_name) { 'prometheus' }
before do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
end
context 'cluster type: group' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
- let(:cluster) { create(:cluster_for_group, :with_installed_helm, groups: [group]) }
+ let(:cluster) { create(:cluster_for_group, groups: [group]) }
it 'ensures Prometheus service is activated' do
expect { described_class.new.perform(cluster.id, service_name) }
@@ -24,7 +24,7 @@ RSpec.describe Clusters::Applications::ActivateServiceWorker, '#perform' do
context 'cluster type: project' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
+ let(:cluster) { create(:cluster, projects: [project]) }
it 'ensures Prometheus service is activated' do
expect { described_class.new.perform(cluster.id, service_name) }
diff --git a/spec/workers/clusters/applications/deactivate_service_worker_spec.rb b/spec/workers/clusters/applications/deactivate_service_worker_spec.rb
index 18cceaaf3b1..4068c5c9eaa 100644
--- a/spec/workers/clusters/applications/deactivate_service_worker_spec.rb
+++ b/spec/workers/clusters/applications/deactivate_service_worker_spec.rb
@@ -6,19 +6,19 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do
context 'cluster exists' do
describe 'prometheus service' do
let(:service_name) { 'prometheus' }
- let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:integration) { create(:clusters_integrations_prometheus, cluster: cluster) }
context 'prometheus service exists' do
let!(:prometheus_service) { create(:prometheus_service, project: project, manual_configuration: false, active: true) }
before do
- application.delete # prometheus service before save synchronises active stated with application existance.
+ integration.delete # prometheus service before save synchronises active stated with integration existence.
end
context 'cluster type: group' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
- let(:cluster) { create(:cluster_for_group, :with_installed_helm, groups: [group]) }
+ let(:cluster) { create(:cluster_for_group, groups: [group]) }
it 'ensures Prometheus service is deactivated' do
expect { described_class.new.perform(cluster.id, service_name) }
@@ -28,7 +28,7 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do
context 'cluster type: project' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
+ let(:cluster) { create(:cluster, projects: [project]) }
it 'ensures Prometheus service is deactivated' do
expect { described_class.new.perform(cluster.id, service_name) }
@@ -38,7 +38,7 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do
context 'cluster type: instance' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm, :instance) }
+ let(:cluster) { create(:cluster, :instance) }
it 'ensures Prometheus service is deactivated' do
expect { described_class.new.perform(cluster.id, service_name) }
@@ -50,7 +50,7 @@ RSpec.describe Clusters::Applications::DeactivateServiceWorker, '#perform' do
context 'prometheus service does not exist' do
context 'cluster type: project' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
+ let(:cluster) { create(:cluster, projects: [project]) }
it 'does not raise errors' do
expect { described_class.new.perform(cluster.id, service_name) }.not_to raise_error
diff --git a/spec/workers/clusters/cleanup/app_worker_spec.rb b/spec/workers/clusters/cleanup/app_worker_spec.rb
deleted file mode 100644
index 661468f037f..00000000000
--- a/spec/workers/clusters/cleanup/app_worker_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Cleanup::AppWorker do
- describe '#perform' do
- subject { worker_instance.perform(cluster.id) }
-
- let!(:worker_instance) { described_class.new }
- let!(:cluster) { create(:cluster, :project, :cleanup_uninstalling_applications, provider_type: :gcp) }
- let!(:logger) { worker_instance.send(:logger) }
-
- it_behaves_like 'cluster cleanup worker base specs'
-
- context 'when exceeded the execution limit' do
- subject { worker_instance.perform(cluster.id, worker_instance.send(:execution_limit)) }
-
- let(:worker_instance) { described_class.new }
- let(:logger) { worker_instance.send(:logger) }
- let!(:helm) { create(:clusters_applications_helm, :installed, cluster: cluster) }
- let!(:ingress) { create(:clusters_applications_ingress, :scheduled, cluster: cluster) }
-
- it 'logs the error' do
- expect(logger).to receive(:error)
- .with(
- hash_including(
- exception: 'ClusterCleanupMethods::ExceededExecutionLimitError',
- cluster_id: kind_of(Integer),
- class_name: described_class.name,
- applications: "helm:installed,ingress:scheduled",
- cleanup_status: cluster.cleanup_status_name,
- event: :failed_to_remove_cluster_and_resources,
- message: "exceeded execution limit of 10 tries"
- )
- )
-
- subject
- end
- end
- end
-end
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 5c1a1d3ae8f..29c69ff8b4b 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -176,6 +176,40 @@ RSpec.describe ApplicationWorker do
end
end
+ describe '.perform_async' do
+ shared_examples_for 'worker utilizes load balancing capabilities' do |data_consistency|
+ before do
+ worker.data_consistency(data_consistency)
+ end
+
+ it 'call perform_in' do
+ expect(worker).to receive(:perform_in).with(described_class::DEFAULT_DELAY_INTERVAL.seconds, 123)
+
+ worker.perform_async(123)
+ end
+ end
+
+ context 'when workers data consistency is :sticky' do
+ it_behaves_like 'worker utilizes load balancing capabilities', :sticky
+ end
+
+ context 'when workers data consistency is :delayed' do
+ it_behaves_like 'worker utilizes load balancing capabilities', :delayed
+ end
+
+ context 'when workers data consistency is :always' do
+ before do
+ worker.data_consistency(:always)
+ end
+
+ it 'does not call perform_in' do
+ expect(worker).not_to receive(:perform_in)
+
+ worker.perform_async
+ end
+ end
+ end
+
describe '.bulk_perform_async' do
it 'enqueues jobs in bulk' do
Sidekiq::Testing.fake! do
diff --git a/spec/workers/concerns/worker_attributes_spec.rb b/spec/workers/concerns/worker_attributes_spec.rb
index a654ecbd3e2..d4b17c65f46 100644
--- a/spec/workers/concerns/worker_attributes_spec.rb
+++ b/spec/workers/concerns/worker_attributes_spec.rb
@@ -62,6 +62,12 @@ RSpec.describe WorkerAttributes do
end
describe '.idempotent!' do
+ it 'sets `idempotent` attribute of the worker class to true' do
+ worker.idempotent!
+
+ expect(worker.send(:class_attributes)[:idempotent]).to eq(true)
+ end
+
context 'when data consistency is not :always' do
it 'raise exception' do
worker.data_consistency(:sticky)
@@ -71,4 +77,66 @@ RSpec.describe WorkerAttributes do
end
end
end
+
+ describe '.idempotent?' do
+ subject(:idempotent?) { worker.idempotent? }
+
+ context 'when the worker is idempotent' do
+ before do
+ worker.idempotent!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when the worker is not idempotent' do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.deduplicate' do
+ it 'sets deduplication_strategy and deduplication_options' do
+ worker.deduplicate(:until_executing, including_scheduled: true)
+
+ expect(worker.send(:class_attributes)[:deduplication_strategy]).to eq(:until_executing)
+ expect(worker.send(:class_attributes)[:deduplication_options]).to eq(including_scheduled: true)
+ end
+ end
+
+ describe '#deduplication_enabled?' do
+ subject(:deduplication_enabled?) { worker.deduplication_enabled? }
+
+ context 'when no feature flag is set' do
+ before do
+ worker.deduplicate(:until_executing)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when feature flag is set' do
+ before do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
+
+ worker.deduplicate(:until_executing, feature_flag: :my_feature_flag)
+ end
+
+ context 'when the FF is enabled' do
+ before do
+ stub_feature_flags(my_feature_flag: true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when the FF is disabled' do
+ before do
+ stub_feature_flags(my_feature_flag: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
end
diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
index 04f568515ed..c399697cbe0 100644
--- a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
@@ -85,7 +85,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
context 'with policy running shortly' do
before do
- repository.cleanup_unfinished! if loopless_enabled?
+ repository.cleanup_unfinished!
policy.update_column(:next_run_at, 1.minute.from_now)
end
@@ -108,371 +108,261 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
it 'skips the repository' do
expect(ContainerExpirationPolicies::CleanupService).not_to receive(:new)
- if loopless_enabled?
- expect { subject }
- .to not_change { ContainerRepository.waiting_for_cleanup.count }
- .and not_change { repository.reload.expiration_policy_cleanup_status }
- else
- expect { subject }.to change { ContainerRepository.waiting_for_cleanup.count }.from(1).to(0)
- expect(repository.reload.cleanup_unscheduled?).to be_truthy
- end
+ expect { subject }
+ .to not_change { ContainerRepository.waiting_for_cleanup.count }
+ .and not_change { repository.reload.expiration_policy_cleanup_status }
end
end
end
- context 'with loopless enabled' do
+ context 'with repository in cleanup unscheduled state' do
before do
- stub_feature_flags(container_registry_expiration_policies_loopless: true)
+ policy.update_column(:next_run_at, 5.minutes.ago)
end
- context 'with repository in cleanup unscheduled state' do
- before do
- policy.update_column(:next_run_at, 5.minutes.ago)
- end
+ it_behaves_like 'handling all repository conditions'
+ end
- it_behaves_like 'handling all repository conditions'
+ context 'with repository in cleanup unfinished state' do
+ before do
+ repository.cleanup_unfinished!
end
- context 'with repository in cleanup unfinished state' do
- before do
- repository.cleanup_unfinished!
- end
+ it_behaves_like 'handling all repository conditions'
+ end
- it_behaves_like 'handling all repository conditions'
- end
+ context 'container repository selection' do
+ where(:repository_cleanup_status, :repository_policy_status, :other_repository_cleanup_status, :other_repository_policy_status, :expected_selected_repository) do
+ :unscheduled | :disabled | :unscheduled | :disabled | :none
+ :unscheduled | :disabled | :unscheduled | :runnable | :other_repository
+ :unscheduled | :disabled | :unscheduled | :not_runnable | :none
- context 'container repository selection' do
- where(:repository_cleanup_status, :repository_policy_status, :other_repository_cleanup_status, :other_repository_policy_status, :expected_selected_repository) do
- :unscheduled | :disabled | :unscheduled | :disabled | :none
- :unscheduled | :disabled | :unscheduled | :runnable | :other_repository
- :unscheduled | :disabled | :unscheduled | :not_runnable | :none
+ :unscheduled | :disabled | :scheduled | :disabled | :none
+ :unscheduled | :disabled | :scheduled | :runnable | :other_repository
+ :unscheduled | :disabled | :scheduled | :not_runnable | :none
- :unscheduled | :disabled | :scheduled | :disabled | :none
- :unscheduled | :disabled | :scheduled | :runnable | :other_repository
- :unscheduled | :disabled | :scheduled | :not_runnable | :none
+ :unscheduled | :disabled | :unfinished | :disabled | :none
+ :unscheduled | :disabled | :unfinished | :runnable | :other_repository
+ :unscheduled | :disabled | :unfinished | :not_runnable | :other_repository
- :unscheduled | :disabled | :unfinished | :disabled | :none
- :unscheduled | :disabled | :unfinished | :runnable | :other_repository
- :unscheduled | :disabled | :unfinished | :not_runnable | :other_repository
+ :unscheduled | :disabled | :ongoing | :disabled | :none
+ :unscheduled | :disabled | :ongoing | :runnable | :none
+ :unscheduled | :disabled | :ongoing | :not_runnable | :none
- :unscheduled | :disabled | :ongoing | :disabled | :none
- :unscheduled | :disabled | :ongoing | :runnable | :none
- :unscheduled | :disabled | :ongoing | :not_runnable | :none
+ :unscheduled | :runnable | :unscheduled | :disabled | :repository
+ :unscheduled | :runnable | :unscheduled | :runnable | :repository
+ :unscheduled | :runnable | :unscheduled | :not_runnable | :repository
- :unscheduled | :runnable | :unscheduled | :disabled | :repository
- :unscheduled | :runnable | :unscheduled | :runnable | :repository
- :unscheduled | :runnable | :unscheduled | :not_runnable | :repository
+ :unscheduled | :runnable | :scheduled | :disabled | :repository
+ :unscheduled | :runnable | :scheduled | :runnable | :repository
+ :unscheduled | :runnable | :scheduled | :not_runnable | :repository
- :unscheduled | :runnable | :scheduled | :disabled | :repository
- :unscheduled | :runnable | :scheduled | :runnable | :repository
- :unscheduled | :runnable | :scheduled | :not_runnable | :repository
+ :unscheduled | :runnable | :unfinished | :disabled | :repository
+ :unscheduled | :runnable | :unfinished | :runnable | :repository
+ :unscheduled | :runnable | :unfinished | :not_runnable | :repository
- :unscheduled | :runnable | :unfinished | :disabled | :repository
- :unscheduled | :runnable | :unfinished | :runnable | :repository
- :unscheduled | :runnable | :unfinished | :not_runnable | :repository
+ :unscheduled | :runnable | :ongoing | :disabled | :repository
+ :unscheduled | :runnable | :ongoing | :runnable | :repository
+ :unscheduled | :runnable | :ongoing | :not_runnable | :repository
- :unscheduled | :runnable | :ongoing | :disabled | :repository
- :unscheduled | :runnable | :ongoing | :runnable | :repository
- :unscheduled | :runnable | :ongoing | :not_runnable | :repository
+ :scheduled | :disabled | :unscheduled | :disabled | :none
+ :scheduled | :disabled | :unscheduled | :runnable | :other_repository
+ :scheduled | :disabled | :unscheduled | :not_runnable | :none
- :scheduled | :disabled | :unscheduled | :disabled | :none
- :scheduled | :disabled | :unscheduled | :runnable | :other_repository
- :scheduled | :disabled | :unscheduled | :not_runnable | :none
+ :scheduled | :disabled | :scheduled | :disabled | :none
+ :scheduled | :disabled | :scheduled | :runnable | :other_repository
+ :scheduled | :disabled | :scheduled | :not_runnable | :none
- :scheduled | :disabled | :scheduled | :disabled | :none
- :scheduled | :disabled | :scheduled | :runnable | :other_repository
- :scheduled | :disabled | :scheduled | :not_runnable | :none
+ :scheduled | :disabled | :unfinished | :disabled | :none
+ :scheduled | :disabled | :unfinished | :runnable | :other_repository
+ :scheduled | :disabled | :unfinished | :not_runnable | :other_repository
- :scheduled | :disabled | :unfinished | :disabled | :none
- :scheduled | :disabled | :unfinished | :runnable | :other_repository
- :scheduled | :disabled | :unfinished | :not_runnable | :other_repository
+ :scheduled | :disabled | :ongoing | :disabled | :none
+ :scheduled | :disabled | :ongoing | :runnable | :none
+ :scheduled | :disabled | :ongoing | :not_runnable | :none
- :scheduled | :disabled | :ongoing | :disabled | :none
- :scheduled | :disabled | :ongoing | :runnable | :none
- :scheduled | :disabled | :ongoing | :not_runnable | :none
+ :scheduled | :runnable | :unscheduled | :disabled | :repository
+ :scheduled | :runnable | :unscheduled | :runnable | :other_repository
+ :scheduled | :runnable | :unscheduled | :not_runnable | :repository
- :scheduled | :runnable | :unscheduled | :disabled | :repository
- :scheduled | :runnable | :unscheduled | :runnable | :other_repository
- :scheduled | :runnable | :unscheduled | :not_runnable | :repository
+ :scheduled | :runnable | :scheduled | :disabled | :repository
+ :scheduled | :runnable | :scheduled | :runnable | :repository
+ :scheduled | :runnable | :scheduled | :not_runnable | :repository
- :scheduled | :runnable | :scheduled | :disabled | :repository
- :scheduled | :runnable | :scheduled | :runnable | :repository
- :scheduled | :runnable | :scheduled | :not_runnable | :repository
+ :scheduled | :runnable | :unfinished | :disabled | :repository
+ :scheduled | :runnable | :unfinished | :runnable | :repository
+ :scheduled | :runnable | :unfinished | :not_runnable | :repository
- :scheduled | :runnable | :unfinished | :disabled | :repository
- :scheduled | :runnable | :unfinished | :runnable | :repository
- :scheduled | :runnable | :unfinished | :not_runnable | :repository
+ :scheduled | :runnable | :ongoing | :disabled | :repository
+ :scheduled | :runnable | :ongoing | :runnable | :repository
+ :scheduled | :runnable | :ongoing | :not_runnable | :repository
- :scheduled | :runnable | :ongoing | :disabled | :repository
- :scheduled | :runnable | :ongoing | :runnable | :repository
- :scheduled | :runnable | :ongoing | :not_runnable | :repository
+ :scheduled | :not_runnable | :unscheduled | :disabled | :none
+ :scheduled | :not_runnable | :unscheduled | :runnable | :other_repository
+ :scheduled | :not_runnable | :unscheduled | :not_runnable | :none
- :scheduled | :not_runnable | :unscheduled | :disabled | :none
- :scheduled | :not_runnable | :unscheduled | :runnable | :other_repository
- :scheduled | :not_runnable | :unscheduled | :not_runnable | :none
+ :scheduled | :not_runnable | :scheduled | :disabled | :none
+ :scheduled | :not_runnable | :scheduled | :runnable | :other_repository
+ :scheduled | :not_runnable | :scheduled | :not_runnable | :none
- :scheduled | :not_runnable | :scheduled | :disabled | :none
- :scheduled | :not_runnable | :scheduled | :runnable | :other_repository
- :scheduled | :not_runnable | :scheduled | :not_runnable | :none
+ :scheduled | :not_runnable | :unfinished | :disabled | :none
+ :scheduled | :not_runnable | :unfinished | :runnable | :other_repository
+ :scheduled | :not_runnable | :unfinished | :not_runnable | :other_repository
- :scheduled | :not_runnable | :unfinished | :disabled | :none
- :scheduled | :not_runnable | :unfinished | :runnable | :other_repository
- :scheduled | :not_runnable | :unfinished | :not_runnable | :other_repository
+ :scheduled | :not_runnable | :ongoing | :disabled | :none
+ :scheduled | :not_runnable | :ongoing | :runnable | :none
+ :scheduled | :not_runnable | :ongoing | :not_runnable | :none
- :scheduled | :not_runnable | :ongoing | :disabled | :none
- :scheduled | :not_runnable | :ongoing | :runnable | :none
- :scheduled | :not_runnable | :ongoing | :not_runnable | :none
+ :unfinished | :disabled | :unscheduled | :disabled | :none
+ :unfinished | :disabled | :unscheduled | :runnable | :other_repository
+ :unfinished | :disabled | :unscheduled | :not_runnable | :none
- :unfinished | :disabled | :unscheduled | :disabled | :none
- :unfinished | :disabled | :unscheduled | :runnable | :other_repository
- :unfinished | :disabled | :unscheduled | :not_runnable | :none
+ :unfinished | :disabled | :scheduled | :disabled | :none
+ :unfinished | :disabled | :scheduled | :runnable | :other_repository
+ :unfinished | :disabled | :scheduled | :not_runnable | :none
- :unfinished | :disabled | :scheduled | :disabled | :none
- :unfinished | :disabled | :scheduled | :runnable | :other_repository
- :unfinished | :disabled | :scheduled | :not_runnable | :none
+ :unfinished | :disabled | :unfinished | :disabled | :none
+ :unfinished | :disabled | :unfinished | :runnable | :other_repository
+ :unfinished | :disabled | :unfinished | :not_runnable | :other_repository
- :unfinished | :disabled | :unfinished | :disabled | :none
- :unfinished | :disabled | :unfinished | :runnable | :other_repository
- :unfinished | :disabled | :unfinished | :not_runnable | :other_repository
+ :unfinished | :disabled | :ongoing | :disabled | :none
+ :unfinished | :disabled | :ongoing | :runnable | :none
+ :unfinished | :disabled | :ongoing | :not_runnable | :none
- :unfinished | :disabled | :ongoing | :disabled | :none
- :unfinished | :disabled | :ongoing | :runnable | :none
- :unfinished | :disabled | :ongoing | :not_runnable | :none
+ :unfinished | :runnable | :unscheduled | :disabled | :repository
+ :unfinished | :runnable | :unscheduled | :runnable | :other_repository
+ :unfinished | :runnable | :unscheduled | :not_runnable | :repository
+
+ :unfinished | :runnable | :scheduled | :disabled | :repository
+ :unfinished | :runnable | :scheduled | :runnable | :other_repository
+ :unfinished | :runnable | :scheduled | :not_runnable | :repository
- :unfinished | :runnable | :unscheduled | :disabled | :repository
- :unfinished | :runnable | :unscheduled | :runnable | :other_repository
- :unfinished | :runnable | :unscheduled | :not_runnable | :repository
-
- :unfinished | :runnable | :scheduled | :disabled | :repository
- :unfinished | :runnable | :scheduled | :runnable | :other_repository
- :unfinished | :runnable | :scheduled | :not_runnable | :repository
+ :unfinished | :runnable | :unfinished | :disabled | :repository
+ :unfinished | :runnable | :unfinished | :runnable | :repository
+ :unfinished | :runnable | :unfinished | :not_runnable | :repository
- :unfinished | :runnable | :unfinished | :disabled | :repository
- :unfinished | :runnable | :unfinished | :runnable | :repository
- :unfinished | :runnable | :unfinished | :not_runnable | :repository
+ :unfinished | :runnable | :ongoing | :disabled | :repository
+ :unfinished | :runnable | :ongoing | :runnable | :repository
+ :unfinished | :runnable | :ongoing | :not_runnable | :repository
- :unfinished | :runnable | :ongoing | :disabled | :repository
- :unfinished | :runnable | :ongoing | :runnable | :repository
- :unfinished | :runnable | :ongoing | :not_runnable | :repository
+ :unfinished | :not_runnable | :unscheduled | :disabled | :repository
+ :unfinished | :not_runnable | :unscheduled | :runnable | :other_repository
+ :unfinished | :not_runnable | :unscheduled | :not_runnable | :repository
- :unfinished | :not_runnable | :unscheduled | :disabled | :repository
- :unfinished | :not_runnable | :unscheduled | :runnable | :other_repository
- :unfinished | :not_runnable | :unscheduled | :not_runnable | :repository
+ :unfinished | :not_runnable | :scheduled | :disabled | :repository
+ :unfinished | :not_runnable | :scheduled | :runnable | :other_repository
+ :unfinished | :not_runnable | :scheduled | :not_runnable | :repository
- :unfinished | :not_runnable | :scheduled | :disabled | :repository
- :unfinished | :not_runnable | :scheduled | :runnable | :other_repository
- :unfinished | :not_runnable | :scheduled | :not_runnable | :repository
+ :unfinished | :not_runnable | :unfinished | :disabled | :repository
+ :unfinished | :not_runnable | :unfinished | :runnable | :repository
+ :unfinished | :not_runnable | :unfinished | :not_runnable | :repository
- :unfinished | :not_runnable | :unfinished | :disabled | :repository
- :unfinished | :not_runnable | :unfinished | :runnable | :repository
- :unfinished | :not_runnable | :unfinished | :not_runnable | :repository
+ :unfinished | :not_runnable | :ongoing | :disabled | :repository
+ :unfinished | :not_runnable | :ongoing | :runnable | :repository
+ :unfinished | :not_runnable | :ongoing | :not_runnable | :repository
- :unfinished | :not_runnable | :ongoing | :disabled | :repository
- :unfinished | :not_runnable | :ongoing | :runnable | :repository
- :unfinished | :not_runnable | :ongoing | :not_runnable | :repository
+ :ongoing | :disabled | :unscheduled | :disabled | :none
+ :ongoing | :disabled | :unscheduled | :runnable | :other_repository
+ :ongoing | :disabled | :unscheduled | :not_runnable | :none
- :ongoing | :disabled | :unscheduled | :disabled | :none
- :ongoing | :disabled | :unscheduled | :runnable | :other_repository
- :ongoing | :disabled | :unscheduled | :not_runnable | :none
+ :ongoing | :disabled | :scheduled | :disabled | :none
+ :ongoing | :disabled | :scheduled | :runnable | :other_repository
+ :ongoing | :disabled | :scheduled | :not_runnable | :none
- :ongoing | :disabled | :scheduled | :disabled | :none
- :ongoing | :disabled | :scheduled | :runnable | :other_repository
- :ongoing | :disabled | :scheduled | :not_runnable | :none
+ :ongoing | :disabled | :unfinished | :disabled | :none
+ :ongoing | :disabled | :unfinished | :runnable | :other_repository
+ :ongoing | :disabled | :unfinished | :not_runnable | :other_repository
- :ongoing | :disabled | :unfinished | :disabled | :none
- :ongoing | :disabled | :unfinished | :runnable | :other_repository
- :ongoing | :disabled | :unfinished | :not_runnable | :other_repository
+ :ongoing | :disabled | :ongoing | :disabled | :none
+ :ongoing | :disabled | :ongoing | :runnable | :none
+ :ongoing | :disabled | :ongoing | :not_runnable | :none
- :ongoing | :disabled | :ongoing | :disabled | :none
- :ongoing | :disabled | :ongoing | :runnable | :none
- :ongoing | :disabled | :ongoing | :not_runnable | :none
+ :ongoing | :runnable | :unscheduled | :disabled | :none
+ :ongoing | :runnable | :unscheduled | :runnable | :other_repository
+ :ongoing | :runnable | :unscheduled | :not_runnable | :none
- :ongoing | :runnable | :unscheduled | :disabled | :none
- :ongoing | :runnable | :unscheduled | :runnable | :other_repository
- :ongoing | :runnable | :unscheduled | :not_runnable | :none
+ :ongoing | :runnable | :scheduled | :disabled | :none
+ :ongoing | :runnable | :scheduled | :runnable | :other_repository
+ :ongoing | :runnable | :scheduled | :not_runnable | :none
- :ongoing | :runnable | :scheduled | :disabled | :none
- :ongoing | :runnable | :scheduled | :runnable | :other_repository
- :ongoing | :runnable | :scheduled | :not_runnable | :none
+ :ongoing | :runnable | :unfinished | :disabled | :none
+ :ongoing | :runnable | :unfinished | :runnable | :other_repository
+ :ongoing | :runnable | :unfinished | :not_runnable | :other_repository
- :ongoing | :runnable | :unfinished | :disabled | :none
- :ongoing | :runnable | :unfinished | :runnable | :other_repository
- :ongoing | :runnable | :unfinished | :not_runnable | :other_repository
+ :ongoing | :runnable | :ongoing | :disabled | :none
+ :ongoing | :runnable | :ongoing | :runnable | :none
+ :ongoing | :runnable | :ongoing | :not_runnable | :none
- :ongoing | :runnable | :ongoing | :disabled | :none
- :ongoing | :runnable | :ongoing | :runnable | :none
- :ongoing | :runnable | :ongoing | :not_runnable | :none
+ :ongoing | :not_runnable | :unscheduled | :disabled | :none
+ :ongoing | :not_runnable | :unscheduled | :runnable | :other_repository
+ :ongoing | :not_runnable | :unscheduled | :not_runnable | :none
- :ongoing | :not_runnable | :unscheduled | :disabled | :none
- :ongoing | :not_runnable | :unscheduled | :runnable | :other_repository
- :ongoing | :not_runnable | :unscheduled | :not_runnable | :none
+ :ongoing | :not_runnable | :scheduled | :disabled | :none
+ :ongoing | :not_runnable | :scheduled | :runnable | :other_repository
+ :ongoing | :not_runnable | :scheduled | :not_runnable | :none
- :ongoing | :not_runnable | :scheduled | :disabled | :none
- :ongoing | :not_runnable | :scheduled | :runnable | :other_repository
- :ongoing | :not_runnable | :scheduled | :not_runnable | :none
+ :ongoing | :not_runnable | :unfinished | :disabled | :none
+ :ongoing | :not_runnable | :unfinished | :runnable | :other_repository
+ :ongoing | :not_runnable | :unfinished | :not_runnable | :other_repository
- :ongoing | :not_runnable | :unfinished | :disabled | :none
- :ongoing | :not_runnable | :unfinished | :runnable | :other_repository
- :ongoing | :not_runnable | :unfinished | :not_runnable | :other_repository
+ :ongoing | :not_runnable | :ongoing | :disabled | :none
+ :ongoing | :not_runnable | :ongoing | :runnable | :none
+ :ongoing | :not_runnable | :ongoing | :not_runnable | :none
+ end
- :ongoing | :not_runnable | :ongoing | :disabled | :none
- :ongoing | :not_runnable | :ongoing | :runnable | :none
- :ongoing | :not_runnable | :ongoing | :not_runnable | :none
+ with_them do
+ before do
+ update_container_repository(repository, repository_cleanup_status, repository_policy_status)
+ update_container_repository(other_repository, other_repository_cleanup_status, other_repository_policy_status)
end
- with_them do
- before do
- update_container_repository(repository, repository_cleanup_status, repository_policy_status)
- update_container_repository(other_repository, other_repository_cleanup_status, other_repository_policy_status)
- end
-
- subject { worker.send(:container_repository) }
-
- if params[:expected_selected_repository] == :none
- it 'does not select any repository' do
- expect(subject).to eq(nil)
- end
- else
- it 'does select a repository' do
- selected_repository = expected_selected_repository == :repository ? repository : other_repository
+ subject { worker.send(:container_repository) }
- expect(subject).to eq(selected_repository)
- end
+ if params[:expected_selected_repository] == :none
+ it 'does not select any repository' do
+ expect(subject).to eq(nil)
end
+ else
+ it 'does select a repository' do
+ selected_repository = expected_selected_repository == :repository ? repository : other_repository
- def update_container_repository(container_repository, cleanup_status, policy_status)
- container_repository.update_column(:expiration_policy_cleanup_status, "cleanup_#{cleanup_status}")
-
- policy = container_repository.project.container_expiration_policy
-
- case policy_status
- when :disabled
- policy.update!(enabled: false)
- when :runnable
- policy.update!(enabled: true)
- policy.update_column(:next_run_at, 5.minutes.ago)
- when :not_runnable
- policy.update!(enabled: true)
- policy.update_column(:next_run_at, 5.minutes.from_now)
- end
+ expect(subject).to eq(selected_repository)
end
end
- end
- context 'with another repository in cleanup unfinished state' do
- let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
+ def update_container_repository(container_repository, cleanup_status, policy_status)
+ container_repository.update_column(:expiration_policy_cleanup_status, "cleanup_#{cleanup_status}")
- before do
- policy.update_column(:next_run_at, 5.minutes.ago)
- end
+ policy = container_repository.project.container_expiration_policy
- it 'process the cleanup scheduled repository first' do
- service_response = cleanup_service_response(repository: repository)
- expect(ContainerExpirationPolicies::CleanupService)
- .to receive(:new).with(repository).and_return(double(execute: service_response))
- expect_log_extra_metadata(service_response: service_response)
-
- subject
+ case policy_status
+ when :disabled
+ policy.update!(enabled: false)
+ when :runnable
+ policy.update!(enabled: true)
+ policy.update_column(:next_run_at, 5.minutes.ago)
+ when :not_runnable
+ policy.update!(enabled: true)
+ policy.update_column(:next_run_at, 5.minutes.from_now)
+ end
end
end
end
- context 'with loopless disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_loopless: false)
- end
-
- context 'with repository in cleanup scheduled state' do
- it_behaves_like 'handling all repository conditions'
- end
-
- context 'with repository in cleanup unfinished state' do
- before do
- repository.cleanup_unfinished!
- end
-
- it_behaves_like 'handling all repository conditions'
- end
-
- context 'with another repository in cleanup unfinished state' do
- let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
-
- it 'process the cleanup scheduled repository first' do
- service_response = cleanup_service_response(repository: repository)
- expect(ContainerExpirationPolicies::CleanupService)
- .to receive(:new).with(repository).and_return(double(execute: service_response))
- expect_log_extra_metadata(service_response: service_response)
-
- subject
- end
- end
-
- context 'with multiple repositories in cleanup unfinished state' do
- let_it_be(:repository2) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 20.minutes.ago) }
- let_it_be(:repository3) { create(:container_repository, :cleanup_unfinished, expiration_policy_started_at: 10.minutes.ago) }
-
- before do
- repository.update!(expiration_policy_cleanup_status: :cleanup_unfinished, expiration_policy_started_at: 30.minutes.ago)
- end
+ context 'with another repository in cleanup unfinished state' do
+ let_it_be(:another_repository) { create(:container_repository, :cleanup_unfinished) }
- it 'process the repository with the oldest expiration_policy_started_at' do
- service_response = cleanup_service_response(repository: repository)
- expect(ContainerExpirationPolicies::CleanupService)
- .to receive(:new).with(repository).and_return(double(execute: service_response))
- expect_log_extra_metadata(service_response: service_response)
-
- subject
- end
- end
-
- context 'with repository in cleanup ongoing state' do
- before do
- repository.cleanup_ongoing!
- end
-
- it 'does not process it' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
-
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
- expect(repository.cleanup_ongoing?).to be_truthy
- end
- end
-
- context 'with no repository in any cleanup state' do
- before do
- repository.cleanup_unscheduled!
- end
-
- it 'does not process it' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
-
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
- expect(repository.cleanup_unscheduled?).to be_truthy
- end
- end
-
- context 'with no container repository waiting' do
- before do
- repository.destroy!
- end
-
- it 'does not execute the cleanup tags service' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
-
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
- end
+ before do
+ policy.update_column(:next_run_at, 5.minutes.ago)
end
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_throttling: false)
- end
-
- it 'is a no-op' do
- expect(Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
+ it 'process the cleanup scheduled repository first' do
+ service_response = cleanup_service_response(repository: repository)
+ expect(ContainerExpirationPolicies::CleanupService)
+ .to receive(:new).with(repository).and_return(double(execute: service_response))
+ expect_log_extra_metadata(service_response: service_response)
- expect { subject }.not_to change { ContainerRepository.waiting_for_cleanup.count }
- end
+ subject
end
end
@@ -509,69 +399,53 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
end
describe '#remaining_work_count' do
- subject { worker.remaining_work_count }
+ let_it_be(:disabled_repository) { create(:container_repository, :cleanup_scheduled) }
- shared_examples 'handling all conditions' do
- context 'with container repositories waiting for cleanup' do
- let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
+ let(:capacity) { 10 }
- it { is_expected.to eq(3) }
+ subject { worker.remaining_work_count }
- it 'logs the work count' do
- expect_log_info(
- cleanup_scheduled_count: 1,
- cleanup_unfinished_count: 2,
- cleanup_total_count: 3
- )
+ before do
+ stub_application_setting(container_registry_expiration_policies_worker_capacity: capacity)
- subject
- end
- end
+ ContainerExpirationPolicy.update_all(enabled: true)
+ repository.project.container_expiration_policy.update_column(:next_run_at, 5.minutes.ago)
+ disabled_repository.project.container_expiration_policy.update_column(:enabled, false)
+ end
- context 'with no container repositories waiting for cleanup' do
- before do
- repository.cleanup_ongoing!
- policy.update_column(:next_run_at, 5.minutes.from_now)
- end
+ context 'with container repositories waiting for cleanup' do
+ let_it_be(:unfinished_repositories) { create_list(:container_repository, 2, :cleanup_unfinished) }
- it { is_expected.to eq(0) }
+ it { is_expected.to eq(3) }
- it 'logs 0 work count' do
- expect_log_info(
- cleanup_scheduled_count: 0,
- cleanup_unfinished_count: 0,
- cleanup_total_count: 0
- )
+ it 'logs the work count' do
+ expect_log_info(
+ cleanup_scheduled_count: 1,
+ cleanup_unfinished_count: 2,
+ cleanup_total_count: 3
+ )
- subject
- end
+ subject
end
end
- context 'with loopless enabled' do
- let_it_be(:disabled_repository) { create(:container_repository, :cleanup_scheduled) }
-
- let(:capacity) { 10 }
-
+ context 'with no container repositories waiting for cleanup' do
before do
- stub_feature_flags(container_registry_expiration_policies_loopless: true)
- stub_application_setting(container_registry_expiration_policies_worker_capacity: capacity)
-
- # loopless mode is more accurate that non loopless: policies need to be enabled
- ContainerExpirationPolicy.update_all(enabled: true)
- repository.project.container_expiration_policy.update_column(:next_run_at, 5.minutes.ago)
- disabled_repository.project.container_expiration_policy.update_column(:enabled, false)
+ repository.cleanup_ongoing!
+ policy.update_column(:next_run_at, 5.minutes.from_now)
end
- it_behaves_like 'handling all conditions'
- end
+ it { is_expected.to eq(0) }
- context 'with loopless disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_loopless: false)
- end
+ it 'logs 0 work count' do
+ expect_log_info(
+ cleanup_scheduled_count: 0,
+ cleanup_unfinished_count: 0,
+ cleanup_total_count: 0
+ )
- it_behaves_like 'handling all conditions'
+ subject
+ end
end
end
@@ -599,8 +473,4 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
expect(worker.logger)
.to receive(:info).with(worker.structured_payload(structure))
end
-
- def loopless_enabled?
- Feature.enabled?(:container_registry_expiration_policies_loopless)
- end
end
diff --git a/spec/workers/container_expiration_policy_worker_spec.rb b/spec/workers/container_expiration_policy_worker_spec.rb
index e8f9a972f10..8562935b0b5 100644
--- a/spec/workers/container_expiration_policy_worker_spec.rb
+++ b/spec/workers/container_expiration_policy_worker_spec.rb
@@ -34,101 +34,18 @@ RSpec.describe ContainerExpirationPolicyWorker do
end
end
- context 'With no container expiration policies' do
- context 'with loopless disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_loopless: false)
- end
-
- it 'does not execute any policies' do
- expect(ContainerRepository).not_to receive(:for_project_id)
-
- expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count }
- end
- end
- end
-
context 'with throttling enabled' do
before do
stub_feature_flags(container_registry_expiration_policies_throttling: true)
end
- context 'with loopless disabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_loopless: false)
- end
-
- context 'with container expiration policies' do
- let_it_be(:container_expiration_policy) { create(:container_expiration_policy, :runnable) }
- let_it_be(:container_repository) { create(:container_repository, project: container_expiration_policy.project) }
-
- before do
- expect(worker).to receive(:with_runnable_policy).and_call_original
- end
-
- context 'with a valid container expiration policy' do
- it 'schedules the next run' do
- expect { subject }.to change { container_expiration_policy.reload.next_run_at }
- end
-
- it 'marks the container repository as scheduled for cleanup' do
- expect { subject }.to change { container_repository.reload.cleanup_scheduled? }.from(false).to(true)
- expect(ContainerRepository.cleanup_scheduled.count).to eq(1)
- end
-
- it 'calls the limited capacity worker' do
- expect(ContainerExpirationPolicies::CleanupContainerRepositoryWorker).to receive(:perform_with_capacity)
-
- subject
- end
- end
-
- context 'with a disabled container expiration policy' do
- before do
- container_expiration_policy.disable!
- end
+ it 'calls the limited capacity worker' do
+ expect(ContainerExpirationPolicies::CleanupContainerRepositoryWorker).to receive(:perform_with_capacity)
- it 'does not run the policy' do
- expect(ContainerRepository).not_to receive(:for_project_id)
-
- expect { subject }.not_to change { ContainerRepository.cleanup_scheduled.count }
- end
- end
-
- context 'with an invalid container expiration policy' do
- let(:user) { container_expiration_policy.project.owner }
-
- before do
- container_expiration_policy.update_column(:name_regex, '*production')
- end
-
- it 'disables the policy and tracks an error' do
- expect(ContainerRepository).not_to receive(:for_project_id)
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(described_class::InvalidPolicyError), container_expiration_policy_id: container_expiration_policy.id)
-
- expect { subject }.to change { container_expiration_policy.reload.enabled }.from(true).to(false)
- expect(ContainerRepository.cleanup_scheduled).to be_empty
- end
- end
- end
-
- it_behaves_like 'handling a taken exclusive lease'
+ subject
end
- context 'with loopless enabled' do
- before do
- stub_feature_flags(container_registry_expiration_policies_loopless: true)
- expect(worker).not_to receive(:with_runnable_policy)
- end
-
- it 'calls the limited capacity worker' do
- expect(ContainerExpirationPolicies::CleanupContainerRepositoryWorker).to receive(:perform_with_capacity)
-
- subject
- end
-
- it_behaves_like 'handling a taken exclusive lease'
- end
+ it_behaves_like 'handling a taken exclusive lease'
end
context 'with throttling disabled' do
@@ -193,5 +110,18 @@ RSpec.describe ContainerExpirationPolicyWorker do
end
end
end
+
+ context 'process stale ongoing cleanups' do
+ let_it_be(:stuck_cleanup) { create(:container_repository, :cleanup_ongoing, expiration_policy_started_at: 1.day.ago) }
+ let_it_be(:container_repository) { create(:container_repository, :cleanup_scheduled) }
+ let_it_be(:container_repository) { create(:container_repository, :cleanup_unfinished) }
+
+ it 'set them as unfinished' do
+ expect { subject }
+ .to change { ContainerRepository.cleanup_ongoing.count }.from(1).to(0)
+ .and change { ContainerRepository.cleanup_unfinished.count }.from(1).to(2)
+ expect(stuck_cleanup.reload).to be_cleanup_unfinished
+ end
+ end
end
end
diff --git a/spec/workers/deployments/execute_hooks_worker_spec.rb b/spec/workers/deployments/execute_hooks_worker_spec.rb
deleted file mode 100644
index fb1dc8cf290..00000000000
--- a/spec/workers/deployments/execute_hooks_worker_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Deployments::ExecuteHooksWorker do
- let(:worker) { described_class.new }
-
- describe '#perform' do
- before do
- allow(ProjectServiceWorker).to receive(:perform_async)
- end
-
- it 'executes project services for deployment_hooks' do
- deployment = create(:deployment, :running)
- project = deployment.project
- service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
-
- expect(ProjectServiceWorker).to receive(:perform_async).with(service.id, an_instance_of(Hash))
-
- worker.perform(deployment.id)
- end
-
- it 'does not execute an inactive service' do
- deployment = create(:deployment, :running)
- project = deployment.project
- create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
-
- expect(ProjectServiceWorker).not_to receive(:perform_async)
-
- worker.perform(deployment.id)
- end
-
- it 'does not execute if a deployment does not exist' do
- expect(ProjectServiceWorker).not_to receive(:perform_async)
-
- worker.perform(non_existing_record_id)
- end
-
- it 'execute webhooks' do
- deployment = create(:deployment, :running)
- project = deployment.project
- web_hook = create(:project_hook, deployment_events: true, project: project)
-
- expect_next_instance_of(WebHookService, web_hook, an_instance_of(Hash), "deployment_hooks") do |service|
- expect(service).to receive(:async_execute)
- end
-
- worker.perform(deployment.id)
- end
- end
-end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index de848e59d57..34d42addef3 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -130,6 +130,7 @@ RSpec.describe 'Every Sidekiq worker' do
'AuthorizedProjectUpdate::ProjectGroupLinkCreateWorker' => 3,
'AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker' => 3,
'AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker' => 3,
+ 'AuthorizedProjectUpdate::UserRefreshFromReplicaWorker' => 3,
'AuthorizedProjectsWorker' => 3,
'AutoDevops::DisableWorker' => 3,
'AutoMergeProcessWorker' => 3,
@@ -165,6 +166,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Ci::ResourceGroups::AssignResourceFromResourceGroupWorker' => 3,
'Ci::TestFailureHistoryWorker' => 3,
'Ci::TriggerDownstreamSubscriptionsWorker' => 3,
+ 'Ci::SyncReportsToReportApprovalRulesWorker' => 3,
'CleanupContainerRepositoryWorker' => 3,
'ClusterConfigureIstioWorker' => 3,
'ClusterInstallAppWorker' => 3,
@@ -195,7 +197,6 @@ RSpec.describe 'Every Sidekiq worker' do
'DeleteUserWorker' => 3,
'Deployments::AutoRollbackWorker' => 3,
'Deployments::DropOlderDeploymentsWorker' => 3,
- 'Deployments::ExecuteHooksWorker' => 3,
'Deployments::FinishedWorker' => 3,
'Deployments::ForwardDeploymentWorker' => 3,
'Deployments::LinkMergeRequestWorker' => 3,
@@ -212,7 +213,6 @@ RSpec.describe 'Every Sidekiq worker' do
'ElasticCommitIndexerWorker' => 2,
'ElasticDeleteProjectWorker' => 2,
'ElasticFullIndexWorker' => 2,
- 'ElasticIndexerWorker' => 2,
'ElasticIndexingControlWorker' => 3,
'ElasticNamespaceIndexerWorker' => 2,
'ElasticNamespaceRolloutWorker' => 2,
@@ -307,8 +307,6 @@ RSpec.describe 'Every Sidekiq worker' do
'IncidentManagement::OncallRotations::PersistAllRotationsShiftsJob' => 3,
'IncidentManagement::OncallRotations::PersistShiftsJob' => 3,
'IncidentManagement::PagerDuty::ProcessIncidentWorker' => 3,
- 'IncidentManagement::ProcessAlertWorker' => 3,
- 'IncidentManagement::ProcessPrometheusAlertWorker' => 3,
'InvalidGpgSignatureUpdateWorker' => 3,
'IrkerWorker' => 3,
'IssuableExportCsvWorker' => 3,
@@ -374,7 +372,6 @@ RSpec.describe 'Every Sidekiq worker' do
'PipelineMetricsWorker' => 3,
'PipelineNotificationWorker' => 3,
'PipelineProcessWorker' => 3,
- 'PipelineUpdateWorker' => 3,
'PostReceive' => 3,
'ProcessCommitWorker' => 3,
'ProjectCacheWorker' => 3,
@@ -433,7 +430,6 @@ RSpec.describe 'Every Sidekiq worker' do
'StoreSecurityScansWorker' => 3,
'SyncSeatLinkRequestWorker' => 20,
'SyncSeatLinkWorker' => 12,
- 'SyncSecurityReportsToReportApprovalRulesWorker' => 3,
'SystemHookPushWorker' => 3,
'TodosDestroyer::ConfidentialEpicWorker' => 3,
'TodosDestroyer::ConfidentialIssueWorker' => 3,
diff --git a/spec/workers/expire_pipeline_cache_worker_spec.rb b/spec/workers/expire_pipeline_cache_worker_spec.rb
index de42eeeab75..6a1a95b8052 100644
--- a/spec/workers/expire_pipeline_cache_worker_spec.rb
+++ b/spec/workers/expire_pipeline_cache_worker_spec.rb
@@ -42,8 +42,15 @@ RSpec.describe ExpirePipelineCacheWorker do
subject.perform(617748)
end
- it_behaves_like 'an idempotent worker' do
- let(:job_args) { [pipeline.id] }
+ skip "with https://gitlab.com/gitlab-org/gitlab/-/issues/325291 resolved" do
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [pipeline.id] }
+ end
end
+
+ it_behaves_like 'worker with data consistency',
+ described_class,
+ feature_flag: :load_balancing_for_expire_pipeline_cache_worker,
+ data_consistency: :delayed
end
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
deleted file mode 100644
index 3df64c35166..00000000000
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'fileutils'
-
-require 'spec_helper'
-
-RSpec.describe GitGarbageCollectWorker do
- let_it_be(:project) { create(:project, :repository) }
-
- let(:lease_uuid) { SecureRandom.uuid }
- let(:lease_key) { "project_housekeeping:#{project.id}" }
- let(:task) { :full_repack }
- let(:params) { [project.id, task, lease_key, lease_uuid] }
-
- subject { described_class.new }
-
- describe "#perform" do
- it 'calls the Projects::GitGarbageGitGarbageCollectWorker with the same params' do
- expect_next_instance_of(Projects::GitGarbageCollectWorker) do |instance|
- expect(instance).to receive(:perform).with(*params)
- end
-
- subject.perform(*params)
- end
- end
-end
diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb
deleted file mode 100644
index 7db9b191677..00000000000
--- a/spec/workers/incident_management/process_alert_worker_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe IncidentManagement::ProcessAlertWorker do
- let_it_be(:project) { create(:project) }
- let_it_be(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) }
-
- describe '#perform' do
- let_it_be(:started_at) { Time.now.rfc3339 }
- let_it_be(:payload) { { 'title' => 'title', 'start_time' => started_at } }
- let_it_be(:alert) { create(:alert_management_alert, project: project, payload: payload, started_at: started_at) }
-
- let(:created_issue) { Issue.last! }
-
- subject { described_class.new.perform(nil, nil, alert.id) }
-
- before do
- allow(Gitlab::AppLogger).to receive(:warn).and_call_original
-
- allow(AlertManagement::CreateAlertIssueService)
- .to receive(:new).with(alert, User.alert_bot)
- .and_call_original
- end
-
- shared_examples 'creates issue successfully' do
- it 'creates an issue' do
- expect(AlertManagement::CreateAlertIssueService)
- .to receive(:new).with(alert, User.alert_bot)
-
- expect { subject }.to change { Issue.count }.by(1)
- end
-
- it 'updates AlertManagement::Alert#issue_id' do
- subject
-
- expect(alert.reload.issue_id).to eq(created_issue.id)
- end
-
- it 'does not write a warning to log' do
- subject
-
- expect(Gitlab::AppLogger).not_to have_received(:warn)
- end
- end
-
- context 'with valid alert' do
- it_behaves_like 'creates issue successfully'
-
- context 'when alert cannot be updated' do
- let_it_be(:alert) { create(:alert_management_alert, :with_validation_errors, project: project, payload: payload) }
-
- it 'updates AlertManagement::Alert#issue_id' do
- expect { subject }.not_to change { alert.reload.issue_id }
- end
-
- it 'logs a warning' do
- subject
-
- expect(Gitlab::AppLogger).to have_received(:warn).with(
- message: 'Cannot process an Incident',
- issue_id: created_issue.id,
- alert_id: alert.id,
- errors: 'Hosts hosts array is over 255 chars'
- )
- end
- end
-
- context 'prometheus alert' do
- let_it_be(:alert) { create(:alert_management_alert, :prometheus, project: project, started_at: started_at) }
-
- it_behaves_like 'creates issue successfully'
- end
- end
-
- context 'with invalid alert' do
- let(:invalid_alert_id) { non_existing_record_id }
-
- subject { described_class.new.perform(nil, nil, invalid_alert_id) }
-
- it 'does not create issues' do
- expect(AlertManagement::CreateAlertIssueService).not_to receive(:new)
-
- expect { subject }.not_to change { Issue.count }
- end
- end
- end
-end
diff --git a/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb b/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb
deleted file mode 100644
index 56f07459a15..00000000000
--- a/spec/workers/incident_management/process_prometheus_alert_worker_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe IncidentManagement::ProcessPrometheusAlertWorker do
- describe '#perform' do
- let_it_be(:project) { create(:project) }
- let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) }
-
- let(:payload_key) { Gitlab::AlertManagement::Payload::Prometheus.new(project: project, payload: alert_params).gitlab_fingerprint }
- let!(:prometheus_alert_event) { create(:prometheus_alert_event, prometheus_alert: prometheus_alert, payload_key: payload_key) }
- let!(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) }
-
- let(:alert_params) do
- {
- startsAt: prometheus_alert.created_at.rfc3339,
- labels: {
- gitlab_alert_id: prometheus_alert.prometheus_metric_id
- }
- }.with_indifferent_access
- end
-
- it 'does nothing' do
- expect { subject.perform(project.id, alert_params) }
- .not_to change(Issue, :count)
- end
- end
-end
diff --git a/spec/workers/issue_placement_worker_spec.rb b/spec/workers/issue_placement_worker_spec.rb
index e0c17bfadee..780790dbb1b 100644
--- a/spec/workers/issue_placement_worker_spec.rb
+++ b/spec/workers/issue_placement_worker_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe IssuePlacementWorker do
it 'schedules rebalancing if needed' do
issue_a.update!(relative_position: RelativePositioning::MAX_POSITION)
- expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
run_worker
end
@@ -101,7 +101,7 @@ RSpec.describe IssuePlacementWorker do
it 'anticipates the failure to place the issues, and schedules rebalancing' do
allow(Issue).to receive(:move_nulls_to_end) { raise RelativePositioning::NoSpaceLeft }
- expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
+ expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, nil, project.group.id)
expect(Gitlab::ErrorTracking)
.to receive(:log_exception)
.with(RelativePositioning::NoSpaceLeft, worker_arguments)
diff --git a/spec/workers/issue_rebalancing_worker_spec.rb b/spec/workers/issue_rebalancing_worker_spec.rb
index e5c6ac3f854..b6e9429d78e 100644
--- a/spec/workers/issue_rebalancing_worker_spec.rb
+++ b/spec/workers/issue_rebalancing_worker_spec.rb
@@ -20,34 +20,83 @@ RSpec.describe IssueRebalancingWorker do
end
end
- it 'runs an instance of IssueRebalancingService' do
- service = double(execute: nil)
- expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
+ shared_examples 'running the worker' do
+ it 'runs an instance of IssueRebalancingService' do
+ service = double(execute: nil)
+ service_param = arguments.second.present? ? kind_of(Project.id_in([project]).class) : kind_of(group&.all_projects.class)
- described_class.new.perform(nil, issue.project_id)
+ expect(IssueRebalancingService).to receive(:new).with(service_param).and_return(service)
+
+ described_class.new.perform(*arguments)
+ end
+
+ it 'anticipates there being too many issues' do
+ service = double
+ service_param = arguments.second.present? ? kind_of(Project.id_in([project]).class) : kind_of(group&.all_projects.class)
+
+ allow(service).to receive(:execute).and_raise(IssueRebalancingService::TooManyIssues)
+ expect(IssueRebalancingService).to receive(:new).with(service_param).and_return(service)
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(project_id: arguments.second, root_namespace_id: arguments.third))
+
+ described_class.new.perform(*arguments)
+ end
+
+ it 'takes no action if the value is nil' do
+ expect(IssueRebalancingService).not_to receive(:new)
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ described_class.new.perform # all arguments are nil
+ end
end
- it 'anticipates the inability to find the issue' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ActiveRecord::RecordNotFound, include(project_id: -1))
- expect(IssueRebalancingService).not_to receive(:new)
+ shared_examples 'safely handles non-existent ids' do
+ it 'anticipates the inability to find the issue' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(ArgumentError, include(project_id: arguments.second, root_namespace_id: arguments.third))
+ expect(IssueRebalancingService).not_to receive(:new)
- described_class.new.perform(nil, -1)
+ described_class.new.perform(*arguments)
+ end
end
- it 'anticipates there being too many issues' do
- service = double
- allow(service).to receive(:execute) { raise IssueRebalancingService::TooManyIssues }
- expect(IssueRebalancingService).to receive(:new).with(issue).and_return(service)
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(IssueRebalancingService::TooManyIssues, include(project_id: issue.project_id))
+ context 'without root_namespace param' do
+ it_behaves_like 'running the worker' do
+ let(:arguments) { [-1, project.id] }
+ end
+
+ it_behaves_like 'safely handles non-existent ids' do
+ let(:arguments) { [nil, -1] }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [-1, project.id] }
+ end
- described_class.new.perform(nil, issue.project_id)
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [nil, -1] }
+ end
end
- it 'takes no action if the value is nil' do
- expect(IssueRebalancingService).not_to receive(:new)
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ context 'with root_namespace param' do
+ it_behaves_like 'running the worker' do
+ let(:arguments) { [nil, nil, group.id] }
+ end
- described_class.new.perform(nil, nil)
+ it_behaves_like 'safely handles non-existent ids' do
+ let(:arguments) { [nil, nil, -1] }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [nil, nil, group.id] }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [nil, nil, -1] }
+ end
end
end
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ expect(described_class.get_deduplication_options).to include({ including_scheduled: true })
+ end
end
diff --git a/spec/workers/merge_requests/assignees_change_worker_spec.rb b/spec/workers/merge_requests/assignees_change_worker_spec.rb
deleted file mode 100644
index 33478daf8d3..00000000000
--- a/spec/workers/merge_requests/assignees_change_worker_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MergeRequests::AssigneesChangeWorker do
- include AfterNextHelpers
-
- let_it_be(:merge_request) { create(:merge_request) }
- let_it_be(:user) { create(:user) }
- let_it_be(:old_assignees) { create_list(:user, 3) }
-
- let(:user_ids) { old_assignees.map(&:id).to_a }
- let(:worker) { described_class.new }
-
- it_behaves_like 'an idempotent worker' do
- let(:job_args) { [merge_request.id, user.id, user_ids] }
- end
-
- describe '#perform' do
- context 'with a non-existing merge request' do
- it 'does nothing' do
- expect(::MergeRequests::HandleAssigneesChangeService).not_to receive(:new)
-
- worker.perform(non_existing_record_id, user.id, user_ids)
- end
- end
-
- context 'with a non-existing user' do
- it 'does nothing' do
- expect(::MergeRequests::HandleAssigneesChangeService).not_to receive(:new)
-
- worker.perform(merge_request.id, non_existing_record_id, user_ids)
- end
- end
-
- context 'when there are no changes' do
- it 'does nothing' do
- expect(::MergeRequests::HandleAssigneesChangeService).not_to receive(:new)
-
- worker.perform(merge_request.id, user.id, merge_request.assignee_ids)
- end
- end
-
- context 'when the old users cannot be found' do
- it 'does nothing' do
- expect(::MergeRequests::HandleAssigneesChangeService).not_to receive(:new)
-
- worker.perform(merge_request.id, user.id, [non_existing_record_id])
- end
- end
-
- it 'gets MergeRequests::UpdateAssigneesService to handle the changes' do
- expect_next(::MergeRequests::HandleAssigneesChangeService)
- .to receive(:execute).with(merge_request, match_array(old_assignees), execute_hooks: true)
-
- worker.perform(merge_request.id, user.id, user_ids)
- 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
new file mode 100644
index 00000000000..a8751ccceae
--- /dev/null
+++ b/spec/workers/packages/debian/generate_distribution_worker_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::GenerateDistributionWorker, type: :worker do
+ describe '#perform' do
+ let(:container_type) { distribution.container_type }
+ let(:distribution_id) { distribution.id }
+
+ subject { described_class.new.perform(container_type, distribution_id) }
+
+ include_context 'with published Debian package'
+
+ [:project, :group].each do |container_type|
+ context "for #{container_type}" do
+ include_context 'with Debian distribution', container_type
+
+ context 'with mocked service' do
+ it 'calls GenerateDistributionService' do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ expect_next_instance_of(::Packages::Debian::GenerateDistributionService) do |service|
+ expect(service).to receive(:execute)
+ .with(no_args)
+ end
+
+ subject
+ end
+ end
+
+ context 'with non existing distribution id' do
+ let(:distribution_id) { non_existing_record_id }
+
+ it 'returns early without error' do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ expect(::Packages::Debian::GenerateDistributionService).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ context 'with nil distribution id' do
+ let(:distribution_id) { nil }
+
+ it 'returns early without error' do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ expect(::Packages::Debian::GenerateDistributionService).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ context 'with valid parameters' do
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [container_type, distribution_id] }
+
+ it_behaves_like 'Generate Debian Distribution and component files'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/pipeline_hooks_worker_spec.rb b/spec/workers/pipeline_hooks_worker_spec.rb
index 7c75cdc8823..5957b355c8e 100644
--- a/spec/workers/pipeline_hooks_worker_spec.rb
+++ b/spec/workers/pipeline_hooks_worker_spec.rb
@@ -22,4 +22,9 @@ RSpec.describe PipelineHooksWorker do
end
end
end
+
+ it_behaves_like 'worker with data consistency',
+ described_class,
+ feature_flag: :load_balancing_for_pipeline_hooks_worker,
+ data_consistency: :delayed
end
diff --git a/spec/workers/pipeline_process_worker_spec.rb b/spec/workers/pipeline_process_worker_spec.rb
index 0c1db3ccc5a..f8140d11f2e 100644
--- a/spec/workers/pipeline_process_worker_spec.rb
+++ b/spec/workers/pipeline_process_worker_spec.rb
@@ -3,10 +3,44 @@
require 'spec_helper'
RSpec.describe PipelineProcessWorker do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+
+ include_examples 'an idempotent worker' do
+ let(:pipeline) { create(:ci_pipeline, :created) }
+ let(:job_args) { [pipeline.id] }
+
+ before do
+ create(:ci_build, :created, pipeline: pipeline)
+ end
+
+ it 'processes the pipeline' do
+ expect(pipeline.status).to eq('created')
+ expect(pipeline.processables.pluck(:status)).to contain_exactly('created')
+
+ subject
+
+ expect(pipeline.reload.status).to eq('pending')
+ expect(pipeline.processables.pluck(:status)).to contain_exactly('pending')
+
+ subject
+
+ expect(pipeline.reload.status).to eq('pending')
+ expect(pipeline.processables.pluck(:status)).to contain_exactly('pending')
+ end
+ end
+
+ context 'when the FF ci_idempotent_pipeline_process_worker is disabled' do
+ before do
+ stub_feature_flags(ci_idempotent_pipeline_process_worker: false)
+ end
+
+ it 'is not deduplicated' do
+ expect(described_class).not_to be_deduplication_enabled
+ end
+ end
+
describe '#perform' do
context 'when pipeline exists' do
- let(:pipeline) { create(:ci_pipeline) }
-
it 'processes pipeline' do
expect_any_instance_of(Ci::ProcessPipelineService).to receive(:execute)
@@ -16,14 +50,9 @@ RSpec.describe PipelineProcessWorker do
context 'when pipeline does not exist' do
it 'does not raise exception' do
- expect { described_class.new.perform(123) }
+ expect { described_class.new.perform(non_existing_record_id) }
.not_to raise_error
end
end
-
- it_behaves_like 'worker with data consistency',
- described_class,
- feature_flag: :load_balancing_for_pipeline_process_worker,
- data_consistency: :delayed
end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index a468c8c3482..4d3cc447d9b 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -375,7 +375,7 @@ RSpec.describe PostReceive do
it 'asks the project to trigger all hooks' do
create(:project_hook, push_events: true, tag_push_events: true, project: project)
- create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project)
+ create(:custom_issue_tracker_integration, push_events: true, merge_requests_events: false, project: project)
allow(Project).to receive(:find_by).and_return(project)
expect(project).to receive(:execute_hooks).twice
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 294a05c652b..3df26c774ba 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe ProcessCommitWorker do
end
end
- describe '#update_issue_metrics' do
+ describe '#update_issue_metrics', :clean_gitlab_redis_cache do
context 'when commit has issue reference' do
subject(:update_metrics_and_reload) do
-> {
diff --git a/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb
deleted file mode 100644
index f284e1ab8c6..00000000000
--- a/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProjectScheduleBulkRepositoryShardMovesWorker do
- it_behaves_like 'schedules bulk repository shard moves' do
- let_it_be_with_reload(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
-
- let(:move_service_klass) { Projects::RepositoryStorageMove }
- let(:worker_klass) { Projects::UpdateRepositoryStorageWorker }
- end
-end
diff --git a/spec/workers/project_service_worker_spec.rb b/spec/workers/project_service_worker_spec.rb
index 237f501e0ec..9383e7ec5c4 100644
--- a/spec/workers/project_service_worker_spec.rb
+++ b/spec/workers/project_service_worker_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe ProjectServiceWorker, '#perform' do
let(:worker) { described_class.new }
- let(:service) { JiraService.new }
+ let(:service) { Integrations::Jira.new }
before do
allow(Integration).to receive(:find).and_return(service)
diff --git a/spec/workers/project_update_repository_storage_worker_spec.rb b/spec/workers/project_update_repository_storage_worker_spec.rb
deleted file mode 100644
index 6924e8a93a3..00000000000
--- a/spec/workers/project_update_repository_storage_worker_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProjectUpdateRepositoryStorageWorker do
- subject { described_class.new }
-
- it_behaves_like 'an update storage move worker' do
- let_it_be_with_refind(:container) { create(:project, :repository) }
- let_it_be(:repository_storage_move) { create(:project_repository_storage_move) }
-
- let(:service_klass) { Projects::UpdateRepositoryStorageService }
- let(:repository_storage_move_klass) { Projects::RepositoryStorageMove }
- end
-end
diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb
index c2f42f03299..50c21575878 100644
--- a/spec/workers/projects/post_creation_worker_spec.rb
+++ b/spec/workers/projects/post_creation_worker_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe Projects::PostCreationWorker do
end
before do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
end
it 'creates PrometheusService record', :aggregate_failures do
@@ -50,7 +50,7 @@ RSpec.describe Projects::PostCreationWorker do
let(:cluster) { create(:cluster, :instance) }
before do
- create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ create(:clusters_integrations_prometheus, cluster: cluster)
end
it 'creates PrometheusService record', :aggregate_failures do
diff --git a/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
index 24957a35b72..7eff8e4dcd7 100644
--- a/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
+++ b/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::ScheduleBulkRepositoryShardMovesWorker do
it_behaves_like 'schedules bulk repository shard moves' do
- let_it_be_with_reload(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
+ let_it_be_with_reload(:container) { create(:project, :repository) }
let(:move_service_klass) { Projects::RepositoryStorageMove }
let(:worker_klass) { Projects::UpdateRepositoryStorageWorker }
diff --git a/spec/workers/propagate_integration_inherit_worker_spec.rb b/spec/workers/propagate_integration_inherit_worker_spec.rb
index 39219eaa3b5..2b4f241f755 100644
--- a/spec/workers/propagate_integration_inherit_worker_spec.rb
+++ b/spec/workers/propagate_integration_inherit_worker_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe PropagateIntegrationInheritWorker do
describe '#perform' do
let_it_be(:integration) { create(:redmine_service, :instance) }
let_it_be(:integration1) { create(:redmine_service, inherit_from_id: integration.id) }
- let_it_be(:integration2) { create(:bugzilla_service, inherit_from_id: integration.id) }
+ let_it_be(:integration2) { create(:bugzilla_integration, inherit_from_id: integration.id) }
let_it_be(:integration3) { create(:redmine_service) }
it_behaves_like 'an idempotent worker' do
diff --git a/spec/workers/propagate_integration_worker_spec.rb b/spec/workers/propagate_integration_worker_spec.rb
index b8c7f2bebe7..2461b30a2ed 100644
--- a/spec/workers/propagate_integration_worker_spec.rb
+++ b/spec/workers/propagate_integration_worker_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe PropagateIntegrationWorker do
describe '#perform' do
let(:integration) do
- PushoverService.create!(
+ Integrations::Pushover.create!(
template: true,
active: true,
device: 'MyDevice',
@@ -21,11 +21,5 @@ RSpec.describe PropagateIntegrationWorker do
subject.perform(integration.id)
end
-
- it 'ignores overwrite parameter from previous version' do
- expect(Admin::PropagateIntegrationService).to receive(:propagate).with(integration)
-
- subject.perform(integration.id, true)
- end
end
end
diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb
index 793f0b9b08c..b692ce3d72b 100644
--- a/spec/workers/propagate_service_template_worker_spec.rb
+++ b/spec/workers/propagate_service_template_worker_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe PropagateServiceTemplateWorker do
describe '#perform' do
it 'calls the propagate service with the template' do
- template = PushoverService.create!(
+ template = Integrations::Pushover.create!(
template: true,
active: true,
properties: {
diff --git a/spec/workers/prune_web_hook_logs_worker_spec.rb b/spec/workers/prune_web_hook_logs_worker_spec.rb
deleted file mode 100644
index 6cd7a54ac7a..00000000000
--- a/spec/workers/prune_web_hook_logs_worker_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe PruneWebHookLogsWorker do
- describe '#perform' do
- before do
- hook = create(:project_hook)
-
- create(:web_hook_log, web_hook: hook, created_at: 5.months.ago)
- create(:web_hook_log, web_hook: hook, created_at: 4.months.ago)
- create(:web_hook_log, web_hook: hook, created_at: 91.days.ago)
- create(:web_hook_log, web_hook: hook, created_at: 89.days.ago)
- create(:web_hook_log, web_hook: hook, created_at: 2.months.ago)
- create(:web_hook_log, web_hook: hook, created_at: 1.month.ago)
- create(:web_hook_log, web_hook: hook, response_status: '404')
- end
-
- it 'removes all web hook logs older than 90 days' do
- described_class.new.perform
-
- expect(WebHookLog.count).to eq(4)
- expect(WebHookLog.last.response_status).to eq('404')
- end
- end
-end
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
index 91031768632..ff5f7b9db27 100644
--- a/spec/workers/remove_expired_group_links_worker_spec.rb
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe RemoveExpiredGroupLinksWorker do
expect(non_expiring_project_group_link.reload).to be_present
end
- it 'removes project authorization' do
+ it 'removes project authorization', :sidekiq_inline do
user = create(:user)
project = expired_project_group_link.project
diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
index 21b9a7b844b..6007d3b34f8 100644
--- a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
+++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
@@ -34,14 +34,14 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
end
it 'removes unreferenced lfs objects' do
- worker.perform
+ expect(worker.perform).to eq(2)
expect(LfsObject.where(id: unreferenced_lfs_object1.id)).to be_empty
expect(LfsObject.where(id: unreferenced_lfs_object2.id)).to be_empty
end
it 'leaves referenced lfs objects' do
- worker.perform
+ expect(worker.perform).to eq(2)
expect(referenced_lfs_object1.reload).to be_present
expect(referenced_lfs_object2.reload).to be_present
@@ -50,10 +50,12 @@ RSpec.describe RemoveUnreferencedLfsObjectsWorker do
it 'removes unreferenced lfs objects after project removal' do
project1.destroy!
- worker.perform
+ expect(worker.perform).to eq(3)
expect(referenced_lfs_object1.reload).to be_present
expect(LfsObject.where(id: referenced_lfs_object2.id)).to be_empty
end
end
+
+ it_behaves_like 'an idempotent worker'
end
diff --git a/spec/workers/snippet_schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/snippet_schedule_bulk_repository_shard_moves_worker_spec.rb
deleted file mode 100644
index a5f1c6b7b3d..00000000000
--- a/spec/workers/snippet_schedule_bulk_repository_shard_moves_worker_spec.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe SnippetScheduleBulkRepositoryShardMovesWorker do
- it_behaves_like 'schedules bulk repository shard moves' do
- let_it_be_with_reload(:container) { create(:snippet, :repository).tap { |snippet| snippet.create_repository } }
-
- let(:move_service_klass) { Snippets::RepositoryStorageMove }
- let(:worker_klass) { Snippets::UpdateRepositoryStorageWorker }
- end
-end
diff --git a/spec/workers/snippet_update_repository_storage_worker_spec.rb b/spec/workers/snippet_update_repository_storage_worker_spec.rb
deleted file mode 100644
index 205cb2e432f..00000000000
--- a/spec/workers/snippet_update_repository_storage_worker_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe SnippetUpdateRepositoryStorageWorker do
- subject { described_class.new }
-
- it_behaves_like 'an update storage move worker' do
- let_it_be_with_refind(:container) { create(:snippet, :repository) }
- let_it_be(:repository_storage_move) { create(:snippet_repository_storage_move) }
-
- let(:service_klass) { Snippets::UpdateRepositoryStorageService }
- let(:repository_storage_move_klass) { Snippets::RepositoryStorageMove }
- end
-end
diff --git a/spec/workers/ssh_keys/expired_notification_worker_spec.rb b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
index 249ee404870..109d24f03ab 100644
--- a/spec/workers/ssh_keys/expired_notification_worker_spec.rb
+++ b/spec/workers/ssh_keys/expired_notification_worker_spec.rb
@@ -15,6 +15,20 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do
describe '#perform' do
let_it_be(:user) { create(:user) }
+ context 'with a large batch' do
+ before do
+ stub_const("SshKeys::ExpiredNotificationWorker::BATCH_SIZE", 5)
+ end
+
+ let_it_be_with_reload(:keys) { create_list(:key, 20, expires_at: 3.days.ago, user: user) }
+
+ it 'updates all keys regardless of batch size' do
+ worker.perform
+
+ expect(keys.pluck(:expiry_notification_delivered_at)).not_to include(nil)
+ end
+ end
+
context 'with expiring key today' do
let_it_be_with_reload(:expired_today) { create(:key, expires_at: Time.current, user: user) }
@@ -35,24 +49,24 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do
perform_multiple(worker: worker)
end
end
+ end
+
+ context 'when key has expired in the past' do
+ let_it_be(:expired_past) { create(:key, expires_at: 1.day.ago, user: user) }
+
+ it 'does update notified column' do
+ expect { worker.perform }.to change { expired_past.reload.expiry_notification_delivered_at }
+ end
- context 'when feature is not enabled' do
+ context 'when key has already been notified of expiration' do
before do
- stub_feature_flags(ssh_key_expiration_email_notification: false)
+ expired_past.update!(expiry_notification_delivered_at: 1.day.ago)
end
it 'does not update notified column' do
- expect { worker.perform }.not_to change { expired_today.reload.expiry_notification_delivered_at }
+ expect { worker.perform }.not_to change { expired_past.reload.expiry_notification_delivered_at }
end
end
end
-
- context 'when key has expired in the past' do
- let_it_be(:expired_past) { create(:key, expires_at: 1.day.ago, user: user) }
-
- it 'does not update notified column' do
- expect { worker.perform }.not_to change { expired_past.reload.expiry_notification_delivered_at }
- end
- end
end
end
diff --git a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
index f9276c86cdf..0a1d4a14ad0 100644
--- a/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
+++ b/spec/workers/ssh_keys/expiring_soon_notification_worker_spec.rb
@@ -35,16 +35,6 @@ RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker do
perform_multiple(worker: worker)
end
end
-
- context 'when feature is not enabled' do
- before do
- stub_feature_flags(ssh_key_expiration_email_notification: false)
- end
-
- it 'does not update notified column' do
- expect { worker.perform }.not_to change { expiring_soon.reload.before_expiry_notification_delivered_at }
- end
- end
end
context 'when key has expired in the past' do
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index 24d3b6fadf5..84b2d87494e 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -9,12 +9,17 @@ RSpec.describe StuckCiJobsWorker do
let!(:job) { create :ci_build, runner: runner }
let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY }
let(:worker_lease_uuid) { SecureRandom.uuid }
+ let(:created_at) { }
+ let(:updated_at) { }
subject(:worker) { described_class.new }
before do
stub_exclusive_lease(worker_lease_key, worker_lease_uuid)
- job.update!(status: status, updated_at: updated_at)
+ job_attributes = { status: status }
+ job_attributes[:created_at] = created_at if created_at
+ job_attributes[:updated_at] = updated_at if updated_at
+ job.update!(job_attributes)
end
shared_examples 'job is dropped' do
@@ -63,22 +68,70 @@ RSpec.describe StuckCiJobsWorker do
allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false)
end
- context 'when job was not updated for more than 1 day ago' do
- let(:updated_at) { 2.days.ago }
+ context 'when job was updated_at more than 1 day ago' do
+ let(:updated_at) { 1.5.days.ago }
- it_behaves_like 'job is dropped'
+ context 'when created_at is the same as updated_at' do
+ let(:created_at) { 1.5.days.ago }
+
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when created_at is before updated_at' do
+ let(:created_at) { 3.days.ago }
+
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when created_at is outside lookback window' do
+ let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
+
+ it_behaves_like 'job is unchanged'
+ end
end
- context 'when job was updated in less than 1 day ago' do
+ context 'when job was updated less than 1 day ago' do
let(:updated_at) { 6.hours.ago }
- it_behaves_like 'job is unchanged'
+ context 'when created_at is the same as updated_at' do
+ let(:created_at) { 1.5.days.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is before updated_at' do
+ let(:created_at) { 3.days.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is outside lookback window' do
+ let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
+
+ it_behaves_like 'job is unchanged'
+ end
end
- context 'when job was not updated for more than 1 hour ago' do
+ context 'when job was updated more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
- it_behaves_like 'job is unchanged'
+ context 'when created_at is the same as updated_at' do
+ let(:created_at) { 2.hours.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is before updated_at' do
+ let(:created_at) { 3.days.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is outside lookback window' do
+ let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
+
+ it_behaves_like 'job is unchanged'
+ end
end
end
@@ -87,17 +140,48 @@ RSpec.describe StuckCiJobsWorker do
allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true)
end
- context 'when job was not updated for more than 1 hour ago' do
- let(:updated_at) { 2.hours.ago }
+ context 'when job was updated_at more than 1 hour ago' do
+ let(:updated_at) { 1.5.hours.ago }
+
+ context 'when created_at is the same as updated_at' do
+ let(:created_at) { 1.5.hours.ago }
+
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when created_at is before updated_at' do
+ let(:created_at) { 3.days.ago }
- it_behaves_like 'job is dropped'
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when created_at is outside lookback window' do
+ let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
+
+ it_behaves_like 'job is unchanged'
+ end
end
- context 'when job was updated in less than 1
- hour ago' do
+ context 'when job was updated in less than 1 hour ago' do
let(:updated_at) { 30.minutes.ago }
- it_behaves_like 'job is unchanged'
+ context 'when created_at is the same as updated_at' do
+ let(:created_at) { 30.minutes.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is before updated_at' do
+ let(:created_at) { 2.days.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is outside lookback window' do
+ let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
+
+ it_behaves_like 'job is unchanged'
+ end
end
end
end
@@ -105,7 +189,7 @@ RSpec.describe StuckCiJobsWorker do
context 'when job is running' do
let(:status) { 'running' }
- context 'when job was not updated for more than 1 hour ago' do
+ context 'when job was updated_at more than an hour ago' do
let(:updated_at) { 2.hours.ago }
it_behaves_like 'job is dropped'
@@ -123,7 +207,23 @@ RSpec.describe StuckCiJobsWorker do
let(:status) { status }
let(:updated_at) { 2.days.ago }
- it_behaves_like 'job is unchanged'
+ context 'when created_at is the same as updated_at' do
+ let(:created_at) { 2.days.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is before updated_at' do
+ let(:created_at) { 3.days.ago }
+
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when created_at is outside lookback window' do
+ let(:created_at) { described_class::BUILD_LOOKBACK - 1.day }
+
+ it_behaves_like 'job is unchanged'
+ end
end
end
diff --git a/spec/workers/users/update_open_issue_count_worker_spec.rb b/spec/workers/users/update_open_issue_count_worker_spec.rb
deleted file mode 100644
index 700055980d8..00000000000
--- a/spec/workers/users/update_open_issue_count_worker_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Users::UpdateOpenIssueCountWorker do
- let_it_be(:first_user) { create(:user) }
- let_it_be(:second_user) { create(:user) }
-
- describe '#perform' do
- let(:target_user_ids) { [first_user.id, second_user.id] }
-
- subject { described_class.new.perform(target_user_ids) }
-
- context 'when arguments are missing' do
- context 'when target_user_ids are missing' do
- context 'when nil' do
- let(:target_user_ids) { nil }
-
- it 'raises an error' do
- expect { subject }.to raise_error(ArgumentError, /No target user ID provided/)
- end
- end
-
- context 'when empty array' do
- let(:target_user_ids) { [] }
-
- it 'raises an error' do
- expect { subject }.to raise_error(ArgumentError, /No target user ID provided/)
- end
- end
-
- context 'when not an ID' do
- let(:target_user_ids) { "nonsense" }
-
- it 'raises an error' do
- expect { subject }.to raise_error(ArgumentError, /No valid target user ID provided/)
- end
- end
- end
- end
-
- context 'when successful' do
- let(:job_args) { [target_user_ids] }
- let(:fake_service1) { double }
- let(:fake_service2) { double }
-
- it 'calls the user update service' do
- expect(Users::UpdateAssignedOpenIssueCountService).to receive(:new).with(target_user: first_user).and_return(fake_service1)
- expect(Users::UpdateAssignedOpenIssueCountService).to receive(:new).with(target_user: second_user).and_return(fake_service2)
- expect(fake_service1).to receive(:execute)
- expect(fake_service2).to receive(:execute)
-
- subject
- end
-
- it_behaves_like 'an idempotent worker' do
- it 'recalculates' do
- subject
-
- expect(first_user.assigned_open_issues_count).to eq(0)
- end
- end
- end
- end
-end
diff --git a/spec/workers/web_hook_worker_spec.rb b/spec/workers/web_hook_worker_spec.rb
index becc7461f2a..548cf4c717a 100644
--- a/spec/workers/web_hook_worker_spec.rb
+++ b/spec/workers/web_hook_worker_spec.rb
@@ -10,9 +10,14 @@ RSpec.describe WebHookWorker do
describe '#perform' do
it 'delegates to WebHookService' do
- expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name).to receive(:execute)
+ expect_next(WebHookService, project_hook, data.with_indifferent_access, hook_name, anything).to receive(:execute)
subject.perform(project_hook.id, data, hook_name)
end
+
+ it_behaves_like 'worker with data consistency',
+ described_class,
+ feature_flag: :load_balancing_for_web_hook_worker,
+ data_consistency: :delayed
end
end