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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/bin/audit_event_type_spec.rb293
-rw-r--r--spec/components/pajamas/spinner_component_spec.rb4
-rw-r--r--spec/components/previews/pajamas/alert_component_preview.rb2
-rw-r--r--spec/components/previews/pajamas/avatar_component_preview.rb6
-rw-r--r--spec/components/previews/pajamas/badge_component_preview.rb4
-rw-r--r--spec/components/previews/pajamas/banner_component_preview.rb2
-rw-r--r--spec/components/previews/pajamas/button_component_preview.rb8
-rw-r--r--spec/components/previews/pajamas/progress_component_preview.rb4
-rw-r--r--spec/components/previews/pajamas/spinner_component_preview.rb22
-rw-r--r--spec/config/inject_enterprise_edition_module_spec.rb18
-rw-r--r--spec/config/metrics/aggregates/aggregated_metrics_spec.rb95
-rw-r--r--spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json6
-rw-r--r--spec/contracts/provider/helpers/publish_contract_helper.rb17
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb2
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb4
-rw-r--r--spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb4
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb4
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb4
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb5
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb6
-rw-r--r--spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb4
-rw-r--r--spec/contracts/publish-contracts.sh23
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb8
-rw-r--r--spec/controllers/admin/hooks_controller_spec.rb24
-rw-r--r--spec/controllers/admin/integrations_controller_spec.rb2
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb35
-rw-r--r--spec/controllers/admin/topics_controller_spec.rb2
-rw-r--r--spec/controllers/admin/users_controller_spec.rb313
-rw-r--r--spec/controllers/concerns/issuable_actions_spec.rb13
-rw-r--r--spec/controllers/concerns/preferred_language_switcher_spec.rb51
-rw-r--r--spec/controllers/concerns/renders_commits_spec.rb4
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb29
-rw-r--r--spec/controllers/confirmations_controller_spec.rb31
-rw-r--r--spec/controllers/dashboard_controller_spec.rb14
-rw-r--r--spec/controllers/explore/groups_controller_spec.rb46
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb25
-rw-r--r--spec/controllers/graphql_controller_spec.rb114
-rw-r--r--spec/controllers/groups/children_controller_spec.rb2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb35
-rw-r--r--spec/controllers/groups/registry/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/groups/releases_controller_spec.rb4
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb12
-rw-r--r--spec/controllers/groups/settings/repository_controller_spec.rb117
-rw-r--r--spec/controllers/groups_controller_spec.rb28
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb69
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb2
-rw-r--r--spec/controllers/passwords_controller_spec.rb16
-rw-r--r--spec/controllers/profiles/personal_access_tokens_controller_spec.rb95
-rw-r--r--spec/controllers/projects/alerting/notifications_controller_spec.rb20
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb70
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb8
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb59
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb114
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb90
-rw-r--r--spec/controllers/projects/learn_gitlab_controller_spec.rb11
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb12
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb32
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb65
-rw-r--r--spec/controllers/projects/product_analytics_controller_spec.rb95
-rw-r--r--spec/controllers/projects/prometheus/alerts_controller_spec.rb11
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb25
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb2
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb17
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb172
-rw-r--r--spec/controllers/projects/starrers_controller_spec.rb6
-rw-r--r--spec/controllers/projects_controller_spec.rb1
-rw-r--r--spec/controllers/registrations_controller_spec.rb73
-rw-r--r--spec/controllers/search_controller_spec.rb9
-rw-r--r--spec/controllers/sessions_controller_spec.rb2
-rw-r--r--spec/db/schema_spec.rb9
-rw-r--r--spec/experiments/application_experiment_spec.rb66
-rw-r--r--spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb28
-rw-r--r--spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb6
-rw-r--r--spec/factories/ci/builds.rb25
-rw-r--r--spec/factories/ci/job_artifacts.rb2
-rw-r--r--spec/factories/ci/pipeline_metadata.rb2
-rw-r--r--spec/factories/ci/pipelines.rb10
-rw-r--r--spec/factories/ci/processable.rb2
-rw-r--r--spec/factories/ci/reports/codequality_degradations.rb9
-rw-r--r--spec/factories/ci/reports/sbom/components.rb14
-rw-r--r--spec/factories/ci/reports/sbom/reports.rb6
-rw-r--r--spec/factories/ci/secure_files.rb9
-rw-r--r--spec/factories/ci/stages.rb2
-rw-r--r--spec/factories/container_repositories.rb4
-rw-r--r--spec/factories/dependency_proxy.rb9
-rw-r--r--spec/factories/experiment_subjects.rb9
-rw-r--r--spec/factories/experiment_users.rb10
-rw-r--r--spec/factories/experiments.rb7
-rw-r--r--spec/factories/integrations.rb13
-rw-r--r--spec/factories/member_roles.rb2
-rw-r--r--spec/factories/merge_request_reviewers.rb9
-rw-r--r--spec/factories/packages/rpm/rpm_repository_files.rb9
-rw-r--r--spec/factories/project_hooks.rb4
-rw-r--r--spec/factories/projects.rb13
-rw-r--r--spec/factories/projects/import_export/export_relation.rb11
-rw-r--r--spec/factories/projects/import_export/relation_export.rb27
-rw-r--r--spec/factories/projects/import_export/relation_export_upload.rb8
-rw-r--r--spec/factories/projects/wiki_repositories.rb7
-rw-r--r--spec/factories/protected_branches.rb37
-rw-r--r--spec/factories/user_statuses.rb4
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/factories/users/ghost_user_migrations.rb1
-rw-r--r--spec/factories/users/namespace_commit_emails.rb9
-rw-r--r--spec/features/admin/admin_dev_ops_reports_spec.rb4
-rw-r--r--spec/features/admin/admin_hook_logs_spec.rb7
-rw-r--r--spec/features/admin/admin_hooks_spec.rb2
-rw-r--r--spec/features/admin/admin_mode/workers_spec.rb54
-rw-r--r--spec/features/admin/admin_runners_spec.rb15
-rw-r--r--spec/features/admin/admin_settings_spec.rb18
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb31
-rw-r--r--spec/features/admin/users/user_spec.rb79
-rw-r--r--spec/features/admin_variables_spec.rb18
-rw-r--r--spec/features/boards/board_filters_spec.rb5
-rw-r--r--spec/features/boards/boards_spec.rb35
-rw-r--r--spec/features/boards/issue_ordering_spec.rb1
-rw-r--r--spec/features/boards/sidebar_labels_spec.rb1
-rw-r--r--spec/features/boards/sidebar_spec.rb1
-rw-r--r--spec/features/boards/user_adds_lists_to_board_spec.rb6
-rw-r--r--spec/features/boards/user_visits_board_spec.rb5
-rw-r--r--spec/features/broadcast_messages_spec.rb3
-rw-r--r--spec/features/commits_spec.rb4
-rw-r--r--spec/features/cycle_analytics_spec.rb8
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb6
-rw-r--r--spec/features/global_search_spec.rb15
-rw-r--r--spec/features/graphql_known_operations_spec.rb2
-rw-r--r--spec/features/group_variables_spec.rb18
-rw-r--r--spec/features/groups/activity_spec.rb2
-rw-r--r--spec/features/groups/board_sidebar_spec.rb3
-rw-r--r--spec/features/groups/empty_states_spec.rb2
-rw-r--r--spec/features/groups/group_runners_spec.rb46
-rw-r--r--spec/features/groups/group_settings_spec.rb27
-rw-r--r--spec/features/groups/issues_spec.rb12
-rw-r--r--spec/features/groups/milestone_spec.rb2
-rw-r--r--spec/features/groups/milestones_sorting_spec.rb2
-rw-r--r--spec/features/groups/settings/repository_spec.rb23
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb2
-rw-r--r--spec/features/ide_spec.rb95
-rw-r--r--spec/features/incidents/user_views_incident_spec.rb60
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb18
-rw-r--r--spec/features/issues/confidential_notes_spec.rb13
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb1
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb1
-rw-r--r--spec/features/issues/form_spec.rb23
-rw-r--r--spec/features/issues/user_bulk_edits_issues_labels_spec.rb2
-rw-r--r--spec/features/issues/user_bulk_edits_issues_spec.rb4
-rw-r--r--spec/features/issues/user_comments_on_issue_spec.rb6
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb6
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb33
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb7
-rw-r--r--spec/features/issues/user_sees_empty_state_spec.rb4
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb2
-rw-r--r--spec/features/jira_connect/subscriptions_spec.rb2
-rw-r--r--spec/features/markdown/sandboxed_mermaid_spec.rb60
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_edits_assignees_sidebar_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb19
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb14
-rw-r--r--spec/features/merge_request/user_sees_discussions_navigation_spec.rb222
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb141
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb2
-rw-r--r--spec/features/monitor_sidebar_link_spec.rb5
-rw-r--r--spec/features/nav/top_nav_tooltip_spec.rb3
-rw-r--r--spec/features/one_trust_spec.rb2
-rw-r--r--spec/features/profile_spec.rb41
-rw-r--r--spec/features/profiles/password_spec.rb44
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb39
-rw-r--r--spec/features/profiles/two_factor_auths_spec.rb2
-rw-r--r--spec/features/project_variables_spec.rb63
-rw-r--r--spec/features/projects/branches/user_views_branches_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb42
-rw-r--r--spec/features/projects/container_registry_spec.rb5
-rw-r--r--spec/features/projects/environments/environment_spec.rb28
-rw-r--r--spec/features/projects/environments/environments_spec.rb5
-rw-r--r--spec/features/projects/fork_spec.rb4
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/integrations/user_activates_issue_tracker_spec.rb2
-rw-r--r--spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb10
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb44
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb6
-rw-r--r--spec/features/projects/jobs_spec.rb4
-rw-r--r--spec/features/projects/members/manage_members_spec.rb2
-rw-r--r--spec/features/projects/network_graph_spec.rb138
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb10
-rw-r--r--spec/features/projects/pipelines/legacy_pipeline_spec.rb7
-rw-r--r--spec/features/projects/pipelines/legacy_pipelines_spec.rb1
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb52
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb5
-rw-r--r--spec/features/projects/product_analytics/events_spec.rb30
-rw-r--r--spec/features/projects/product_analytics/graphs_spec.rb25
-rw-r--r--spec/features/projects/product_analytics/setup_spec.rb19
-rw-r--r--spec/features/projects/product_analytics/test_spec.rb27
-rw-r--r--spec/features/projects/releases/user_views_edit_release_spec.rb2
-rw-r--r--spec/features/projects/releases/user_views_releases_spec.rb2
-rw-r--r--spec/features/projects/settings/branch_names_settings_spec.rb48
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/user_changes_default_branch_spec.rb4
-rw-r--r--spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb1
-rw-r--r--spec/features/projects/settings/webhooks_settings_spec.rb51
-rw-r--r--spec/features/projects/user_changes_project_visibility_spec.rb3
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb323
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb61
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb74
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb177
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb84
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb67
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb5
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb118
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb70
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb6
-rw-r--r--spec/features/snippets/search_snippets_spec.rb6
-rw-r--r--spec/features/user_sees_marketing_header_spec.rb52
-rw-r--r--spec/features/users/active_sessions_spec.rb4
-rw-r--r--spec/features/work_items/work_item_children_spec.rb30
-rw-r--r--spec/finders/autocomplete/users_finder_spec.rb49
-rw-r--r--spec/finders/branches_finder_spec.rb2
-rw-r--r--spec/finders/clusters/agent_authorizations_finder_spec.rb16
-rw-r--r--spec/finders/clusters/agent_tokens_finder_spec.rb48
-rw-r--r--spec/finders/incident_management/timeline_event_tags_finder_spec.rb58
-rw-r--r--spec/finders/projects_finder_spec.rb46
-rw-r--r--spec/finders/users_star_projects_finder_spec.rb10
-rw-r--r--spec/fixtures/api/schemas/entities/codequality_degradation.json5
-rw-r--r--spec/fixtures/api/schemas/entities/codequality_reports_comparer.json16
-rw-r--r--spec/fixtures/api/schemas/entities/protected_ref_access.json25
-rw-r--r--spec/fixtures/api/schemas/graphql/packages/package_details.json12
-rw-r--r--spec/fixtures/api/schemas/ml/run.json46
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule_variable.json17
-rw-r--r--spec/fixtures/api/schemas/project_mirror.json48
-rw-r--r--spec/fixtures/api/schemas/protected_branch.json33
-rw-r--r--spec/fixtures/api/schemas/protected_branches.json6
-rw-r--r--spec/fixtures/api/schemas/protected_tag.json19
-rw-r--r--spec/fixtures/api/schemas/protected_tags.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/metadata.json34
-rw-r--r--spec/fixtures/gitlab/import_export/project.tar.gzbin0 -> 1113 bytes
-rw-r--r--spec/fixtures/gitlab/import_export/uploads.tar.gzbin0 -> 1843 bytes
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric.yml2
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_ee.yml2
-rw-r--r--spec/fixtures/lib/generators/gitlab/usage_metric_definition_generator/sample_metric_with_name_suggestions.yml2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson2
-rw-r--r--spec/fixtures/lib/sbom/package-url-test-cases.json502
-rw-r--r--spec/fixtures/markdown/markdown_golden_master_examples.yml14
-rw-r--r--spec/fixtures/packages/rpm/payload.json18
-rw-r--r--spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml (renamed from spec/fixtures/packages/rpm/repodata/repomd.xml)5
-rw-r--r--spec/frontend/__helpers__/raw_transformer.js6
-rw-r--r--spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap1
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js1
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js18
-rw-r--r--spec/frontend/admin/signup_restrictions/mock_data.js6
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js57
-rw-r--r--spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js107
-rw-r--r--spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap3
-rw-r--r--spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap34
-rw-r--r--spec/frontend/admin/users/components/associations/associations_list_item_spec.js25
-rw-r--r--spec/frontend/admin/users/components/associations/associations_list_spec.js78
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js22
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js7
-rw-r--r--spec/frontend/admin/users/mock_data.js14
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js2
-rw-r--r--spec/frontend/api/groups_api_spec.js27
-rw-r--r--spec/frontend/api/user_api_spec.js17
-rw-r--r--spec/frontend/artifacts/components/artifact_row_spec.js67
-rw-r--r--spec/frontend/artifacts/components/artifacts_table_row_details_spec.js122
-rw-r--r--spec/frontend/artifacts/components/job_artifacts_table_spec.js341
-rw-r--r--spec/frontend/artifacts/graphql/cache_update_spec.js67
-rw-r--r--spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js145
-rw-r--r--spec/frontend/blob/blob_blame_link_spec.js12
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap18
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js105
-rw-r--r--spec/frontend/blob/utils_spec.js62
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js48
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js241
-rw-r--r--spec/frontend/boards/board_list_helper.js2
-rw-r--r--spec/frontend/boards/board_list_spec.js6
-rw-r--r--spec/frontend/boards/components/board_add_new_column_spec.js2
-rw-r--r--spec/frontend/boards/components/board_app_spec.js1
-rw-r--r--spec/frontend/boards/components/board_card_move_to_position_spec.js1
-rw-r--r--spec/frontend/boards/components/board_card_spec.js2
-rw-r--r--spec/frontend/boards/components/board_content_spec.js84
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js44
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js2
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js1
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js11
-rw-r--r--spec/frontend/boards/mock_data.js81
-rw-r--r--spec/frontend/boards/stores/actions_spec.js18
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap139
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js143
-rw-r--r--spec/frontend/branches/mock_data.js7
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js38
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js (renamed from spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js280
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js)19
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js (renamed from spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js (renamed from spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js40
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js (renamed from spec/frontend/pipeline_schedules/mock_data.js)27
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js (renamed from spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js)26
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js (renamed from spec/frontend/runner/admin_runners/admin_runners_app_spec.js)39
-rw-r--r--spec/frontend/ci/runner/components/__snapshots__/runner_status_popover_spec.js.snap (renamed from spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap)0
-rw-r--r--spec/frontend/ci/runner/components/cells/link_cell_spec.js (renamed from spec/frontend/runner/components/cells/link_cell_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_actions_cell_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_owner_cell_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js (renamed from spec/frontend/runner/components/cells/runner_status_cell_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js (renamed from spec/frontend/runner/components/cells/runner_summary_field_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js (renamed from spec/frontend/runner/components/registration/registration_dropdown_spec.js)8
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js (renamed from spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js (renamed from spec/frontend/runner/components/registration/registration_token_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_assigned_item_spec.js (renamed from spec/frontend/runner/components/runner_assigned_item_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_checkbox_spec.js (renamed from spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js (renamed from spec/frontend/runner/components/runner_bulk_delete_spec.js)142
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js (renamed from spec/frontend/runner/components/runner_delete_button_spec.js)23
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_modal_spec.js (renamed from spec/frontend/runner/components/runner_delete_modal_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js (renamed from spec/frontend/runner/components/runner_details_spec.js)12
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_button_spec.js (renamed from spec/frontend/runner/components/runner_edit_button_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js (renamed from spec/frontend/runner/components/runner_filtered_search_bar_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/runner_groups_spec.js (renamed from spec/frontend/runner/components/runner_groups_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js (renamed from spec/frontend/runner/components/runner_header_spec.js)13
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_spec.js (renamed from spec/frontend/runner/components/runner_jobs_spec.js)14
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js (renamed from spec/frontend/runner/components/runner_jobs_table_spec.js)20
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js (renamed from spec/frontend/runner/components/runner_list_empty_state_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js (renamed from spec/frontend/runner/components/runner_list_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/runner_membership_toggle_spec.js (renamed from spec/frontend/runner/components/runner_membership_toggle_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_pagination_spec.js (renamed from spec/frontend/runner/components/runner_pagination_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js (renamed from spec/frontend/runner/components/runner_pause_button_spec.js)10
-rw-r--r--spec/frontend/ci/runner/components/runner_paused_badge_spec.js (renamed from spec/frontend/runner/components/runner_paused_badge_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_projects_spec.js (renamed from spec/frontend/runner/components/runner_projects_spec.js)14
-rw-r--r--spec/frontend/ci/runner/components/runner_status_badge_spec.js (renamed from spec/frontend/runner/components/runner_status_badge_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_status_popover_spec.js (renamed from spec/frontend/runner/components/runner_status_popover_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_tag_spec.js (renamed from spec/frontend/runner/components/runner_tag_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_tags_spec.js (renamed from spec/frontend/runner/components/runner_tags_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/runner_type_badge_spec.js (renamed from spec/frontend/runner/components/runner_type_badge_spec.js)4
-rw-r--r--spec/frontend/ci/runner/components/runner_type_tabs_spec.js (renamed from spec/frontend/runner/components/runner_type_tabs_spec.js)6
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js (renamed from spec/frontend/runner/components/runner_update_form_spec.js)14
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js (renamed from spec/frontend/runner/components/search_tokens/tag_token_spec.js)2
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_count_spec.js (renamed from spec/frontend/runner/components/stat/runner_count_spec.js)12
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js (renamed from spec/frontend/runner/components/stat/runner_single_stat_spec.js)6
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_stats_spec.js (renamed from spec/frontend/runner/components/stat/runner_stats_spec.js)6
-rw-r--r--spec/frontend/ci/runner/graphql/local_state_spec.js (renamed from spec/frontend/runner/graphql/local_state_spec.js)8
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js (renamed from spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js)22
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js (renamed from spec/frontend/runner/group_runners/group_runners_app_spec.js)47
-rw-r--r--spec/frontend/ci/runner/local_storage_alert/save_alert_to_local_storage_spec.js (renamed from spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js)4
-rw-r--r--spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js (renamed from spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js)4
-rw-r--r--spec/frontend/ci/runner/mock_data.js (renamed from spec/frontend/runner/mock_data.js)24
-rw-r--r--spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js (renamed from spec/frontend/runner/runner_edit/runner_edit_app_spec.js)14
-rw-r--r--spec/frontend/ci/runner/runner_search_utils_spec.js (renamed from spec/frontend/runner/runner_search_utils_spec.js)2
-rw-r--r--spec/frontend/ci/runner/runner_update_form_utils_spec.js (renamed from spec/frontend/runner/runner_update_form_utils_spec.js)9
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js (renamed from spec/frontend/runner/sentry_utils_spec.js)4
-rw-r--r--spec/frontend/ci/runner/utils_spec.js (renamed from spec/frontend/runner/utils_spec.js)9
-rw-r--r--spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js175
-rw-r--r--spec/frontend/ci_variable_list/components/ci_group_variables_spec.js181
-rw-r--r--spec/frontend/ci_variable_list/components/ci_project_variables_spec.js202
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js90
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js48
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js2
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js428
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js119
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js323
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js38
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js86
-rw-r--r--spec/frontend/ci_variable_list/mocks.js77
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js319
-rw-r--r--spec/frontend/ci_variable_list/store/getters_spec.js21
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js136
-rw-r--r--spec/frontend/ci_variable_list/store/utils_spec.js49
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js1
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec.js9
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec_helper.js26
-rw-r--r--spec/frontend/content_editor/render_html_and_json_for_all_examples.js83
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js55
-rw-r--r--spec/frontend/content_editor/test_utils.js85
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js67
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js14
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap4
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js56
-rw-r--r--spec/frontend/diffs/mock_data/diff_file.js30
-rw-r--r--spec/frontend/diffs/store/actions_spec.js38
-rw-r--r--spec/frontend/diffs/store/utils_spec.js54
-rw-r--r--spec/frontend/diffs/utils/tree_worker_utils_spec.js64
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js71
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json2
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json6
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json6
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links.json (renamed from spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json)22
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json13
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json11
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml63
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml11
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml3
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml64
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_syntax_desc.yml (renamed from spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml)1
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml4
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml112
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml10
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml10
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js9
-rw-r--r--spec/frontend/environments/environment_actions_spec.js6
-rw-r--r--spec/frontend/environments/environment_rollback_spec.js2
-rw-r--r--spec/frontend/environments/graphql/mock_data.js1
-rw-r--r--spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js7
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb2
-rw-r--r--spec/frontend/fixtures/api_projects.rb2
-rw-r--r--spec/frontend/fixtures/application_settings.rb2
-rw-r--r--spec/frontend/fixtures/blob.rb2
-rw-r--r--spec/frontend/fixtures/branches.rb2
-rw-r--r--spec/frontend/fixtures/clusters.rb2
-rw-r--r--spec/frontend/fixtures/deploy_keys.rb2
-rw-r--r--spec/frontend/fixtures/freeze_period.rb2
-rw-r--r--spec/frontend/fixtures/integrations.rb2
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/job_artifacts.rb28
-rw-r--r--spec/frontend/fixtures/jobs.rb2
-rw-r--r--spec/frontend/fixtures/labels.rb2
-rw-r--r--spec/frontend/fixtures/merge_requests.rb16
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb2
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb2
-rw-r--r--spec/frontend/fixtures/namespaces.rb20
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb13
-rw-r--r--spec/frontend/fixtures/pipelines.rb2
-rw-r--r--spec/frontend/fixtures/projects.rb2
-rw-r--r--spec/frontend/fixtures/prometheus_integration.rb2
-rw-r--r--spec/frontend/fixtures/raw.rb2
-rw-r--r--spec/frontend/fixtures/runner.rb4
-rw-r--r--spec/frontend/fixtures/snippet.rb2
-rw-r--r--spec/frontend/fixtures/static/gl_field_errors.html3
-rw-r--r--spec/frontend/fixtures/todos.rb2
-rw-r--r--spec/frontend/flash_spec.js14
-rw-r--r--spec/frontend/gfm_auto_complete/mock_data.js57
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js95
-rw-r--r--spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js102
-rw-r--r--spec/frontend/gitlab_version_check/index_spec.js116
-rw-r--r--spec/frontend/gl_field_errors_spec.js2
-rw-r--r--spec/frontend/google_cloud/service_accounts/list_spec.js29
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js48
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js56
-rw-r--r--spec/frontend/groups_projects/components/transfer_locations_spec.js377
-rw-r--r--spec/frontend/ide/components/ide_spec.js31
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js26
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js45
-rw-r--r--spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js214
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js27
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js86
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js45
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js76
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js57
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js60
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js2
-rw-r--r--spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js554
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js48
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js8
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js58
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js86
-rw-r--r--spec/frontend/issues/list/mock_data.js117
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js2
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js30
-rw-r--r--spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js20
-rw-r--r--spec/frontend/jobs/components/job/sidebar_spec.js72
-rw-r--r--spec/frontend/jobs/mock_data.js24
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js39
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js103
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js80
-rw-r--r--spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js37
-rw-r--r--spec/frontend/lib/utils/dom_utils_spec.js28
-rw-r--r--spec/frontend/lib/utils/unit_format/index_spec.js6
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js90
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js5
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap223
-rw-r--r--spec/frontend/ml/experiment_tracking/components/experiment_spec.js44
-rw-r--r--spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js27
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js101
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js14
-rw-r--r--spec/frontend/notebook/cells/output/markdown_spec.js44
-rw-r--r--spec/frontend/notebook/mock_data.js2
-rw-r--r--spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap4
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js74
-rw-r--r--spec/frontend/notes/components/note_header_spec.js2
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js2
-rw-r--r--spec/frontend/notes/stores/getters_spec.js22
-rw-r--r--spec/frontend/observability/observability_app_spec.js73
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js60
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js57
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js71
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js152
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js43
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js104
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js34
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap54
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js33
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js158
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js78
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js17
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js280
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js75
-rw-r--r--spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js82
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap7
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js134
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js20
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js2
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js2
-rw-r--r--spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js161
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js26
-rw-r--r--spec/frontend/pipelines/mock_data.js4
-rw-r--r--spec/frontend/pipelines/pipeline_graph/utils_spec.js17
-rw-r--r--spec/frontend/pipelines/pipeline_tabs_spec.js32
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js16
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js6
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap2
-rw-r--r--spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap2
-rw-r--r--spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js2
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js68
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js24
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js47
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js5
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js11
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js268
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js4
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js37
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js45
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js20
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js42
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js151
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js1
-rw-r--r--spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap2
-rw-r--r--spec/frontend/reports/components/grouped_issues_list_spec.js2
-rw-r--r--spec/frontend/reports/components/report_item_spec.js4
-rw-r--r--spec/frontend/reports/grouped_test_report/components/modal_spec.js68
-rw-r--r--spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js96
-rw-r--r--spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js355
-rw-r--r--spec/frontend/reports/grouped_test_report/store/actions_spec.js168
-rw-r--r--spec/frontend/reports/grouped_test_report/store/mutations_spec.js162
-rw-r--r--spec/frontend/reports/grouped_test_report/store/utils_spec.js255
-rw-r--r--spec/frontend/runner/components/runner_stacked_layout_banner_spec.js41
-rw-r--r--spec/frontend/search/mock_data.js84
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js120
-rw-r--r--spec/frontend/search/sidebar/components/filters_spec.js132
-rw-r--r--spec/frontend/search/sidebar/components/scope_navigation_spec.js80
-rw-r--r--spec/frontend/search/store/actions_spec.js35
-rw-r--r--spec/frontend/search/store/mutations_spec.js22
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap16
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_form_spec.js10
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js6
-rw-r--r--spec/frontend/self_monitor/store/mutations_spec.js2
-rw-r--r--spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js93
-rw-r--r--spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js36
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_spec.js285
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js202
-rw-r--r--spec/frontend/token_access/mock_data.js12
-rw-r--r--spec/frontend/token_access/token_access_spec.js7
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js13
-rw-r--r--spec/frontend/users_select/utils_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js425
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap202
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js182
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js60
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js342
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js43
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js70
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js109
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js37
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js129
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js42
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js72
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js13
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/router_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/gitlab_version_check_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js202
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js205
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/mock_data.js42
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/mock_data.js6
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js236
-rw-r--r--spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js30
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js54
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js18
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js12
-rw-r--r--spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap453
-rw-r--r--spec/frontend/webhooks/components/form_url_app_spec.js125
-rw-r--r--spec/frontend/webhooks/components/form_url_mask_item_spec.js78
-rw-r--r--spec/frontend/webhooks/components/push_events_spec.js117
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_description_rendered_spec.js108
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js295
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js1
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js103
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js25
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js205
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js31
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js32
-rw-r--r--spec/frontend/work_items/mock_data.js118
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js39
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js11
-rw-r--r--spec/frontend/work_items/router_spec.js28
-rw-r--r--spec/graphql/graphql_triggers_spec.rb69
-rw-r--r--spec/graphql/mutations/ci/runner/bulk_delete_spec.rb73
-rw-r--r--spec/graphql/mutations/ci/runner/update_spec.rb46
-rw-r--r--spec/graphql/mutations/commits/create_spec.rb2
-rw-r--r--spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb2
-rw-r--r--spec/graphql/mutations/container_repositories/destroy_spec.rb22
-rw-r--r--spec/graphql/mutations/incident_management/timeline_event/create_spec.rb101
-rw-r--r--spec/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb52
-rw-r--r--spec/graphql/mutations/security/ci_configuration/base_security_analyzer_spec.rb2
-rw-r--r--spec/graphql/mutations/todos/restore_many_spec.rb2
-rw-r--r--spec/graphql/resolvers/board_list_issues_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/board_lists_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/board_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/boards_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/container_repositories_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/group_packages_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/incident_management/timeline_event_tags_resolver_spec.rb92
-rw-r--r--spec/graphql/resolvers/project_issues_resolver_spec.rb (renamed from spec/graphql/resolvers/issues_resolver_spec.rb)12
-rw-r--r--spec/graphql/resolvers/projects_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/recent_boards_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/users_resolver_spec.rb14
-rw-r--r--spec/graphql/resolvers/work_item_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/work_items/types_resolver_spec.rb20
-rw-r--r--spec/graphql/types/ci/pipeline_schedule_status_enum_spec.rb2
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb5
-rw-r--r--spec/graphql/types/commit_signature_interface_spec.rb25
-rw-r--r--spec/graphql/types/commit_signatures/gpg_signature_type_spec.rb18
-rw-r--r--spec/graphql/types/commit_signatures/verification_status_enum_spec.rb16
-rw-r--r--spec/graphql/types/commit_signatures/x509_signature_type_spec.rb18
-rw-r--r--spec/graphql/types/commit_type_spec.rb4
-rw-r--r--spec/graphql/types/deployment_details_type_spec.rb2
-rw-r--r--spec/graphql/types/incident_management/timeline_event_tag_type_spec.rb18
-rw-r--r--spec/graphql/types/incident_management/timeline_event_type_spec.rb1
-rw-r--r--spec/graphql/types/issue_type_enum_spec.rb4
-rw-r--r--spec/graphql/types/issue_type_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/ci/runner_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb127
-rw-r--r--spec/graphql/types/projects/branch_rule_type_spec.rb5
-rw-r--r--spec/graphql/types/projects/repository_language_type_spec.rb15
-rw-r--r--spec/graphql/types/release_links_type_spec.rb10
-rw-r--r--spec/graphql/types/release_source_type_spec.rb2
-rw-r--r--spec/graphql/types/repository_type_spec.rb2
-rw-r--r--spec/graphql/types/subscription_type_spec.rb1
-rw-r--r--spec/graphql/types/x509_certificate_type_spec.rb14
-rw-r--r--spec/graphql/types/x509_issuer_type_spec.rb13
-rw-r--r--spec/helpers/appearances_helper_spec.rb9
-rw-r--r--spec/helpers/application_helper_spec.rb83
-rw-r--r--spec/helpers/application_settings_helper_spec.rb4
-rw-r--r--spec/helpers/diff_helper_spec.rb61
-rw-r--r--spec/helpers/environments_helper_spec.rb2
-rw-r--r--spec/helpers/events_helper_spec.rb24
-rw-r--r--spec/helpers/form_helper_spec.rb42
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb15
-rw-r--r--spec/helpers/groups/observability_helper_spec.rb92
-rw-r--r--spec/helpers/groups_helper_spec.rb72
-rw-r--r--spec/helpers/hooks_helper_spec.rb35
-rw-r--r--spec/helpers/icons_helper_spec.rb2
-rw-r--r--spec/helpers/ide_helper_spec.rb2
-rw-r--r--spec/helpers/integrations_helper_spec.rb50
-rw-r--r--spec/helpers/json_helper_spec.rb36
-rw-r--r--spec/helpers/markup_helper_spec.rb149
-rw-r--r--spec/helpers/nav/top_nav_helper_spec.rb126
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb2
-rw-r--r--spec/helpers/projects/ml/experiments_helper_spec.rb49
-rw-r--r--spec/helpers/projects/pipeline_helper_spec.rb1
-rw-r--r--spec/helpers/projects/project_members_helper_spec.rb3
-rw-r--r--spec/helpers/projects_helper_spec.rb3
-rw-r--r--spec/helpers/recaptcha_helper_spec.rb25
-rw-r--r--spec/helpers/routing/packages_helper_spec.rb13
-rw-r--r--spec/helpers/search_helper_spec.rb179
-rw-r--r--spec/helpers/snippets_helper_spec.rb4
-rw-r--r--spec/helpers/todos_helper_spec.rb45
-rw-r--r--spec/initializers/hashie_mash_permitted_patch_spec.rb29
-rw-r--r--spec/initializers/memory_watchdog_spec.rb126
-rw-r--r--spec/initializers/sawyer_patch_spec.rb70
-rw-r--r--spec/lib/api/entities/merge_request_basic_spec.rb16
-rw-r--r--spec/lib/api/entities/ml/mlflow/run_info_spec.rb4
-rw-r--r--spec/lib/api/entities/release_spec.rb8
-rw-r--r--spec/lib/api/entities/user_counts_spec.rb24
-rw-r--r--spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb17
-rw-r--r--spec/lib/api/helpers/packages_helpers_spec.rb119
-rw-r--r--spec/lib/api/helpers_spec.rb6
-rw-r--r--spec/lib/api/validations/validators/email_or_email_list_spec.rb2
-rw-r--r--spec/lib/api/validations/validators/git_ref_spec.rb2
-rw-r--r--spec/lib/api/validations/validators/limit_spec.rb4
-rw-r--r--spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb35
-rw-r--r--spec/lib/backup/database_backup_error_spec.rb8
-rw-r--r--spec/lib/backup/file_backup_error_spec.rb4
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/math_filter_spec.rb13
-rw-r--r--spec/lib/banzai/filter/references/alert_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/commit_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/references/issue_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/references/label_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/references/project_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/references/user_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/repository_link_filter_spec.rb1
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb43
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb2
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb26
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb40
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb8
-rw-r--r--spec/lib/bulk_imports/clients/http_spec.rb16
-rw-r--r--spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb2
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb25
-rw-r--r--spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb131
-rw-r--r--spec/lib/error_tracking/sentry_client/issue_spec.rb2
-rw-r--r--spec/lib/feature/gitaly_spec.rb229
-rw-r--r--spec/lib/feature_spec.rb48
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb2
-rw-r--r--spec/lib/gitlab/app_logger_spec.rb7
-rw-r--r--spec/lib/gitlab/application_context_spec.rb8
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb23
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb62
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb17
-rw-r--r--spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb95
-rw-r--r--spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb222
-rw-r--r--spec/lib/gitlab/background_migration/batched_migration_job_spec.rb77
-rw-r--r--spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb72
-rw-r--r--spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_spec.rb70
-rw-r--r--spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_spec.rb70
-rw-r--r--spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb89
-rw-r--r--spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/cache/metrics_spec.rb118
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb42
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/config/entry/variable_spec.rb118
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb62
-rw-r--r--spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb136
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb119
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/component_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/report_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/reports/security/flag_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/reports/security/reports_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/templates/MATLAB_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/npm_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/templates/themekit_gitlab_ci_yaml_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb77
-rw-r--r--spec/lib/gitlab/cluster/lifecycle_events_spec.rb59
-rw-r--r--spec/lib/gitlab/cluster/puma_worker_killer_initializer_spec.rb30
-rw-r--r--spec/lib/gitlab/config_checker/external_database_checker_spec.rb21
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb25
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb12
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb98
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb1
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb18
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb96
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb2
-rw-r--r--spec/lib/gitlab/database/load_balancing/configuration_spec.rb12
-rw-r--r--spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb4
-rw-r--r--spec/lib/gitlab/database/load_balancing/service_discovery/sampler_spec.rb80
-rw-r--r--spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb34
-rw-r--r--spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb2
-rw-r--r--spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb15
-rw-r--r--spec/lib/gitlab/database/load_balancing_spec.rb5
-rw-r--r--spec/lib/gitlab/database/migration_helpers/v2_spec.rb94
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb1276
-rw-r--r--spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb3
-rw-r--r--spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb95
-rw-r--r--spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb679
-rw-r--r--spec/lib/gitlab/database/migrations/extension_helpers_spec.rb65
-rw-r--r--spec/lib/gitlab/database/migrations/lock_retries_helpers_spec.rb52
-rw-r--r--spec/lib/gitlab/database/migrations/runner_spec.rb63
-rw-r--r--spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb91
-rw-r--r--spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb14
-rw-r--r--spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb8
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb170
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb9
-rw-r--r--spec/lib/gitlab/database/postgres_partition_spec.rb32
-rw-r--r--spec/lib/gitlab/database/query_analyzer_spec.rb8
-rw-r--r--spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb121
-rw-r--r--spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb (renamed from spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb)12
-rw-r--r--spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb38
-rw-r--r--spec/lib/gitlab/database/tables_truncate_spec.rb20
-rw-r--r--spec/lib/gitlab/database/type/symbolized_jsonb_spec.rb64
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb4
-rw-r--r--spec/lib/gitlab/database_spec.rb6
-rw-r--r--spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb3
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb32
-rw-r--r--spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb10
-rw-r--r--spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb10
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb62
-rw-r--r--spec/lib/gitlab/error_tracking_spec.rb19
-rw-r--r--spec/lib/gitlab/experimentation/group_types_spec.rb13
-rw-r--r--spec/lib/gitlab/feature_categories_spec.rb24
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb66
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb12
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb2
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb33
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb227
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb20
-rw-r--r--spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb275
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb134
-rw-r--r--spec/lib/gitlab/github_import/attachments_downloader_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb118
-rw-r--r--spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb75
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb149
-rw-r--r--spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb16
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb83
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb35
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb141
-rw-r--r--spec/lib/gitlab/github_import/representation/protected_branch_spec.rb15
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_requests/review_requests_spec.rb49
-rw-r--r--spec/lib/gitlab/gon_helper_spec.rb52
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/filter_parameters_spec.rb62
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb4
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml6
-rw-r--r--spec/lib/gitlab/import_export/decompressed_archive_size_validator_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/design_repo_restorer_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/group/tree_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/import_test_coverage_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb75
-rw-r--r--spec/lib/gitlab/import_export/project/relation_saver_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb54
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml4
-rw-r--r--spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb4
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb99
-rw-r--r--spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb29
-rw-r--r--spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb19
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb20
-rw-r--r--spec/lib/gitlab/json_spec.rb56
-rw-r--r--spec/lib/gitlab/kas_spec.rb12
-rw-r--r--spec/lib/gitlab/kroki_spec.rb3
-rw-r--r--spec/lib/gitlab/memory/watchdog/configuration_spec.rb61
-rw-r--r--spec/lib/gitlab/memory/watchdog/configurator_spec.rb199
-rw-r--r--spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb39
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb10
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb4
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb2
-rw-r--r--spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/dashboard/finder_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/dashboard/url_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/global_search_slis_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/loose_foreign_keys_slis_spec.rb81
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb47
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb20
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb25
-rw-r--r--spec/lib/gitlab/observability_spec.rb33
-rw-r--r--spec/lib/gitlab/octokit/middleware_spec.rb8
-rw-r--r--spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb2
-rw-r--r--spec/lib/gitlab/pagination_delegate_spec.rb157
-rw-r--r--spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb4
-rw-r--r--spec/lib/gitlab/project_template_spec.rb14
-rw-r--r--spec/lib/gitlab/qa_spec.rb29
-rw-r--r--spec/lib/gitlab/query_limiting/transaction_spec.rb3
-rw-r--r--spec/lib/gitlab/redis/multi_store_spec.rb544
-rw-r--r--spec/lib/gitlab/request_forgery_protection_spec.rb6
-rw-r--r--spec/lib/gitlab/runtime_spec.rb2
-rw-r--r--spec/lib/gitlab/service_desk_email_spec.rb40
-rw-r--r--spec/lib/gitlab/sidekiq_config_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb4
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb312
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb4
-rw-r--r--spec/lib/gitlab/sidekiq_middleware_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb322
-rw-r--r--spec/lib/gitlab/slash_commands/application_help_spec.rb22
-rw-r--r--spec/lib/gitlab/slash_commands/command_spec.rb20
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/incident_management/incident_new_spec.rb15
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb45
-rw-r--r--spec/lib/gitlab/tracking/helpers/weak_password_error_event_spec.rb45
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb51
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb20
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb434
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb25
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb63
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_disabled_metric_spec.rb19
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_metric_spec.rb33
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_period_setting_metric_spec.rb21
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_setting_enabled_metric_spec.rb21
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb55
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb52
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb24
-rw-r--r--spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb13
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/having_constraints_spec.rb19
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/where_constraints_spec.rb (renamed from spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/constraints_spec.rb)7
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb35
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb6
-rw-r--r--spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb196
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb57
-rw-r--r--spec/lib/gitlab/utils_spec.rb14
-rw-r--r--spec/lib/gitlab/webpack/file_loader_spec.rb4
-rw-r--r--spec/lib/grafana/client_spec.rb2
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb147
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb2
-rw-r--r--spec/lib/rouge/formatters/html_gitlab_spec.rb10
-rw-r--r--spec/lib/sbom/package_url/argument_validator_spec.rb51
-rw-r--r--spec/lib/sbom/package_url/decoder_spec.rb121
-rw-r--r--spec/lib/sbom/package_url/encoder_spec.rb29
-rw-r--r--spec/lib/sbom/package_url/normalizer_spec.rb76
-rw-r--r--spec/lib/sbom/package_url_spec.rb162
-rw-r--r--spec/lib/serializers/symbolized_json_spec.rb42
-rw-r--r--spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb46
-rw-r--r--spec/lib/sidebars/projects/menus/monitor_menu_spec.rb14
-rw-r--r--spec/lib/unnested_in_filters/rewriter_spec.rb91
-rw-r--r--spec/mailers/emails/identity_verification_spec.rb42
-rw-r--r--spec/mailers/emails/releases_spec.rb1
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb71
-rw-r--r--spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb2
-rw-r--r--spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb9
-rw-r--r--spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb66
-rw-r--r--spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb37
-rw-r--r--spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb91
-rw-r--r--spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb36
-rw-r--r--spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb37
-rw-r--r--spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb16
-rw-r--r--spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb27
-rw-r--r--spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb29
-rw-r--r--spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb63
-rw-r--r--spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb61
-rw-r--r--spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb49
-rw-r--r--spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb69
-rw-r--r--spec/migrations/finalize_invalid_member_cleanup_spec.rb72
-rw-r--r--spec/migrations/queue_backfill_user_details_fields_spec.rb24
-rw-r--r--spec/migrations/queue_populate_projects_star_count_spec.rb24
-rw-r--r--spec/migrations/recount_epic_cache_counts_spec.rb32
-rw-r--r--spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb41
-rw-r--r--spec/migrations/sanitize_confidential_note_todos_spec.rb33
-rw-r--r--spec/migrations/schedule_migrate_shared_vulnerability_scanners_spec.rb59
-rw-r--r--spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb41
-rw-r--r--spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb42
-rw-r--r--spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb42
-rw-r--r--spec/models/active_session_spec.rb22
-rw-r--r--spec/models/alert_management/http_integration_spec.rb9
-rw-r--r--spec/models/appearance_spec.rb14
-rw-r--r--spec/models/application_setting_spec.rb17
-rw-r--r--spec/models/broadcast_message_spec.rb7
-rw-r--r--spec/models/ci/bridge_spec.rb93
-rw-r--r--spec/models/ci/build_metadata_spec.rb32
-rw-r--r--spec/models/ci/build_runner_session_spec.rb6
-rw-r--r--spec/models/ci/build_spec.rb121
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb5
-rw-r--r--spec/models/ci/build_trace_spec.rb4
-rw-r--r--spec/models/ci/pipeline_metadata_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb39
-rw-r--r--spec/models/ci/processable_spec.rb22
-rw-r--r--spec/models/ci/secure_file_spec.rb11
-rw-r--r--spec/models/ci/sources/pipeline_spec.rb4
-rw-r--r--spec/models/ci/stage_spec.rb2
-rw-r--r--spec/models/ci/trigger_request_spec.rb2
-rw-r--r--spec/models/ci/unit_test_spec.rb2
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb5
-rw-r--r--spec/models/clusters/applications/crossplane_spec.rb5
-rw-r--r--spec/models/clusters/applications/helm_spec.rb4
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb5
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb4
-rw-r--r--spec/models/clusters/applications/knative_spec.rb4
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb11
-rw-r--r--spec/models/clusters/applications/runner_spec.rb4
-rw-r--r--spec/models/clusters/cluster_spec.rb4
-rw-r--r--spec/models/clusters/integrations/prometheus_spec.rb10
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb57
-rw-r--r--spec/models/clusters/providers/aws_spec.rb2
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb3
-rw-r--r--spec/models/commit_signatures/gpg_signature_spec.rb6
-rw-r--r--spec/models/commit_status_spec.rb1
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb6
-rw-r--r--spec/models/concerns/ci/has_variable_spec.rb1
-rw-r--r--spec/models/concerns/ci/partitionable/switch_spec.rb316
-rw-r--r--spec/models/concerns/ci/partitionable_spec.rb21
-rw-r--r--spec/models/concerns/encrypted_user_password_spec.rb138
-rw-r--r--spec/models/concerns/file_store_mounter_spec.rb93
-rw-r--r--spec/models/concerns/has_user_type_spec.rb42
-rw-r--r--spec/models/concerns/issuable_spec.rb41
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb38
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb2
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb10
-rw-r--r--spec/models/concerns/subquery_spec.rb61
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb10
-rw-r--r--spec/models/container_repository_spec.rb32
-rw-r--r--spec/models/dependency_proxy/group_setting_spec.rb5
-rw-r--r--spec/models/deploy_token_spec.rb2
-rw-r--r--spec/models/deployment_spec.rb26
-rw-r--r--spec/models/diff_discussion_spec.rb2
-rw-r--r--spec/models/diff_viewer/server_side_spec.rb28
-rw-r--r--spec/models/environment_spec.rb4
-rw-r--r--spec/models/environment_status_spec.rb2
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb28
-rw-r--r--spec/models/event_collection_spec.rb254
-rw-r--r--spec/models/event_spec.rb8
-rw-r--r--spec/models/experiment_spec.rb428
-rw-r--r--spec/models/experiment_subject_spec.rb72
-rw-r--r--spec/models/experiment_user_spec.rb14
-rw-r--r--spec/models/exported_protected_branch_spec.rb2
-rw-r--r--spec/models/factories_spec.rb136
-rw-r--r--spec/models/group_spec.rb180
-rw-r--r--spec/models/hooks/active_hook_filter_spec.rb113
-rw-r--r--spec/models/hooks/project_hook_spec.rb20
-rw-r--r--spec/models/hooks/system_hook_spec.rb11
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb19
-rw-r--r--spec/models/hooks/web_hook_spec.rb142
-rw-r--r--spec/models/incident_management/timeline_event_spec.rb1
-rw-r--r--spec/models/incident_management/timeline_event_tag_spec.rb37
-rw-r--r--spec/models/instance_metadata_spec.rb3
-rw-r--r--spec/models/integration_spec.rb55
-rw-r--r--spec/models/integrations/assembla_spec.rb4
-rw-r--r--spec/models/integrations/bamboo_spec.rb46
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb22
-rw-r--r--spec/models/integrations/base_issue_tracker_spec.rb4
-rw-r--r--spec/models/integrations/base_third_party_wiki_spec.rb4
-rw-r--r--spec/models/integrations/buildkite_spec.rb6
-rw-r--r--spec/models/integrations/campfire_spec.rb4
-rw-r--r--spec/models/integrations/chat_message/pipeline_message_spec.rb41
-rw-r--r--spec/models/integrations/confluence_spec.rb5
-rw-r--r--spec/models/integrations/datadog_spec.rb8
-rw-r--r--spec/models/integrations/discord_spec.rb2
-rw-r--r--spec/models/integrations/drone_ci_spec.rb7
-rw-r--r--spec/models/integrations/emails_on_push_spec.rb2
-rw-r--r--spec/models/integrations/hangouts_chat_spec.rb6
-rw-r--r--spec/models/integrations/harbor_spec.rb11
-rw-r--r--spec/models/integrations/jenkins_spec.rb142
-rw-r--r--spec/models/integrations/jira_spec.rb77
-rw-r--r--spec/models/integrations/mattermost_slash_commands_spec.rb4
-rw-r--r--spec/models/integrations/microsoft_teams_spec.rb36
-rw-r--r--spec/models/integrations/mock_ci_spec.rb2
-rw-r--r--spec/models/integrations/packagist_spec.rb84
-rw-r--r--spec/models/integrations/pipelines_email_spec.rb2
-rw-r--r--spec/models/integrations/prometheus_spec.rb8
-rw-r--r--spec/models/integrations/pushover_spec.rb4
-rw-r--r--spec/models/integrations/shimo_spec.rb6
-rw-r--r--spec/models/integrations/slack_slash_commands_spec.rb2
-rw-r--r--spec/models/integrations/slack_spec.rb140
-rw-r--r--spec/models/integrations/teamcity_spec.rb50
-rw-r--r--spec/models/integrations/zentao_spec.rb6
-rw-r--r--spec/models/issue_spec.rb32
-rw-r--r--spec/models/jira_connect_installation_spec.rb16
-rw-r--r--spec/models/member_spec.rb8
-rw-r--r--spec/models/members/group_member_spec.rb6
-rw-r--r--spec/models/members/last_group_owner_assigner_spec.rb88
-rw-r--r--spec/models/members/project_member_spec.rb4
-rw-r--r--spec/models/merge_request_diff_file_spec.rb10
-rw-r--r--spec/models/merge_request_diff_spec.rb13
-rw-r--r--spec/models/merge_request_spec.rb73
-rw-r--r--spec/models/metrics/dashboard/annotation_spec.rb2
-rw-r--r--spec/models/ml/candidate_metric_spec.rb13
-rw-r--r--spec/models/ml/candidate_spec.rb36
-rw-r--r--spec/models/namespace_setting_spec.rb6
-rw-r--r--spec/models/namespace_spec.rb20
-rw-r--r--spec/models/network/graph_spec.rb24
-rw-r--r--spec/models/note_spec.rb68
-rw-r--r--spec/models/notification_setting_spec.rb6
-rw-r--r--spec/models/oauth_access_token_spec.rb31
-rw-r--r--spec/models/operations/feature_flag_spec.rb5
-rw-r--r--spec/models/packages/package_file_spec.rb2
-rw-r--r--spec/models/packages/package_spec.rb12
-rw-r--r--spec/models/pages_domain_spec.rb4
-rw-r--r--spec/models/personal_access_token_spec.rb57
-rw-r--r--spec/models/preloaders/labels_preloader_spec.rb5
-rw-r--r--spec/models/preloaders/project_root_ancestor_preloader_spec.rb8
-rw-r--r--spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb58
-rw-r--r--spec/models/project_authorization_spec.rb77
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb6
-rw-r--r--spec/models/project_setting_spec.rb9
-rw-r--r--spec/models/project_spec.rb108
-rw-r--r--spec/models/projects/wiki_repository_spec.rb16
-rw-r--r--spec/models/protected_branch_spec.rb45
-rw-r--r--spec/models/repository_spec.rb7
-rw-r--r--spec/models/serverless/domain_cluster_spec.rb10
-rw-r--r--spec/models/spam_log_spec.rb37
-rw-r--r--spec/models/terraform/state_spec.rb6
-rw-r--r--spec/models/terraform/state_version_spec.rb9
-rw-r--r--spec/models/time_tracking/timelog_category_spec.rb4
-rw-r--r--spec/models/todo_spec.rb2
-rw-r--r--spec/models/user_spec.rb274
-rw-r--r--spec/models/users/calloutable_spec.rb4
-rw-r--r--spec/models/users/ghost_user_migration_spec.rb13
-rw-r--r--spec/models/users/namespace_commit_email_spec.rb21
-rw-r--r--spec/models/users_star_project_spec.rb80
-rw-r--r--spec/models/work_items/type_spec.rb7
-rw-r--r--spec/models/work_items/widgets/hierarchy_spec.rb32
-rw-r--r--spec/models/work_items/widgets/milestone_spec.rb27
-rw-r--r--spec/policies/global_policy_spec.rb30
-rw-r--r--spec/policies/group_member_policy_spec.rb130
-rw-r--r--spec/policies/group_policy_spec.rb13
-rw-r--r--spec/policies/issuable_policy_spec.rb8
-rw-r--r--spec/policies/note_policy_spec.rb24
-rw-r--r--spec/policies/project_member_policy_spec.rb32
-rw-r--r--spec/policies/project_policy_spec.rb138
-rw-r--r--spec/policies/user_policy_spec.rb44
-rw-r--r--spec/presenters/blob_presenter_spec.rb12
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb41
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb4
-rw-r--r--spec/presenters/deployments/deployment_presenter_spec.rb2
-rw-r--r--spec/presenters/issue_presenter_spec.rb36
-rw-r--r--spec/presenters/packages/npm/package_presenter_spec.rb8
-rw-r--r--spec/presenters/project_presenter_spec.rb8
-rw-r--r--spec/rack_servers/configs/config.ru2
-rw-r--r--spec/requests/admin/broadcast_messages_controller_spec.rb89
-rw-r--r--spec/requests/api/admin/ci/variables_spec.rb27
-rw-r--r--spec/requests/api/alert_management_alerts_spec.rb8
-rw-r--r--spec/requests/api/boards_spec.rb4
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb16
-rw-r--r--spec/requests/api/ci/jobs_spec.rb26
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb14
-rw-r--r--spec/requests/api/ci/resource_groups_spec.rb44
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb24
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb2
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb2
-rw-r--r--spec/requests/api/ci/runners_spec.rb2
-rw-r--r--spec/requests/api/ci/secure_files_spec.rb12
-rw-r--r--spec/requests/api/ci/triggers_spec.rb2
-rw-r--r--spec/requests/api/ci/variables_spec.rb25
-rw-r--r--spec/requests/api/clusters/agent_tokens_spec.rb12
-rw-r--r--spec/requests/api/dependency_proxy_spec.rb2
-rw-r--r--spec/requests/api/deployments_spec.rb4
-rw-r--r--spec/requests/api/features_spec.rb39
-rw-r--r--spec/requests/api/files_spec.rb35
-rw-r--r--spec/requests/api/go_proxy_spec.rb31
-rw-r--r--spec/requests/api/graphql/boards/board_list_issues_query_spec.rb40
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb63
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/pipelines_spec.rb6
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb3
-rw-r--r--spec/requests/api/graphql/group/work_item_types_spec.rb11
-rw-r--r--spec/requests/api/graphql/issues_spec.rb117
-rw-r--r--spec/requests/api/graphql/metadata_query_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb41
-rw-r--r--spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb23
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb32
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb57
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb33
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb64
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb14
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb105
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_task_spec.rb16
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb20
-rw-r--r--spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb104
-rw-r--r--spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb104
-rw-r--r--spec/requests/api/graphql/project/branch_rules_spec.rb143
-rw-r--r--spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb43
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb286
-rw-r--r--spec/requests/api/graphql/project/languages_spec.rb62
-rw-r--r--spec/requests/api/graphql/project/tree/tree_spec.rb86
-rw-r--r--spec/requests/api/graphql/project/work_item_types_spec.rb11
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb25
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb46
-rw-r--r--spec/requests/api/group_boards_spec.rb2
-rw-r--r--spec/requests/api/group_container_repositories_spec.rb8
-rw-r--r--spec/requests/api/group_variables_spec.rb25
-rw-r--r--spec/requests/api/groups_spec.rb26
-rw-r--r--spec/requests/api/import_github_spec.rb22
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb42
-rw-r--r--spec/requests/api/invitations_spec.rb133
-rw-r--r--spec/requests/api/issues/issues_spec.rb4
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb4
-rw-r--r--spec/requests/api/labels_spec.rb2
-rw-r--r--spec/requests/api/markdown_spec.rb2
-rw-r--r--spec/requests/api/maven_packages_spec.rb10
-rw-r--r--spec/requests/api/members_spec.rb125
-rw-r--r--spec/requests/api/merge_requests_spec.rb6
-rw-r--r--spec/requests/api/metrics/dashboard/annotations_spec.rb2
-rw-r--r--spec/requests/api/ml/mlflow_spec.rb7
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb33
-rw-r--r--spec/requests/api/pages/pages_spec.rb2
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb19
-rw-r--r--spec/requests/api/project_attributes.yml1
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb22
-rw-r--r--spec/requests/api/project_import_spec.rb4
-rw-r--r--spec/requests/api/project_milestones_spec.rb2
-rw-r--r--spec/requests/api/project_snippets_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb64
-rw-r--r--spec/requests/api/protected_branches_spec.rb55
-rw-r--r--spec/requests/api/protected_tags_spec.rb8
-rw-r--r--spec/requests/api/pypi_packages_spec.rb3
-rw-r--r--spec/requests/api/release/links_spec.rb18
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb101
-rw-r--r--spec/requests/api/rpm_project_packages_spec.rb70
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb5
-rw-r--r--spec/requests/api/search_spec.rb64
-rw-r--r--spec/requests/api/snippets_spec.rb10
-rw-r--r--spec/requests/api/submodules_spec.rb2
-rw-r--r--spec/requests/api/suggestions_spec.rb15
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb5
-rw-r--r--spec/requests/api/terraform/state_spec.rb37
-rw-r--r--spec/requests/api/todos_spec.rb2
-rw-r--r--spec/requests/api/unleash_spec.rb12
-rw-r--r--spec/requests/api/user_counts_spec.rb26
-rw-r--r--spec/requests/api/users_spec.rb188
-rw-r--r--spec/requests/groups/observability_controller_spec.rb218
-rw-r--r--spec/requests/groups/settings/access_tokens_controller_spec.rb25
-rw-r--r--spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb59
-rw-r--r--spec/requests/jira_connect/oauth_application_ids_controller_spec.rb34
-rw-r--r--spec/requests/jira_connect/subscriptions_controller_spec.rb42
-rw-r--r--spec/requests/oauth/tokens_controller_spec.rb7
-rw-r--r--spec/requests/product_analytics/collector_app_attack_spec.rb41
-rw-r--r--spec/requests/product_analytics/collector_app_spec.rb58
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb4
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb6
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb2
-rw-r--r--spec/requests/projects/issue_links_controller_spec.rb20
-rw-r--r--spec/requests/projects/merge_requests/context_commit_diffs_spec.rb2
-rw-r--r--spec/requests/projects/merge_requests/diffs_spec.rb18
-rw-r--r--spec/requests/projects/ml/experiments_controller_spec.rb100
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb25
-rw-r--r--spec/requests/projects/work_items_spec.rb20
-rw-r--r--spec/requests/search_controller_spec.rb14
-rw-r--r--spec/requests/self_monitoring_project_spec.rb8
-rw-r--r--spec/requests/verifies_with_email_spec.rb152
-rw-r--r--spec/routing/group_routing_spec.rb12
-rw-r--r--spec/routing/project_routing_spec.rb4
-rw-r--r--spec/rubocop/cop/api/ensure_string_detail_spec.rb136
-rw-r--r--spec/rubocop/cop/gitlab/json_spec.rb45
-rw-r--r--spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb4
-rw-r--r--spec/rubocop/cop/gitlab/rspec/avoid_setup_spec.rb36
-rw-r--r--spec/rubocop/cop/graphql/enum_names_spec.rb52
-rw-r--r--spec/rubocop/cop/graphql/enum_values_spec.rb79
-rw-r--r--spec/rubocop/cop/migration/schema_addition_methods_no_post_spec.rb24
-rw-r--r--spec/rubocop/cop/rake/require_spec.rb60
-rw-r--r--spec/rubocop/cop/rspec/duplicate_spec_location_spec.rb (renamed from spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb)18
-rw-r--r--spec/rubocop/cop/rspec/factory_bot/strategy_in_callback_spec.rb71
-rw-r--r--spec/rubocop/migration_helpers_spec.rb17
-rw-r--r--spec/rubocop_spec_helper.rb5
-rw-r--r--spec/scripts/failed_tests_spec.rb2
-rw-r--r--spec/scripts/lib/glfm/update_example_snapshots_spec.rb57
-rw-r--r--spec/scripts/lib/glfm/update_specification_spec.rb240
-rw-r--r--spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb9
-rw-r--r--spec/scripts/pipeline_test_report_builder_spec.rb2
-rw-r--r--spec/serializers/ci/pipeline_entity_spec.rb20
-rw-r--r--spec/serializers/codequality_degradation_entity_spec.rb3
-rw-r--r--spec/serializers/diff_file_entity_spec.rb44
-rw-r--r--spec/serializers/diffs_entity_spec.rb73
-rw-r--r--spec/serializers/diffs_metadata_entity_spec.rb53
-rw-r--r--spec/serializers/integrations/event_entity_spec.rb1
-rw-r--r--spec/serializers/integrations/field_entity_spec.rb34
-rw-r--r--spec/serializers/issue_board_entity_spec.rb14
-rw-r--r--spec/serializers/issue_entity_spec.rb16
-rw-r--r--spec/serializers/linked_project_issue_entity_spec.rb16
-rw-r--r--spec/serializers/merge_request_poll_cached_widget_entity_spec.rb16
-rw-r--r--spec/serializers/merge_request_poll_widget_entity_spec.rb6
-rw-r--r--spec/serializers/merge_requests/pipeline_entity_spec.rb21
-rw-r--r--spec/serializers/paginated_diff_entity_spec.rb76
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb3
-rw-r--r--spec/serializers/remote_mirror_entity_spec.rb3
-rw-r--r--spec/services/admin/set_feature_flag_service_spec.rb9
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb2
-rw-r--r--spec/services/boards/issues/list_service_spec.rb4
-rw-r--r--spec/services/branches/create_service_spec.rb23
-rw-r--r--spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb4
-rw-r--r--spec/services/ci/after_requeue_job_service_spec.rb36
-rw-r--r--spec/services/ci/compare_test_reports_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/partitioning_spec.rb5
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb38
-rw-r--r--spec/services/ci/create_pipeline_service/variables_spec.rb136
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb47
-rw-r--r--spec/services/ci/job_artifacts/create_service_spec.rb6
-rw-r--r--spec/services/ci/job_artifacts/destroy_batch_service_spec.rb81
-rw-r--r--spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb127
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb2
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb23
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml54
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml53
-rw-r--r--spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb63
-rw-r--r--spec/services/ci/play_build_service_spec.rb10
-rw-r--r--spec/services/ci/process_build_service_spec.rb189
-rw-r--r--spec/services/ci/register_job_service_spec.rb13
-rw-r--r--spec/services/ci/retry_job_service_spec.rb284
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb23
-rw-r--r--spec/services/ci/runners/bulk_delete_runners_service_spec.rb170
-rw-r--r--spec/services/ci/runners/set_runner_associated_projects_service_spec.rb13
-rw-r--r--spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb44
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb204
-rw-r--r--spec/services/clusters/applications/check_uninstall_progress_service_spec.rb155
-rw-r--r--spec/services/clusters/applications/check_upgrade_progress_service_spec.rb94
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb279
-rw-r--r--spec/services/clusters/applications/patch_service_spec.rb80
-rw-r--r--spec/services/clusters/applications/prometheus_update_service_spec.rb111
-rw-r--r--spec/services/clusters/applications/update_service_spec.rb91
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/verify_provision_status_service_spec.rb10
-rw-r--r--spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb223
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb2
-rw-r--r--spec/services/dependency_proxy/find_cached_manifest_service_spec.rb30
-rw-r--r--spec/services/deployments/create_for_build_service_spec.rb87
-rw-r--r--spec/services/environments/create_for_build_service_spec.rb (renamed from spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb)112
-rw-r--r--spec/services/event_create_service_spec.rb107
-rw-r--r--spec/services/git/base_hooks_service_spec.rb94
-rw-r--r--spec/services/git/tag_push_service_spec.rb1
-rw-r--r--spec/services/google_cloud/generate_pipeline_service_spec.rb11
-rw-r--r--spec/services/groups/create_service_spec.rb29
-rw-r--r--spec/services/groups/destroy_service_spec.rb40
-rw-r--r--spec/services/groups/transfer_service_spec.rb4
-rw-r--r--spec/services/groups/update_service_spec.rb9
-rw-r--r--spec/services/groups/update_shared_runners_service_spec.rb2
-rw-r--r--spec/services/import/fogbugz_service_spec.rb2
-rw-r--r--spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb2
-rw-r--r--spec/services/incident_management/timeline_event_tags/create_service_spec.rb71
-rw-r--r--spec/services/incident_management/timeline_events/create_service_spec.rb107
-rw-r--r--spec/services/incident_management/timeline_events/update_service_spec.rb6
-rw-r--r--spec/services/issuable/discussions_list_service_spec.rb27
-rw-r--r--spec/services/issues/close_service_spec.rb21
-rw-r--r--spec/services/issues/create_service_spec.rb61
-rw-r--r--spec/services/issues/export_csv_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb48
-rw-r--r--spec/services/issues/relative_position_rebalancing_service_spec.rb12
-rw-r--r--spec/services/issues/reopen_service_spec.rb20
-rw-r--r--spec/services/issues/update_service_spec.rb33
-rw-r--r--spec/services/labels/promote_service_spec.rb2
-rw-r--r--spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb198
-rw-r--r--spec/services/markup/rendering_service_spec.rb163
-rw-r--r--spec/services/members/destroy_service_spec.rb25
-rw-r--r--spec/services/members/invite_service_spec.rb6
-rw-r--r--spec/services/members/update_service_spec.rb356
-rw-r--r--spec/services/merge_requests/approval_service_spec.rb32
-rw-r--r--spec/services/merge_requests/build_service_spec.rb14
-rw-r--r--spec/services/merge_requests/create_service_spec.rb43
-rw-r--r--spec/services/merge_requests/mergeability/run_checks_service_spec.rb22
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb30
-rw-r--r--spec/services/merge_requests/remove_approval_service_spec.rb16
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb9
-rw-r--r--spec/services/merge_requests/update_service_spec.rb4
-rw-r--r--spec/services/milestones/transfer_service_spec.rb4
-rw-r--r--spec/services/namespaces/statistics_refresher_service_spec.rb19
-rw-r--r--spec/services/notes/build_service_spec.rb8
-rw-r--r--spec/services/notes/update_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb17
-rw-r--r--spec/services/packages/composer/composer_json_service_spec.rb2
-rw-r--r--spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb17
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb4
-rw-r--r--spec/services/packages/rpm/repository_metadata/base_builder_spec.rb33
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb54
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb21
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb27
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb21
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb23
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb35
-rw-r--r--spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb (renamed from spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb)2
-rw-r--r--spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb177
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb2
-rw-r--r--spec/services/projects/move_users_star_projects_service_spec.rb5
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb10
-rw-r--r--spec/services/protected_branches/api_service_spec.rb34
-rw-r--r--spec/services/protected_branches/cache_service_spec.rb11
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb15
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb34
-rw-r--r--spec/services/resource_events/change_milestone_service_spec.rb31
-rw-r--r--spec/services/resource_events/change_state_service_spec.rb10
-rw-r--r--spec/services/search/group_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb154
-rw-r--r--spec/services/security/ci_configuration/sast_parser_service_spec.rb8
-rw-r--r--spec/services/security/merge_reports_service_spec.rb4
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb4
-rw-r--r--spec/services/tags/create_service_spec.rb20
-rw-r--r--spec/services/todo_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb6
-rw-r--r--spec/services/topics/merge_service_spec.rb8
-rw-r--r--spec/services/users/approve_service_spec.rb2
-rw-r--r--spec/services/users/ban_service_spec.rb2
-rw-r--r--spec/services/users/destroy_service_spec.rb620
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb29
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_service_spec.rb92
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb97
-rw-r--r--spec/services/users/reject_service_spec.rb31
-rw-r--r--spec/services/users/unban_service_spec.rb2
-rw-r--r--spec/services/web_hook_service_spec.rb43
-rw-r--r--spec/services/work_items/create_service_spec.rb10
-rw-r--r--spec/services/work_items/update_service_spec.rb28
-rw-r--r--spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb22
-rw-r--r--spec/services/work_items/widgets/milestone_service/create_service_spec.rb28
-rw-r--r--spec/services/work_items/widgets/milestone_service/update_service_spec.rb58
-rw-r--r--spec/services/x509_certificate_revoke_service_spec.rb6
-rw-r--r--spec/spec_helper.rb11
-rw-r--r--spec/support/database/query_recorder.rb9
-rw-r--r--spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb4
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb2
-rw-r--r--spec/support/helpers/bare_repo_operations.rb44
-rw-r--r--spec/support/helpers/ci/template_helpers.rb45
-rw-r--r--spec/support/helpers/content_security_policy_helpers.rb20
-rw-r--r--spec/support/helpers/database/multiple_databases_helpers.rb (renamed from spec/support/database/multiple_databases.rb)75
-rw-r--r--spec/support/helpers/features/access_token_helpers.rb19
-rw-r--r--spec/support/helpers/features/releases_helpers.rb2
-rw-r--r--spec/support/helpers/filter_spec_helper.rb5
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb4
-rw-r--r--spec/support/helpers/full_name_helper.rb9
-rw-r--r--spec/support/helpers/git_helpers.rb11
-rw-r--r--spec/support/helpers/graphql_helpers.rb2
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb25
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb6
-rw-r--r--spec/support/helpers/reference_parser_helpers.rb4
-rw-r--r--spec/support/helpers/search_helpers.rb4
-rw-r--r--spec/support/helpers/stub_configuration.rb4
-rw-r--r--spec/support/helpers/stub_feature_flags.rb4
-rw-r--r--spec/support/helpers/test_env.rb42
-rw-r--r--spec/support/helpers/usage_data_helpers.rb12
-rw-r--r--spec/support/matchers/exceed_query_limit.rb2
-rw-r--r--spec/support/matchers/graphql_matchers.rb6
-rw-r--r--spec/support/migration.rb36
-rw-r--r--spec/support/models/ci/partitioning_testing/cascade_check.rb (renamed from spec/support/models/partitionable_check.rb)25
-rw-r--r--spec/support/models/ci/partitioning_testing/partition_identifiers.rb13
-rw-r--r--spec/support/models/ci/partitioning_testing/rspec_hooks.rb19
-rw-r--r--spec/support/models/ci/partitioning_testing/schema_helpers.rb86
-rw-r--r--spec/support/multiple_databases.rb25
-rw-r--r--spec/support/rate_limiter.rb7
-rw-r--r--spec/support/redis.rb6
-rw-r--r--spec/support/rspec.rb3
-rw-r--r--spec/support/rspec_order_todo.yml27
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb40
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb3
-rw-r--r--spec/support/shared_contexts/container_repositories_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/jobs/handling_retried_jobs_shared_context.rb26
-rw-r--r--spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb26
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb5
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/policies/project_policy_table_shared_context.rb57
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb4
-rw-r--r--spec/support/shared_examples/ci/retryable_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/controllers/preferred_language_switcher_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/features/confidential_notes_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb304
-rw-r--r--spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/graphql/notes_creation_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/lib/cache_helpers_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/lib/email/email_shared_examples.rb140
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb46
-rw-r--r--spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/sentry/client_shared_examples.rb37
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb150
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb2
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb75
-rw-r--r--spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb119
-rw-r--r--spec/support/shared_examples/requests/api/discussions_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb170
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb77
-rw-r--r--spec/support/shared_examples/requests/api/issues_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/members_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/base_rpm_service_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb112
-rw-r--r--spec/support/shared_examples/services/merge_status_updated_trigger_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb56
-rw-r--r--spec/support/sidekiq_middleware.rb9
-rw-r--r--spec/support/webmock.rb7
-rw-r--r--spec/support_specs/database/multiple_databases_helpers_spec.rb (renamed from spec/support_specs/database/multiple_databases_spec.rb)5
-rw-r--r--spec/support_specs/graphql/arguments_spec.rb4
-rw-r--r--spec/tasks/gitlab/sidekiq_rake_spec.rb42
-rw-r--r--spec/tasks/gitlab/update_templates_rake_spec.rb7
-rw-r--r--spec/tooling/danger/project_helper_spec.rb6
-rw-r--r--spec/tooling/lib/tooling/find_codeowners_spec.rb4
-rw-r--r--spec/tooling/lib/tooling/helm3_client_spec.rb26
-rw-r--r--spec/tooling/rspec_flaky/example_spec.rb9
-rw-r--r--spec/tooling/rspec_flaky/flaky_example_spec.rb79
-rw-r--r--spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb3
-rw-r--r--spec/tooling/rspec_flaky/listener_spec.rb2
-rw-r--r--spec/tooling/rspec_flaky/report_spec.rb1
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb2
-rw-r--r--spec/uploaders/object_storage/cdn/google_cdn_spec.rb46
-rw-r--r--spec/uploaders/object_storage/cdn_spec.rb27
-rw-r--r--spec/validators/web_hooks/wildcard_branch_filter_validator_spec.rb (renamed from spec/validators/branch_filter_validator_spec.rb)2
-rw-r--r--spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb23
-rw-r--r--spec/views/admin/application_settings/general.html.haml_spec.rb10
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb2
-rw-r--r--spec/views/devise/confirmations/almost_there.html.haml_spec.rb37
-rw-r--r--spec/views/events/event/_common.html.haml_spec.rb17
-rw-r--r--spec/views/groups/observability.html.haml_spec.rb18
-rw-r--r--spec/views/groups/observability/observability.html.haml_spec.rb18
-rw-r--r--spec/views/layouts/header/_gitlab_version.html.haml_spec.rb17
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb18
-rw-r--r--spec/views/projects/artifacts/_artifact.html.haml_spec.rb85
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb8
-rw-r--r--spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb12
-rw-r--r--spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb13
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb4
-rw-r--r--spec/views/search/show.html.haml_spec.rb10
-rw-r--r--spec/views/shared/access_tokens/_table.html.haml_spec.rb151
-rw-r--r--spec/views/shared/deploy_tokens/_form.html.haml_spec.rb62
-rw-r--r--spec/views/shared/issuable/_sidebar.html.haml_spec.rb6
-rw-r--r--spec/workers/bulk_imports/entity_worker_spec.rb25
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb95
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb54
-rw-r--r--spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb23
-rw-r--r--spec/workers/cluster_configure_istio_worker_spec.rb41
-rw-r--r--spec/workers/cluster_update_app_worker_spec.rb112
-rw-r--r--spec/workers/cluster_wait_for_app_update_worker_spec.rb27
-rw-r--r--spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb32
-rw-r--r--spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb32
-rw-r--r--spec/workers/concerns/reenqueuer_spec.rb21
-rw-r--r--spec/workers/container_registry/cleanup_worker_spec.rb81
-rw-r--r--spec/workers/container_registry/delete_container_repository_worker_spec.rb146
-rw-r--r--spec/workers/database/batched_background_migration/execution_worker_spec.rb141
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb5
-rw-r--r--spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb46
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb31
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb3
-rw-r--r--spec/workers/gitlab_shell_worker_spec.rb50
-rw-r--r--spec/workers/incident_management/add_severity_system_note_worker_spec.rb25
-rw-r--r--spec/workers/loose_foreign_keys/cleanup_worker_spec.rb36
-rw-r--r--spec/workers/mail_scheduler/notification_service_worker_spec.rb39
-rw-r--r--spec/workers/merge_requests/delete_branch_worker_spec.rb65
-rw-r--r--spec/workers/merge_requests/delete_source_branch_worker_spec.rb120
-rw-r--r--spec/workers/namespaces/root_statistics_worker_spec.rb13
-rw-r--r--spec/workers/pages/invalidate_domain_cache_worker_spec.rb61
-rw-r--r--spec/workers/pages_worker_spec.rb24
-rw-r--r--spec/workers/projects/after_import_worker_spec.rb2
-rw-r--r--spec/workers/projects/post_creation_worker_spec.rb34
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb26
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb62
-rw-r--r--spec/workers/repository_import_worker_spec.rb32
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb53
-rw-r--r--spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb12
1649 files changed, 42319 insertions, 23369 deletions
diff --git a/spec/bin/audit_event_type_spec.rb b/spec/bin/audit_event_type_spec.rb
new file mode 100644
index 00000000000..d4b1ebf08de
--- /dev/null
+++ b/spec/bin/audit_event_type_spec.rb
@@ -0,0 +1,293 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+load File.expand_path('../../bin/audit-event-type', __dir__)
+
+RSpec.describe 'bin/audit-event-type' do
+ using RSpec::Parameterized::TableSyntax
+
+ describe AuditEventTypeCreator do
+ let(:argv) { %w[test_audit_event -d test -g govern::compliance -s -t -i https://url -m http://url] }
+ let(:options) { AuditEventTypeOptionParser.parse(argv) }
+ let(:creator) { described_class.new(options) }
+ let(:existing_audit_event_types) do
+ { 'existing_audit_event_type' => File.join('config', 'audit_events', 'types', 'existing_audit_event_type.yml') }
+ end
+
+ before do
+ allow(creator).to receive(:all_audit_event_type_names) { existing_audit_event_types }
+ allow(creator).to receive(:branch_name).and_return('feature-branch')
+ allow(creator).to receive(:editor).and_return(nil)
+
+ # ignore writes
+ allow(File).to receive(:write).and_return(true)
+
+ # ignore stdin
+ allow(Readline).to receive(:readline).and_raise('EOF')
+ end
+
+ subject(:create_audit_event_type) { creator.execute }
+
+ it 'properly creates an audit event type' do
+ expect(File).to receive(:write).with(
+ File.join('config', 'audit_events', 'types', 'test_audit_event.yml'),
+ anything)
+
+ expect do
+ create_audit_event_type
+ end.to output(/name: test_audit_event/).to_stdout
+ end
+
+ context 'when running on master' do
+ it 'requires feature branch' do
+ expect(creator).to receive(:branch_name).and_return('master')
+
+ expect { create_audit_event_type }.to raise_error(AuditEventTypeHelpers::Abort, /Create a branch first/)
+ end
+ end
+
+ context 'with invalid audit event type names' do
+ where(:argv, :ex) do
+ %w[.invalid.audit.type] | /Provide a name for the audit event type that is/
+ %w[existing_audit_event_type] | /already exists!/
+ end
+
+ with_them do
+ it do
+ expect { create_audit_event_type }.to raise_error(ex)
+ end
+ end
+ end
+ end
+
+ describe AuditEventTypeOptionParser do
+ describe '.parse' do
+ where(:param, :argv, :result) do
+ :name | %w[foo] | 'foo'
+ :amend | %w[foo --amend] | true
+ :force | %w[foo -f] | true
+ :force | %w[foo --force] | true
+ :description | %w[foo -d desc] | 'desc'
+ :description | %w[foo --description desc] | 'desc'
+ :group | %w[foo -g govern::compliance] | 'govern::compliance'
+ :group | %w[foo --group govern::compliance] | 'govern::compliance'
+ :milestone | %w[foo -M 15.6] | '15.6'
+ :milestone | %w[foo --milestone 15.6] | '15.6'
+ :saved_to_database | %w[foo -s] | true
+ :saved_to_database | %w[foo --saved-to-database] | true
+ :saved_to_database | %w[foo --no-saved-to-database] | false
+ :streamed | %w[foo -t] | true
+ :streamed | %w[foo --streamed] | true
+ :streamed | %w[foo --no-streamed] | false
+ :dry_run | %w[foo -n] | true
+ :dry_run | %w[foo --dry-run] | true
+ :ee | %w[foo -e] | true
+ :ee | %w[foo --ee] | true
+ :jh | %w[foo -j] | true
+ :jh | %w[foo --jh] | true
+ :introduced_by_mr | %w[foo -m https://url] | 'https://url'
+ :introduced_by_mr | %w[foo --introduced-by-mr https://url] | 'https://url'
+ :introduced_by_issue | %w[foo -i https://url] | 'https://url'
+ :introduced_by_issue | %w[foo --introduced-by-issue https://url] | 'https://url'
+ end
+
+ with_them do
+ it do
+ options = described_class.parse(Array(argv))
+
+ expect(options.public_send(param)).to eq(result)
+ end
+ end
+
+ it 'raises an error when name of the audit event type is missing' do
+ expect do
+ expect do
+ described_class.parse(%w[--amend])
+ end.to output(/Name for the type of audit event is required/).to_stdout
+ end.to raise_error(AuditEventTypeHelpers::Abort)
+ end
+
+ it 'parses -h' do
+ expect do
+ expect { described_class.parse(%w[foo -h]) }.to output(%r{Usage: bin/audit-event-type}).to_stdout
+ end.to raise_error(AuditEventTypeHelpers::Done)
+ end
+ end
+
+ describe '.read_description' do
+ let(:description) { 'This is a test description for an audit event type.' }
+
+ it 'reads description from stdin' do
+ expect(Readline).to receive(:readline).and_return(description)
+ expect do
+ expect(described_class.read_description).to eq('This is a test description for an audit event type.')
+ end.to output(/Specify a human-readable description of how this event is triggered:/).to_stdout
+ end
+
+ context 'when description is empty' do
+ let(:description) { '' }
+
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(description)
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.read_description }.to raise_error(/EOF/)
+ end.to output(/Specify a human-readable description of how this event is triggered:/)
+ .to_stdout.and output(/description is a required field/).to_stderr
+ end
+ end
+ end
+
+ describe '.read_group' do
+ let(:group) { 'govern::compliance' }
+
+ it 'reads group from stdin' do
+ expect(Readline).to receive(:readline).and_return(group)
+ expect do
+ expect(described_class.read_group).to eq('govern::compliance')
+ end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/).to_stdout
+ end
+
+ context 'when group is empty' do
+ let(:group) { '' }
+
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(group)
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.read_group }.to raise_error(/EOF/)
+ end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/)
+ .to_stdout.and output(/group is a required field/).to_stderr
+ end
+ end
+ end
+
+ describe '.read_saved_to_database' do
+ let(:saved_to_database) { 'true' }
+
+ it 'reads saved_to_database from stdin' do
+ expect(Readline).to receive(:readline).and_return(saved_to_database)
+ expect do
+ expect(described_class.read_saved_to_database).to eq(true)
+ end.to output(/Specify whether to persist events to database and JSON logs \[yes, no\]:/).to_stdout
+ end
+
+ context 'when saved_to_database is invalid' do
+ let(:saved_to_database) { 'non boolean value' }
+
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(saved_to_database)
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.read_saved_to_database }.to raise_error(/EOF/)
+ end.to output(/Specify whether to persist events to database and JSON logs \[yes, no\]:/)
+ .to_stdout.and output(/saved_to_database is a required boolean field/).to_stderr
+ end
+ end
+ end
+
+ describe '.read_streamed' do
+ let(:streamed) { 'true' }
+
+ it 'reads streamed from stdin' do
+ expect(Readline).to receive(:readline).and_return(streamed)
+ expect do
+ expect(described_class.read_streamed).to eq(true)
+ end.to output(/Specify if events should be streamed to external services \(if configured\) \[yes, no\]:/)
+ .to_stdout
+ end
+
+ context 'when streamed is invalid' do
+ let(:streamed) { 'non boolean value' }
+
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(streamed)
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.read_streamed }.to raise_error(/EOF/)
+ end.to output(/Specify if events should be streamed to external services \(if configured\) \[yes, no\]:/)
+ .to_stdout.and output(/streamed is a required boolean field/).to_stderr
+ end
+ end
+ end
+
+ describe '.read_introduced_by_mr' do
+ let(:url) { 'https://merge-request' }
+
+ it 'reads introduced_by_mr from stdin' do
+ expect(Readline).to receive(:readline).and_return(url)
+ expect do
+ expect(described_class.read_introduced_by_mr).to eq('https://merge-request')
+ end.to output(/URL to GitLab merge request that added this type of audit event:/).to_stdout
+ end
+
+ context 'when URL is empty' do
+ let(:url) { '' }
+
+ it 'does not raise an error' do
+ expect(Readline).to receive(:readline).and_return(url)
+
+ expect do
+ expect(described_class.read_introduced_by_mr).to be_nil
+ end.to output(/URL to GitLab merge request that added this type of audit event:/).to_stdout
+ end
+ end
+
+ context 'when URL is invalid' do
+ let(:url) { 'invalid' }
+
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(url)
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.read_introduced_by_mr }.to raise_error(/EOF/)
+ end.to output(/URL to GitLab merge request that added this type of audit event:/)
+ .to_stdout.and output(/URL needs to start with https/).to_stderr
+ end
+ end
+ end
+
+ describe '.read_introduced_by_issue' do
+ let(:url) { 'https://issue' }
+
+ it 'reads type from stdin' do
+ expect(Readline).to receive(:readline).and_return(url)
+ expect do
+ expect(described_class.read_introduced_by_issue).to eq('https://issue')
+ end.to output(/URL to GitLab issue that added this type of audit event:/).to_stdout
+ end
+
+ context 'when URL is invalid' do
+ let(:type) { 'invalid' }
+
+ it 'shows error message and retries' do
+ expect(Readline).to receive(:readline).and_return(type)
+ expect(Readline).to receive(:readline).and_raise('EOF')
+
+ expect do
+ expect { described_class.read_introduced_by_issue }.to raise_error(/EOF/)
+ end.to output(/URL to GitLab issue that added this type of audit event:/)
+ .to_stdout.and output(/URL needs to start with https/).to_stderr
+ end
+ end
+ end
+
+ describe '.read_milestone' do
+ before do
+ allow(File).to receive(:read).with('VERSION').and_return('15.6.0-pre')
+ allow(File).to receive(:read).and_call_original
+ end
+
+ it 'returns the correct milestone from the VERSION file' do
+ expect(described_class.read_milestone).to eq('15.6')
+ end
+ end
+ end
+end
diff --git a/spec/components/pajamas/spinner_component_spec.rb b/spec/components/pajamas/spinner_component_spec.rb
index 9aac9a0085c..f03d8c9561b 100644
--- a/spec/components/pajamas/spinner_component_spec.rb
+++ b/spec/components/pajamas/spinner_component_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Pajamas::SpinnerComponent, type: :component do
describe 'inline' do
context 'by default' do
it 'renders a div' do
- expect(page).to have_css 'div.gl-spinner'
+ expect(page).to have_css 'div.gl-spinner-container'
end
end
@@ -43,7 +43,7 @@ RSpec.describe Pajamas::SpinnerComponent, type: :component do
let(:options) { { inline: true } }
it 'renders a span' do
- expect(page).to have_css 'span.gl-spinner'
+ expect(page).to have_css 'span.gl-spinner-container'
end
end
end
diff --git a/spec/components/previews/pajamas/alert_component_preview.rb b/spec/components/previews/pajamas/alert_component_preview.rb
index e1889032c8b..4768ef47975 100644
--- a/spec/components/previews/pajamas/alert_component_preview.rb
+++ b/spec/components/previews/pajamas/alert_component_preview.rb
@@ -4,7 +4,7 @@ module Pajamas
# @param title text
# @param body text
# @param dismissible toggle
- # @param variant select [info, warning, success, danger, tip]
+ # @param variant select {{ Pajamas::AlertComponent::VARIANT_ICONS.keys }}
def default(title: "Alert title (optional)", body: "Alert message goes here.", dismissible: true, variant: :info)
render(Pajamas::AlertComponent.new(
title: title,
diff --git a/spec/components/previews/pajamas/avatar_component_preview.rb b/spec/components/previews/pajamas/avatar_component_preview.rb
index e5cdde1ccef..147d89169b0 100644
--- a/spec/components/previews/pajamas/avatar_component_preview.rb
+++ b/spec/components/previews/pajamas/avatar_component_preview.rb
@@ -9,17 +9,17 @@ module Pajamas
end
# We show user avatars in a circle.
- # @param size select [16, 24, 32, 48, 64, 96]
+ # @param size select {{ Pajamas::AvatarComponent::SIZE_OPTIONS }}
def user(size: 64)
render(Pajamas::AvatarComponent.new(User.first, size: size))
end
- # @param size select [16, 24, 32, 48, 64, 96]
+ # @param size select {{ Pajamas::AvatarComponent::SIZE_OPTIONS }}
def project(size: 64)
render(Pajamas::AvatarComponent.new(Project.first, size: size))
end
- # @param size select [16, 24, 32, 48, 64, 96]
+ # @param size select {{ Pajamas::AvatarComponent::SIZE_OPTIONS }}
def group(size: 64)
render(Pajamas::AvatarComponent.new(Group.first, size: size))
end
diff --git a/spec/components/previews/pajamas/badge_component_preview.rb b/spec/components/previews/pajamas/badge_component_preview.rb
index e740a4a38aa..e833c4e458d 100644
--- a/spec/components/previews/pajamas/badge_component_preview.rb
+++ b/spec/components/previews/pajamas/badge_component_preview.rb
@@ -10,9 +10,9 @@ module Pajamas
# @param icon select [~, star-o, issue-closed, tanuki]
# @param icon_only toggle
# @param href url
- # @param size select [sm, md, lg]
+ # @param size select {{ Pajamas::BadgeComponent::SIZE_OPTIONS }}
# @param text text
- # @param variant select [muted, neutral, info, success, warning, danger]
+ # @param variant select {{ Pajamas::BadgeComponent::VARIANT_OPTIONS }}
def default(icon: :tanuki, icon_only: false, href: nil, size: :md, text: "Tanuki", variant: :muted)
render Pajamas::BadgeComponent.new(
text,
diff --git a/spec/components/previews/pajamas/banner_component_preview.rb b/spec/components/previews/pajamas/banner_component_preview.rb
index 861e3ff95dc..19f4f5243c0 100644
--- a/spec/components/previews/pajamas/banner_component_preview.rb
+++ b/spec/components/previews/pajamas/banner_component_preview.rb
@@ -9,7 +9,7 @@ module Pajamas
# @param button_link text
# @param content textarea
# @param embedded toggle
- # @param variant select [introduction, promotion]
+ # @param variant select {{ Pajamas::BannerComponent::VARIANT_OPTIONS }}
def default(
button_text: "Learn more",
button_link: "https://about.gitlab.com/",
diff --git a/spec/components/previews/pajamas/button_component_preview.rb b/spec/components/previews/pajamas/button_component_preview.rb
index 1f61d9cf2bc..c07d898d9cd 100644
--- a/spec/components/previews/pajamas/button_component_preview.rb
+++ b/spec/components/previews/pajamas/button_component_preview.rb
@@ -5,10 +5,10 @@ module Pajamas
# ----
# See its design reference [here](https://design.gitlab.com/components/banner).
#
- # @param category select [primary, secondary, tertiary]
- # @param variant select [default, confirm, danger, dashed, link, reset]
- # @param size select [small, medium]
- # @param type select [button, reset, submit]
+ # @param category select {{ Pajamas::ButtonComponent::CATEGORY_OPTIONS }}
+ # @param variant select {{ Pajamas::ButtonComponent::VARIANT_OPTIONS }}
+ # @param size select {{ Pajamas::ButtonComponent::SIZE_OPTIONS }}
+ # @param type select {{ Pajamas::ButtonComponent::TYPE_OPTIONS }}
# @param disabled toggle
# @param loading toggle
# @param block toggle
diff --git a/spec/components/previews/pajamas/progress_component_preview.rb b/spec/components/previews/pajamas/progress_component_preview.rb
index 4de07872a80..1562ffddf7e 100644
--- a/spec/components/previews/pajamas/progress_component_preview.rb
+++ b/spec/components/previews/pajamas/progress_component_preview.rb
@@ -7,8 +7,8 @@ module Pajamas
#
# See its design reference [here](https://design.gitlab.com/components/progress-bar).
#
- # @param value number
- # @param variant select [primary, success]
+ # @param value range { min: 0, max: 100, step: 1 }
+ # @param variant select {{ Pajamas::ProgressComponent::VARIANT_OPTIONS }}
def default(value: 50, variant: :primary)
render Pajamas::ProgressComponent.new(value: value, variant: variant)
end
diff --git a/spec/components/previews/pajamas/spinner_component_preview.rb b/spec/components/previews/pajamas/spinner_component_preview.rb
index 149bfddcfc2..34cc386763f 100644
--- a/spec/components/previews/pajamas/spinner_component_preview.rb
+++ b/spec/components/previews/pajamas/spinner_component_preview.rb
@@ -7,16 +7,30 @@ module Pajamas
#
# @param inline toggle
# @param label text
- # @param size select [[small, sm], [medium, md], [large, lg], [extra large, xl]]
+ # @param size select {{ Pajamas::SpinnerComponent::SIZE_OPTIONS }}
def default(inline: false, label: "Loading", size: :md)
- render(Pajamas::SpinnerComponent.new(inline: inline, label: label, size: size))
+ render Pajamas::SpinnerComponent.new(
+ inline: inline,
+ label: label,
+ size: size
+ )
end
- # Use a light spinner on dark backgrounds
+ # Use a light spinner on dark backgrounds.
#
- # @display bg_color "#222"
+ # @display bg_dark true
def light
render(Pajamas::SpinnerComponent.new(color: :light))
end
+
+ # Any extra HTML attributes like `class`, `data` or `id` get automatically applied to the spinner container element.
+ #
+ def extra_attributes
+ render Pajamas::SpinnerComponent.new(
+ class: "js-do-something",
+ data: { foo: "bar" },
+ id: "my-special-spinner"
+ )
+ end
end
end
diff --git a/spec/config/inject_enterprise_edition_module_spec.rb b/spec/config/inject_enterprise_edition_module_spec.rb
index 6ef74a2b616..96fc26fc80a 100644
--- a/spec/config/inject_enterprise_edition_module_spec.rb
+++ b/spec/config/inject_enterprise_edition_module_spec.rb
@@ -126,4 +126,22 @@ RSpec.describe InjectEnterpriseEditionModule do
describe '#include_mod' do
it_behaves_like 'expand the assumed extension with', :include
end
+
+ describe '#gitlab_extensions' do
+ context 'when there are no extension modules' do
+ it 'returns the class itself' do
+ expect(fish_class.gitlab_extensions).to contain_exactly(fish_class)
+ end
+ end
+
+ context 'when there are extension modules' do
+ it 'returns the class itself and any extensions' do
+ stub_const(extension_name, extension_namespace)
+ extension_namespace.const_set(fish_name, fish_extension)
+ fish_class.prepend_mod
+
+ expect(fish_class.gitlab_extensions).to contain_exactly(fish_class, fish_extension)
+ end
+ end
+ end
end
diff --git a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb b/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
deleted file mode 100644
index 1984aff01db..00000000000
--- a/spec/config/metrics/aggregates/aggregated_metrics_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'aggregated metrics' do
- RSpec::Matchers.define :be_known_event do
- match do |event|
- Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event)
- end
-
- failure_message do |event|
- "Event with name: `#{event}` can not be found within `#{Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH}`"
- end
- end
-
- RSpec::Matchers.define :has_known_source do
- match do |aggregate|
- Gitlab::Usage::Metrics::Aggregates::SOURCES.include?(aggregate[:source])
- end
-
- failure_message do |aggregate|
- "Aggregate with name: `#{aggregate[:name]}` uses not allowed source `#{aggregate[:source]}`"
- end
- end
-
- RSpec::Matchers.define :have_known_time_frame do
- allowed_time_frames = [
- 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|
- (aggregate[:time_frame] - allowed_time_frames).empty?
- end
-
- failure_message do |aggregate|
- "Aggregate with name: `#{aggregate[:name]}` uses not allowed time_frame`#{aggregate[:time_frame] - allowed_time_frames}`"
- end
- end
-
- let_it_be(:known_events) do
- Gitlab::UsageDataCounters::HLLRedisCounter.known_events
- end
-
- Gitlab::Usage::Metrics::Aggregates::Aggregate.new(Time.current).send(:aggregated_metrics).tap do |aggregated_metrics|
- it 'all events has unique name' do
- event_names = aggregated_metrics&.map { |event| event[:name] }
-
- expect(event_names).to eq(event_names&.uniq)
- end
-
- it 'all aggregated metrics has known source' do
- expect(aggregated_metrics).to all has_known_source
- end
-
- it 'all aggregated metrics has known time frame' do
- expect(aggregated_metrics).to all have_known_time_frame
- end
-
- aggregated_metrics&.select { |agg| agg[:source] == Gitlab::Usage::Metrics::Aggregates::REDIS_SOURCE }&.each do |aggregate|
- context "for #{aggregate[:name]} aggregate of #{aggregate[:events].join(' ')}" 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::Usage::TimeFrame::ALL_TIME_TIME_FRAME_NAME)
- end
-
- it "only refers to known events", :skip do
- expect(aggregate[:events]).to all be_known_event
- end
-
- it "has expected structure" do
- expect(aggregate.keys).to include(*%w[name operator events])
- end
-
- it "uses allowed aggregation operators" do
- expect(Gitlab::Usage::Metrics::Aggregates::ALLOWED_METRICS_AGGREGATIONS).to include aggregate[:operator]
- end
-
- it "uses events from the same Redis slot" do
- event_slots = events_records.map { |event| event[:redis_slot] }.uniq
-
- expect(event_slots).to contain_exactly(be_present)
- end
-
- it "uses events with the same aggregation period" do
- event_slots = events_records.map { |event| event[:aggregation] }.uniq
-
- expect(event_slots).to contain_exactly(be_present)
- end
- end
- end
- end
-end
diff --git a/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json b/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json
index b725ae400a7..2ebfd1bfdf2 100644
--- a/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json
+++ b/spec/contracts/contracts/project/pipeline/index/pipelines#index-get_list_project_pipelines.json
@@ -198,6 +198,9 @@
"match": "regex",
"regex": "^(push|web|trigger|schedule|api|external|pipeline|chat|webide|merge_request_event|external_pull_request_event|parent_pipeline|ondemand_dast_scan|ondemand_dast_validation)$"
},
+ "$.body.pipelines[*].name": {
+ "match": "type"
+ },
"$.body.pipelines[*].created_at": {
"match": "regex",
"regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
@@ -335,6 +338,9 @@
"$.body.pipelines[*].details.name": {
"match": "type"
},
+ "$.body.pipelines[*].details.event_type_name": {
+ "match": "type"
+ },
"$.body.pipelines[*].details.manual_actions": {
"min": 1
},
diff --git a/spec/contracts/provider/helpers/publish_contract_helper.rb b/spec/contracts/provider/helpers/publish_contract_helper.rb
new file mode 100644
index 00000000000..102a73d87ee
--- /dev/null
+++ b/spec/contracts/provider/helpers/publish_contract_helper.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Provider
+ module PublishContractHelper
+ PROVIDER_VERSION = ENV['GIT_COMMIT'] || `git rev-parse --verify HEAD`.strip
+ PROVIDER_BRANCH = ENV['GIT_BRANCH'] || `git name-rev --name-only HEAD`.strip
+ PUBLISH_FLAG = true
+
+ def self.publish_contract_setup
+ @setup ||= -> {
+ app_version PROVIDER_VERSION
+ app_version_branch PROVIDER_BRANCH
+ publish_verification_results PUBLISH_FLAG
+ }
+ end
+ end
+end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb
index f94ce47b1f3..71f302f2c44 100644
--- a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_batch_helper.rb
@@ -11,6 +11,8 @@ module Provider
honours_pact_with 'MergeRequest#show' do
pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json'
end
+
+ Provider::PublishContractHelper.publish_contract_setup.call
end
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb
index 61567214b7a..60a3abea5ae 100644
--- a/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/merge_request/show/diffs_metadata_helper.rb
@@ -11,6 +11,10 @@ module Provider
honours_pact_with 'MergeRequest#show' do
pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_metadata_endpoint.json'
end
+
+ app_version Provider::PublishContractHelper::PROVIDER_VERSION
+ app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
+ publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
end
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb b/spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb
index fa76ce8889a..b9308af0a1a 100644
--- a/spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/merge_request/show/discussions_helper.rb
@@ -11,6 +11,10 @@ module Provider
honours_pact_with 'MergeRequest#show' do
pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json'
end
+
+ app_version Provider::PublishContractHelper::PROVIDER_VERSION
+ app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
+ publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
end
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb
index 247a7c4ca8e..2af960bc9fd 100644
--- a/spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/pipeline/index/create_a_new_pipeline_helper.rb
@@ -11,6 +11,10 @@ module Provider
honours_pact_with 'Pipelines#new' do
pact_uri '../contracts/project/pipeline/new/pipelines#new-post_create_a_new_pipeline.json'
end
+
+ app_version Provider::PublishContractHelper::PROVIDER_VERSION
+ app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
+ publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
end
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb
index 80cbbe3b4dd..37cddd1b80e 100644
--- a/spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/pipeline/index/get_list_project_pipelines_helper.rb
@@ -11,6 +11,10 @@ module Provider
honours_pact_with 'Pipelines#index' do
pact_uri '../contracts/project/project/pipeline/index/pipelines#index-get_list_project_pipelines.json'
end
+
+ app_version Provider::PublishContractHelper::PROVIDER_VERSION
+ app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
+ publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
end
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb
index 2d29fabfeca..0455281fcd7 100644
--- a/spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/pipeline/show/delete_pipeline_helper.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require_relative '../../../../spec_helper'
+require_relative '../../../../helpers/publish_contract_helper'
require_relative '../../../../states/project/pipeline/show_state'
module Provider
@@ -11,6 +12,10 @@ module Provider
honours_pact_with 'Pipelines#show' do
pact_uri '../contracts/project/pipeline/show/pipelines#show-delete_pipeline.json'
end
+
+ app_version Provider::PublishContractHelper::PROVIDER_VERSION
+ app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
+ publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
end
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb
index bc8c04cc455..bce1c4ab3f4 100644
--- a/spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/pipeline/show/get_pipeline_header_data_helper.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require_relative '../../../../spec_helper'
+require_relative '../../../../helpers/publish_contract_helper'
require_relative '../../../../states/project/pipeline/show_state'
module Provider
@@ -10,7 +11,12 @@ module Provider
honours_pact_with 'Pipelines#show' do
pact_uri '../contracts/project/pipeline/show/pipelines#show-get_project_pipeline_header_data.json'
+ # pact_uri 'http://localhost:9292/pacts/provider/GET%20pipeline%20header%20data/consumer/Pipelines%23show/latest'
end
+
+ app_version Provider::PublishContractHelper::PROVIDER_VERSION
+ app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
+ publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
end
end
end
diff --git a/spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb b/spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb
index a83aa9524dc..d95a09abd8d 100644
--- a/spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb
+++ b/spec/contracts/provider/pact_helpers/project/pipeline_schedule/update_pipeline_schedule_helper.rb
@@ -11,6 +11,10 @@ module Provider
honours_pact_with 'PipelineSchedule#edit' do
pact_uri '../contracts/project/pipeline_schedule/edit/pipelineschedules#edit-put_edit_a_pipeline_schedule.json'
end
+
+ app_version Provider::PublishContractHelper::PROVIDER_VERSION
+ app_version_branch Provider::PublishContractHelper::PROVIDER_BRANCH
+ publish_verification_results Provider::PublishContractHelper::PUBLISH_FLAG
end
end
end
diff --git a/spec/contracts/publish-contracts.sh b/spec/contracts/publish-contracts.sh
new file mode 100644
index 00000000000..f20cc43e258
--- /dev/null
+++ b/spec/contracts/publish-contracts.sh
@@ -0,0 +1,23 @@
+LATEST_SHA=$(git rev-parse HEAD)
+GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+BROKER_BASE_URL="http://localhost:9292"
+
+CONTRACTS=$(find ./contracts -name "*.json")
+ERROR=0
+
+trap 'catch' ERR
+
+function catch() {
+ printf "\e[31mAn error occured while trying to publish the pact.\033[0m\n"
+ ERROR=1
+}
+
+for contract in $CONTRACTS
+do
+ printf "\e[32mPublishing ${contract}...\033[0m\n"
+ pact-broker publish $contract --consumer-app-version $LATEST_SHA --branch $GIT_BRANCH --broker-base-url $BROKER_BASE_URL --output json
+done
+
+if [ ${ERROR} = 1 ]; then
+ exit 1;
+fi \ No newline at end of file
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 37cb0a1f289..6085f0e1239 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -43,5 +43,13 @@ RSpec.describe Admin::GroupsController do
post :create, params: { group: { path: 'test', name: 'test', admin_note_attributes: { note: 'test' } } }
end.to change { Namespace::AdminNote.count }.by(1)
end
+
+ it 'delegates to Groups::CreateService service instance' do
+ expect_next_instance_of(::Groups::CreateService) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
+ post :create, params: { group: { path: 'test', name: 'test' } }
+ end
end
end
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
index 14f4a2f40e7..82e4b873bf6 100644
--- a/spec/controllers/admin/hooks_controller_spec.rb
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Admin::HooksController do
- let(:admin) { create(:admin) }
+ let_it_be(:admin) { create(:admin) }
before do
sign_in(admin)
@@ -33,7 +33,23 @@ RSpec.describe Admin::HooksController do
end
describe 'POST #update' do
- let!(:hook) { create(:system_hook) }
+ let_it_be_with_reload(:hook) { create(:system_hook) }
+
+ context 'with an existing token' do
+ hook_params = {
+ token: WebHook::SECRET_MASK,
+ url: "http://example.com"
+ }
+
+ it 'does not change a token' do
+ expect do
+ post :update, params: { id: hook.id, hook: hook_params }
+ end.not_to change { hook.reload.token }
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:alert]).to be_blank
+ end
+ end
it 'sets all parameters' do
hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' })
@@ -61,8 +77,8 @@ RSpec.describe Admin::HooksController do
end
describe 'DELETE #destroy' do
- let!(:hook) { create(:system_hook) }
- let!(:log) { create(:web_hook_log, web_hook: hook) }
+ let_it_be(:hook) { create(:system_hook) }
+ let_it_be(:log) { create(:web_hook_log, web_hook: hook) }
let(:params) { { id: hook } }
it_behaves_like 'Web hook destroyer'
diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb
index 0e456858b49..e75f27589d7 100644
--- a/spec/controllers/admin/integrations_controller_spec.rb
+++ b/spec/controllers/admin/integrations_controller_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Admin::IntegrationsController do
describe '#edit' do
Integration.available_integration_names.each do |integration_name|
- context "#{integration_name}" do
+ context integration_name.to_s do
it 'successfully displays the template' do
get :edit, params: { id: integration_name }
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 48221f496fb..51f7ecdece6 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -27,34 +27,17 @@ RSpec.describe Admin::SpamLogsController do
expect(response).to have_gitlab_http_status(:ok)
end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal', :sidekiq_inline do
- expect do
- delete :destroy, params: { id: first_spam.id, remove_user: true }
- end.not_to change { SpamLog.count }
-
- expect(response).to have_gitlab_http_status(:found)
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- ).to be_exists
- expect(flash[:notice]).to eq("User #{user.username} was successfully removed.")
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'removes user and their spam logs when removing the user', :sidekiq_inline do
+ it 'initiates user removal', :sidekiq_inline do
+ expect do
delete :destroy, params: { id: first_spam.id, remove_user: true }
+ end.not_to change { SpamLog.count }
- expect(flash[:notice]).to eq "User #{user.username} was successfully removed."
- expect(response).to have_gitlab_http_status(:found)
- expect(SpamLog.count).to eq(0)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ expect(response).to have_gitlab_http_status(:found)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ ).to be_exists
+ expect(flash[:notice]).to eq("User #{user.username} was successfully removed.")
end
end
diff --git a/spec/controllers/admin/topics_controller_spec.rb b/spec/controllers/admin/topics_controller_spec.rb
index 111fdcc3be6..e640f8bb7ec 100644
--- a/spec/controllers/admin/topics_controller_spec.rb
+++ b/spec/controllers/admin/topics_controller_spec.rb
@@ -176,7 +176,7 @@ RSpec.describe Admin::TopicsController do
describe 'POST #merge' do
let_it_be(:source_topic) { create(:topic, name: 'source_topic') }
- let_it_be(:project) { create(:project, topic_list: source_topic.name ) }
+ let_it_be(:project) { create(:project, topic_list: source_topic.name) }
it 'merges source topic into target topic' do
post :merge, params: { source_topic_id: source_topic.id, target_topic_id: topic.id }
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 682399f4dd9..eecb803fb1a 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -63,130 +63,179 @@ RSpec.describe Admin::UsersController do
expect(response).to be_redirect
expect(response.location).to end_with(user.username)
end
- end
- describe 'DELETE #destroy', :sidekiq_might_not_need_inline do
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:issue) { create(:issue, author: user) }
+ describe 'impersonation_error_text' do
+ context 'when user can be impersonated' do
+ it 'sets impersonation_error_text to nil' do
+ get :show, params: { id: user.username.downcase }
- before do
- project.add_developer(user)
- end
+ expect(assigns(:impersonation_error_text)).to eq(nil)
+ end
+ end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal' do
- delete :destroy, params: { id: user.username }, format: :json
+ context 'when impersonation is already in progress' do
+ let(:admin2) { create(:admin) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin,
- hard_delete: false)
- ).to be_exists
+ before do
+ post :impersonate, params: { id: admin2.username }
+ end
+
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You are already impersonating another user"))
+ end
end
- it 'initiates user removal and passes hard delete option' do
- delete :destroy, params: { id: user.username, hard_delete: true }, format: :json
+ context 'when user is blocked' do
+ before do
+ user.block
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin,
- hard_delete: true)
- ).to be_exists
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate a blocked user"))
+ end
end
- context 'prerequisites for account deletion' do
- context 'solo-owned groups' do
- let(:group) { create(:group) }
+ context "when the user's password is expired" do
+ before do
+ user.update!(password_expires_at: 1.day.ago)
+ end
- context 'if the user is the sole owner of at least one group' do
- before do
- create(:group_member, :owner, group: group, user: user)
- end
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate a user with an expired password"))
+ end
+ end
- context 'soft-delete' do
- it 'fails' do
- delete :destroy, params: { id: user.username }
+ context "when the user is internal" do
+ before do
+ user.update!(user_type: :migration_bot)
+ end
- message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
- expect(flash[:alert]).to eq(message)
- expect(response).to have_gitlab_http_status(:see_other)
- expect(response).to redirect_to admin_user_path(user)
- expect(Users::GhostUserMigration).not_to exist
- end
- end
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate an internal user"))
+ end
+ end
- context 'hard-delete' do
- it 'succeeds' do
- delete :destroy, params: { id: user.username, hard_delete: true }
-
- expect(response).to redirect_to(admin_users_path)
- expect(flash[:notice]).to eq(_('The user is being deleted.'))
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin,
- hard_delete: true)
- ).to be_exists
- end
- end
- end
+ context "when the user is a project bot" do
+ before do
+ user.update!(user_type: :project_bot)
+ end
+
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate a user who cannot log in"))
end
end
end
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ describe 'can_impersonate' do
+ context 'when user can be impersonated' do
+ it 'sets can_impersonate to true' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:can_impersonate)).to eq(true)
+ end
end
- it 'deletes user and ghosts their contributions' do
- delete :destroy, params: { id: user.username }, format: :json
+ context 'when impersonation is already in progress' do
+ let(:admin2) { create(:admin) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(User.exists?(user.id)).to be_falsy
- expect(issue.reload.author).to be_ghost
+ before do
+ post :impersonate, params: { id: admin2.username }
+ end
+
+ it 'sets can_impersonate to false' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:can_impersonate)).to eq(false)
+ end
end
- it 'deletes the user and their contributions when hard delete is specified' do
- delete :destroy, params: { id: user.username, hard_delete: true }, format: :json
+ context 'when user cannot log in' do
+ before do
+ user.update!(user_type: :project_bot)
+ end
+
+ it 'sets can_impersonate to false' do
+ get :show, params: { id: user.username.downcase }
- expect(response).to have_gitlab_http_status(:ok)
- expect(User.exists?(user.id)).to be_falsy
- expect(Issue.exists?(issue.id)).to be_falsy
+ expect(assigns(:can_impersonate)).to eq(false)
+ end
end
+ end
+ end
- context 'prerequisites for account deletion' do
- context 'solo-owned groups' do
- let(:group) { create(:group) }
+ describe 'DELETE #destroy', :sidekiq_might_not_need_inline do
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:issue) { create(:issue, author: user) }
- context 'if the user is the sole owner of at least one group' do
- before do
- create(:group_member, :owner, group: group, user: user)
- end
+ before do
+ project.add_developer(user)
+ end
- context 'soft-delete' do
- it 'fails' do
- delete :destroy, params: { id: user.username }
+ it 'initiates user removal' do
+ delete :destroy, params: { id: user.username }, format: :json
- message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: false)
+ ).to be_exists
+ end
- expect(flash[:alert]).to eq(message)
- expect(response).to have_gitlab_http_status(:see_other)
- expect(response).to redirect_to admin_user_path(user)
- expect(User.exists?(user.id)).to be_truthy
- end
- end
+ it 'initiates user removal and passes hard delete option' do
+ delete :destroy, params: { id: user.username, hard_delete: true }, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: true)
+ ).to be_exists
+ end
- context 'hard-delete' do
- it 'succeeds' do
- delete :destroy, params: { id: user.username, hard_delete: true }
+ context 'prerequisites for account deletion' do
+ context 'solo-owned groups' do
+ let(:group) { create(:group) }
+
+ context 'if the user is the sole owner of at least one group' do
+ before do
+ create(:group_member, :owner, group: group, user: user)
+ end
+
+ context 'soft-delete' do
+ it 'fails' do
+ delete :destroy, params: { id: user.username }
+
+ message = s_('AdminUsers|You must transfer ownership or delete the groups owned by this user before you can delete their account')
+
+ expect(flash[:alert]).to eq(message)
+ expect(response).to have_gitlab_http_status(:see_other)
+ expect(response).to redirect_to admin_user_path(user)
+ expect(Users::GhostUserMigration).not_to exist
+ end
+ end
- expect(response).to redirect_to(admin_users_path)
- expect(flash[:notice]).to eq(_('The user is being deleted.'))
- expect(User.exists?(user.id)).to be_falsy
- end
+ context 'hard-delete' do
+ it 'succeeds' do
+ delete :destroy, params: { id: user.username, hard_delete: true }
+
+ expect(response).to redirect_to(admin_users_path)
+ expect(flash[:notice]).to eq(_('The user is being deleted.'))
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: true)
+ ).to be_exists
end
end
end
@@ -200,27 +249,13 @@ RSpec.describe Admin::UsersController do
context 'when rejecting a pending user' do
let(:user) { create(:user, :blocked_pending_approval) }
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal', :sidekiq_inline do
- subject
-
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'hard deletes the user', :sidekiq_inline do
- subject
+ it 'initiates user removal', :sidekiq_inline do
+ subject
- expect(User.exists?(user.id)).to be_falsy
- end
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ ).to be_exists
end
it 'displays the rejection message' do
@@ -909,6 +944,60 @@ RSpec.describe Admin::UsersController do
expect(session[:github_access_token]).to be_nil
end
+
+ context "when the user's password is expired" do
+ before do
+ user.update!(password_expires_at: 1.day.ago)
+ end
+
+ it "shows a notice" do
+ post :impersonate, params: { id: user.username }
+
+ expect(flash[:alert]).to eq(_('You cannot impersonate a user with an expired password'))
+ end
+
+ it "doesn't sign us in as the user" do
+ post :impersonate, params: { id: user.username }
+
+ expect(warden.user).to eq(admin)
+ end
+ end
+
+ context "when the user is internal" do
+ before do
+ user.update!(user_type: :migration_bot)
+ end
+
+ it "shows a notice" do
+ post :impersonate, params: { id: user.username }
+
+ expect(flash[:alert]).to eq(_("You cannot impersonate an internal user"))
+ end
+
+ it "doesn't sign us in as the user" do
+ post :impersonate, params: { id: user.username }
+
+ expect(warden.user).to eq(admin)
+ end
+ end
+
+ context "when the user is a project bot" do
+ before do
+ user.update!(user_type: :project_bot)
+ end
+
+ it "shows a notice" do
+ post :impersonate, params: { id: user.username }
+
+ expect(flash[:alert]).to eq(_("You cannot impersonate a user who cannot log in"))
+ end
+
+ it "doesn't sign us in as the user" do
+ post :impersonate, params: { id: user.username }
+
+ expect(warden.user).to eq(admin)
+ end
+ end
end
context "when impersonation is disabled" do
diff --git a/spec/controllers/concerns/issuable_actions_spec.rb b/spec/controllers/concerns/issuable_actions_spec.rb
index c3fef591b91..37d9dc080e1 100644
--- a/spec/controllers/concerns/issuable_actions_spec.rb
+++ b/spec/controllers/concerns/issuable_actions_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe IssuableActions do
let(:project) { double('project') }
let(:user) { double('user') }
let(:issuable) { double('issuable') }
- let(:finder_params_for_issuable) { {} }
- let(:notes_result) { double('notes_result') }
+ let(:finder_params_for_issuable) { { project: project, target: issuable } }
+ let(:notes_result) { [] }
let(:discussion_serializer) { double('discussion_serializer') }
let(:controller) do
@@ -55,13 +55,20 @@ RSpec.describe IssuableActions do
end
it 'instantiates and calls NotesFinder as expected' do
+ expect(issuable).to receive(:to_ability_name).and_return('issue')
+ expect(issuable).to receive(:project).and_return(project)
+ expect(Ability).to receive(:allowed?).at_least(1).and_return(true)
expect(Discussion).to receive(:build_collection).and_return([])
expect(DiscussionSerializer).to receive(:new).and_return(discussion_serializer)
expect(NotesFinder).to receive(:new).with(user, finder_params_for_issuable).and_call_original
expect_any_instance_of(NotesFinder).to receive(:execute).and_return(notes_result)
- expect(notes_result).to receive_messages(inc_relations_for_view: notes_result, includes: notes_result, fresh: notes_result)
+ expect(notes_result).to receive_messages(
+ with_web_entity_associations: notes_result,
+ inc_relations_for_view: notes_result,
+ fresh: notes_result
+ )
controller.discussions
end
diff --git a/spec/controllers/concerns/preferred_language_switcher_spec.rb b/spec/controllers/concerns/preferred_language_switcher_spec.rb
new file mode 100644
index 00000000000..40d6ac10c37
--- /dev/null
+++ b/spec/controllers/concerns/preferred_language_switcher_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PreferredLanguageSwitcher, type: :controller do
+ controller(ActionController::Base) do
+ include PreferredLanguageSwitcher # rubocop:disable RSpec/DescribedClass
+
+ before_action :init_preferred_language, only: :new
+
+ def new
+ render html: 'new page'
+ end
+ end
+
+ context 'when first visit' do
+ before do
+ get :new
+ end
+
+ it 'sets preferred_language to default' do
+ expect(cookies[:preferred_language]).to eq Gitlab::CurrentSettings.default_preferred_language
+ end
+ end
+
+ context 'when preferred language in cookies has been modified' do
+ let(:user_preferred_language) { nil }
+
+ before do
+ cookies[:preferred_language] = user_preferred_language
+
+ get :new
+ end
+
+ context 'with a valid value' do
+ let(:user_preferred_language) { 'zh_CN' }
+
+ it 'keeps preferred language unchanged' do
+ expect(cookies[:preferred_language]).to eq user_preferred_language
+ end
+ end
+
+ context 'with an invalid value' do
+ let(:user_preferred_language) { 'xxx' }
+
+ it 'sets preferred_language to default' do
+ expect(cookies[:preferred_language]).to eq Gitlab::CurrentSettings.default_preferred_language
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/renders_commits_spec.rb b/spec/controllers/concerns/renders_commits_spec.rb
index acdeb98bb16..6a504681527 100644
--- a/spec/controllers/concerns/renders_commits_spec.rb
+++ b/spec/controllers/concerns/renders_commits_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe RendersCommits do
context 'rendering commits' do
render_views
- it 'avoids N + 1' do
+ it 'avoids N + 1', :request_store do
stub_const("MergeRequestDiff::COMMITS_SAFE_SIZE", 5)
control_count = ActiveRecord::QueryRecorder.new do
@@ -59,7 +59,7 @@ RSpec.describe RendersCommits do
end
describe '.prepare_commits_for_rendering' do
- it 'avoids N+1' do
+ it 'avoids N+1', :request_store do
control = ActiveRecord::QueryRecorder.new do
subject.prepare_commits_for_rendering(merge_request.commits.take(1))
end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index 32304815bbb..0b24387483b 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -18,6 +18,12 @@ RSpec.describe SendFileUpload do
end
end
+ let(:cdn_uploader_class) do
+ Class.new(uploader_class) do
+ include ObjectStorage::CDN::Concern
+ end
+ end
+
let(:controller_class) do
Class.new do
include SendFileUpload
@@ -269,5 +275,28 @@ RSpec.describe SendFileUpload do
it_behaves_like 'handles image resize requests'
end
+
+ context 'when CDN-enabled remote file is used' do
+ let(:uploader) { cdn_uploader_class.new(object, :file) }
+ let(:request) { instance_double('ActionDispatch::Request', remote_ip: '18.245.0.42') }
+ let(:signed_url) { 'https://cdn.example.org.test' }
+ let(:cdn_provider) { instance_double('ObjectStorage::CDN::GoogleCDN', signed_url: signed_url) }
+
+ before do
+ stub_uploads_object_storage(uploader: cdn_uploader_class)
+ uploader.object_store = ObjectStorage::Store::REMOTE
+ uploader.store!(temp_file)
+ allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { false }
+ end
+
+ it 'sends a file when CDN URL' do
+ expect(uploader).to receive(:use_cdn?).and_return(true)
+ expect(uploader).to receive(:cdn_provider).and_return(cdn_provider)
+ expect(controller).to receive(:request).and_return(request)
+ expect(controller).to receive(:redirect_to).with(signed_url)
+
+ subject
+ end
+ end
end
end
diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb
index 111bfb24c7e..773a416dcb4 100644
--- a/spec/controllers/confirmations_controller_spec.rb
+++ b/spec/controllers/confirmations_controller_spec.rb
@@ -10,17 +10,27 @@ RSpec.describe ConfirmationsController do
end
describe '#show' do
+ let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
+ let(:confirmation_token) { user.confirmation_token }
+
render_views
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) }
+ context 'when signup info is required' do
+ before do
+ allow(controller).to receive(:current_user) { user }
+ user.set_role_required!
+ end
- let(:confirmation_token) { user.confirmation_token }
+ it 'does not redirect' do
+ expect(perform_request).not_to redirect_to(users_sign_up_welcome_path)
+ end
+ end
+ context 'user is already confirmed' do
before do
user.confirm
end
@@ -57,10 +67,6 @@ RSpec.describe ConfirmationsController do
end
context 'user accesses the link after the expiry of confirmation token has passed' do
- let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
-
- let(:confirmation_token) { user.confirmation_token }
-
before do
allow(Devise).to receive(:confirm_within).and_return(1.day)
end
@@ -133,6 +139,17 @@ RSpec.describe ConfirmationsController do
stub_feature_flags(identity_verification: false)
end
+ context 'when signup info is required' do
+ before do
+ allow(controller).to receive(:current_user) { user }
+ user.set_role_required!
+ end
+
+ it 'does not redirect' do
+ expect(perform_request).not_to redirect_to(users_sign_up_welcome_path)
+ end
+ end
+
context 'when reCAPTCHA is disabled' do
before do
stub_application_setting(recaptcha_enabled: false)
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index 21810f64cb4..ea12b0c5ad7 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -41,20 +41,6 @@ RSpec.describe DashboardController do
expect(assigns[:issues].map(&:id)).to include(task.id)
end
-
- context 'when work_items is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does not include tasks in issue list' do
- task = create(:work_item, :task, project: project, author: user)
-
- get :issues, params: { author_id: user.id }
-
- expect(assigns[:issues].map(&:id)).not_to include(task.id)
- end
- end
end
describe 'GET merge requests' do
diff --git a/spec/controllers/explore/groups_controller_spec.rb b/spec/controllers/explore/groups_controller_spec.rb
index 310fe609cf1..a3bd8102462 100644
--- a/spec/controllers/explore/groups_controller_spec.rb
+++ b/spec/controllers/explore/groups_controller_spec.rb
@@ -9,31 +9,43 @@ RSpec.describe Explore::GroupsController do
sign_in(user)
end
- it 'renders group trees' do
- expect(described_class).to include(GroupTree)
- end
+ shared_examples 'explore groups' do
+ it 'renders group trees' do
+ expect(described_class).to include(GroupTree)
+ end
- it 'includes public projects' do
- member_of_group = create(:group)
- member_of_group.add_developer(user)
- public_group = create(:group, :public)
+ it 'includes public projects' do
+ member_of_group = create(:group)
+ member_of_group.add_developer(user)
+ public_group = create(:group, :public)
- get :index
+ get :index
- expect(assigns(:groups)).to contain_exactly(member_of_group, public_group)
- end
+ expect(assigns(:groups)).to contain_exactly(member_of_group, public_group)
+ end
- context 'restricted visibility level is public' do
- before do
- sign_out(user)
+ context 'restricted visibility level is public' do
+ before do
+ sign_out(user)
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it 'redirects to login page' do
+ get :index
+
+ expect(response).to redirect_to new_user_session_path
+ end
end
+ end
- it 'redirects to login page' do
- get :index
+ it_behaves_like 'explore groups'
- expect(response).to redirect_to new_user_session_path
+ context 'generic_explore_groups flag is disabled' do
+ before do
+ stub_feature_flags(generic_explore_groups: false)
end
+
+ it_behaves_like 'explore groups'
end
end
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index bf578489916..5c977439af4 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -208,19 +208,26 @@ RSpec.describe Explore::ProjectsController do
render_views
# some N+1 queries still exist
- it 'avoids N+1 queries' do
- projects = create_list(:project, 3, :repository, :public)
- projects.each do |project|
- pipeline = create(:ci_pipeline, :success, project: project, sha: project.commit.id)
- create(:commit_status, :success, pipeline: pipeline, ref: pipeline.ref)
+ it 'avoids N+1 queries', :request_store do
+ # Because we enable the request store for this spec, Gitaly may report too many invocations.
+ # Allow N+1s here and when creating additional objects below because we're just creating test objects.
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ projects = create_list(:project, 3, :repository, :public)
+
+ projects.each do |project|
+ pipeline = create(:ci_pipeline, :success, project: project, sha: project.commit.id)
+ create(:commit_status, :success, pipeline: pipeline, ref: pipeline.ref)
+ end
end
control = ActiveRecord::QueryRecorder.new { get endpoint }
- new_projects = create_list(:project, 2, :repository, :public)
- new_projects.each do |project|
- pipeline = create(:ci_pipeline, :success, project: project, sha: project.commit.id)
- create(:commit_status, :success, pipeline: pipeline, ref: pipeline.ref)
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ new_projects = create_list(:project, 2, :repository, :public)
+ new_projects.each do |project|
+ pipeline = create(:ci_pipeline, :success, project: project, sha: project.commit.id)
+ create(:commit_status, :success, pipeline: pipeline, ref: pipeline.ref)
+ end
end
expect { get endpoint }.not_to exceed_query_limit(control).with_threshold(8)
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 7c9236704ec..fe8b0291733 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -8,10 +8,6 @@ RSpec.describe GraphqlController do
# two days is enough to make timezones irrelevant
let_it_be(:last_activity_on) { 2.days.ago.to_date }
- before do
- stub_feature_flags(graphql: true)
- end
-
describe 'rescue_from' do
let_it_be(:message) { 'green ideas sleep furiously' }
@@ -418,4 +414,114 @@ RSpec.describe GraphqlController do
expect(log_payload.dig(:exception_object)).to eq(exception)
end
end
+
+ describe 'removal of deprecated items' do
+ let(:mock_schema) do
+ Class.new(GraphQL::Schema) do
+ lazy_resolve ::Gitlab::Graphql::Lazy, :force
+
+ query(Class.new(::Types::BaseObject) do
+ graphql_name 'Query'
+
+ field :foo, GraphQL::Types::Boolean,
+ deprecated: { milestone: '0.1', reason: :renamed }
+
+ field :bar, (Class.new(::Types::BaseEnum) do
+ graphql_name 'BarEnum'
+
+ value 'FOOBAR', value: 'foobar', deprecated: { milestone: '0.1', reason: :renamed }
+ end)
+
+ field :baz, GraphQL::Types::Boolean do
+ argument :arg, String, required: false, deprecated: { milestone: '0.1', reason: :renamed }
+ end
+
+ def foo
+ false
+ end
+
+ def bar
+ 'foobar'
+ end
+
+ def baz(arg:)
+ false
+ end
+ end)
+ end
+ end
+
+ before do
+ allow(GitlabSchema).to receive(:execute).and_wrap_original do |method, *args|
+ mock_schema.execute(*args)
+ end
+ end
+
+ context 'without `remove_deprecated` param' do
+ let(:params) { { query: '{ foo bar baz(arg: "test") }' } }
+
+ subject { post :execute, params: params }
+
+ it "sets context's `remove_deprecated` value to false" do
+ subject
+
+ expect(assigns(:context)[:remove_deprecated]).to be false
+ end
+
+ it 'returns deprecated items in response' do
+ subject
+
+ expect(json_response).to include('data' => { 'foo' => false, 'bar' => 'FOOBAR', 'baz' => false })
+ end
+ end
+
+ context 'with `remove_deprecated` param' do
+ let(:params) { { remove_deprecated: 'true' } }
+
+ subject { post :execute, params: params }
+
+ it "sets context's `remove_deprecated` value to true" do
+ subject
+
+ expect(assigns(:context)[:remove_deprecated]).to be true
+ end
+
+ it 'does not allow deprecated field' do
+ params[:query] = '{ foo }'
+
+ subject
+
+ expect(json_response).not_to include('data' => { 'foo' => false })
+ expect(json_response).to include(
+ 'errors' => include(a_hash_including('message' => /Field 'foo' doesn't exist on type 'Query'/))
+ )
+ end
+
+ it 'does not allow deprecated enum value' do
+ params[:query] = '{ bar }'
+
+ subject
+
+ expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
+ expect(json_response).to include(
+ 'errors' => include(
+ a_hash_including(
+ 'message' => /`Query.bar` returned `"foobar"` at `bar`, but this isn't a valid value for `BarEnum`/
+ )
+ )
+ )
+ end
+
+ it 'does not allow deprecated argument' do
+ params[:query] = '{ baz(arg: "test") }'
+
+ subject
+
+ expect(json_response).not_to include('data' => { 'bar' => 'FOOBAR' })
+ expect(json_response).to include(
+ 'errors' => include(a_hash_including('message' => /Field 'baz' doesn't accept argument 'arg'/))
+ )
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index 04cf7785f1e..f05551432fa 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -277,7 +277,7 @@ RSpec.describe Groups::ChildrenController do
context 'with only projects' do
let!(:other_project) { create(:project, :public, namespace: group) }
- let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group ) }
+ let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) }
it 'has projects on the first page' do
get :index, params: { group_id: group.to_param, sort: 'id_desc' }, format: :json
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index a3659ae9163..4e5dc01f466 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -342,6 +342,41 @@ RSpec.describe Groups::GroupMembersController do
end
end
+ context 'with owners from a parent' do
+ context 'when top-level group' do
+ context 'with group sharing' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ before do
+ create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
+ create(:group_member, :owner, group: subgroup)
+ end
+
+ it 'does not allow removal of last direct group owner' do
+ delete :leave, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'when subgroup' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ before do
+ subgroup.add_owner(user)
+ end
+
+ it 'allows removal of last direct group owner', :aggregate_failures do
+ delete :leave, params: { group_id: subgroup }
+
+ expect(controller).to set_flash.to "You left the \"#{subgroup.human_name}\" group."
+ expect(response).to redirect_to(dashboard_groups_path)
+ expect(subgroup.users).not_to include user
+ end
+ end
+ end
+
context 'and there is another owner' do
before do
create(:group_member, :owner, source: group)
diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb
index 9ac19b06718..62c15201a95 100644
--- a/spec/controllers/groups/registry/repositories_controller_spec.rb
+++ b/spec/controllers/groups/registry/repositories_controller_spec.rb
@@ -117,7 +117,7 @@ RSpec.describe Groups::Registry::RepositoriesController do
it_behaves_like 'a package tracking event', described_class.name, 'list_repositories'
context 'with project in subgroup' do
- let_it_be(:test_group) { create(:group, parent: group ) }
+ let_it_be(:test_group) { create(:group, parent: group) }
it_behaves_like 'renders a list of repositories'
diff --git a/spec/controllers/groups/releases_controller_spec.rb b/spec/controllers/groups/releases_controller_spec.rb
index 7dd0bc6206a..40e8cb4efc5 100644
--- a/spec/controllers/groups/releases_controller_spec.rb
+++ b/spec/controllers/groups/releases_controller_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Groups::ReleasesController do
end
it 'does not return any releases' do
- expect(json_response.map { |r| r['tag'] } ).to be_empty
+ expect(json_response.map { |r| r['tag'] }).to be_empty
end
it 'returns OK' do
@@ -56,7 +56,7 @@ RSpec.describe Groups::ReleasesController do
index
- expect(json_response.map { |r| r['tag'] } ).to match_array(%w(p2 p1 v2 v1))
+ expect(json_response.map { |r| r['tag'] }).to match_array(%w(p2 p1 v2 v1))
end
end
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 6dbf0803892..2add3cd3b18 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -168,7 +168,7 @@ RSpec.describe Groups::RunnersController do
new_desc = runner.description.swapcase
expect do
- post :update, params: params.merge(runner: { description: new_desc } )
+ post :update, params: params.merge(runner: { description: new_desc })
end.to change { runner.ensure_runner_queue_value }
expect(response).to have_gitlab_http_status(:found)
@@ -179,7 +179,7 @@ RSpec.describe Groups::RunnersController do
new_desc = instance_runner.description.swapcase
expect do
- post :update, params: params_runner_instance.merge(runner: { description: new_desc } )
+ post :update, params: params_runner_instance.merge(runner: { description: new_desc })
end.to not_change { instance_runner.ensure_runner_queue_value }
.and not_change { instance_runner.description }
@@ -190,7 +190,7 @@ RSpec.describe Groups::RunnersController do
new_desc = project_runner.description.swapcase
expect do
- post :update, params: params_runner_project.merge(runner: { description: new_desc } )
+ post :update, params: params_runner_project.merge(runner: { description: new_desc })
end.to change { project_runner.ensure_runner_queue_value }
expect(response).to have_gitlab_http_status(:found)
@@ -207,7 +207,7 @@ RSpec.describe Groups::RunnersController do
old_desc = runner.description
expect do
- post :update, params: params.merge(runner: { description: old_desc.swapcase } )
+ post :update, params: params.merge(runner: { description: old_desc.swapcase })
end.not_to change { runner.ensure_runner_queue_value }
expect(response).to have_gitlab_http_status(:not_found)
@@ -218,7 +218,7 @@ RSpec.describe Groups::RunnersController do
old_desc = instance_runner.description
expect do
- post :update, params: params_runner_instance.merge(runner: { description: old_desc.swapcase } )
+ post :update, params: params_runner_instance.merge(runner: { description: old_desc.swapcase })
end.not_to change { instance_runner.ensure_runner_queue_value }
expect(response).to have_gitlab_http_status(:not_found)
@@ -229,7 +229,7 @@ RSpec.describe Groups::RunnersController do
old_desc = project_runner.description
expect do
- post :update, params: params_runner_project.merge(runner: { description: old_desc.swapcase } )
+ post :update, params: params_runner_project.merge(runner: { description: old_desc.swapcase })
end.not_to change { project_runner.ensure_runner_queue_value }
expect(response).to have_gitlab_http_status(:not_found)
diff --git a/spec/controllers/groups/settings/repository_controller_spec.rb b/spec/controllers/groups/settings/repository_controller_spec.rb
index cbf55218b94..73a205069f5 100644
--- a/spec/controllers/groups/settings/repository_controller_spec.rb
+++ b/spec/controllers/groups/settings/repository_controller_spec.rb
@@ -13,88 +13,73 @@ RSpec.describe Groups::Settings::RepositoryController do
end
describe 'POST create_deploy_token' do
- context 'when ajax_new_deploy_token feature flag is disabled for the project' do
- before do
- stub_feature_flags(ajax_new_deploy_token: false)
- entity.add_owner(user)
- end
+ let(:good_deploy_token_params) do
+ {
+ name: 'name',
+ expires_at: 1.day.from_now.to_s,
+ username: 'deployer',
+ read_repository: '1',
+ deploy_token_type: DeployToken.deploy_token_types[:group_type]
+ }
+ end
- it_behaves_like 'a created deploy token' do
- let(:entity) { group }
- let(:create_entity_params) { { group_id: group } }
- let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
- end
+ let(:request_params) do
+ {
+ group_id: group.to_param,
+ deploy_token: deploy_token_params
+ }
end
- context 'when ajax_new_deploy_token feature flag is enabled for the project' do
- let(:good_deploy_token_params) do
- {
- name: 'name',
- expires_at: 1.day.from_now.to_s,
- username: 'deployer',
- read_repository: '1',
- deploy_token_type: DeployToken.deploy_token_types[:group_type]
- }
- end
+ before do
+ group.add_owner(user)
+ end
+
+ subject { post :create_deploy_token, params: request_params, format: :json }
- let(:request_params) do
+ context('a good request') do
+ let(:deploy_token_params) { good_deploy_token_params }
+ let(:expected_response) do
{
- group_id: group.to_param,
- deploy_token: deploy_token_params
+ 'id' => be_a(Integer),
+ 'name' => deploy_token_params[:name],
+ 'username' => deploy_token_params[:username],
+ 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
+ 'token' => be_a(String),
+ 'expired' => false,
+ 'revoked' => false,
+ 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
+ key, value = kv
+ key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
+ end
}
end
- before do
- group.add_owner(user)
- end
+ it 'creates the deploy token' do
+ subject
- subject { post :create_deploy_token, params: request_params, format: :json }
-
- context('a good request') do
- let(:deploy_token_params) { good_deploy_token_params }
- let(:expected_response) do
- {
- 'id' => be_a(Integer),
- 'name' => deploy_token_params[:name],
- 'username' => deploy_token_params[:username],
- 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
- 'token' => be_a(String),
- 'expired' => false,
- 'revoked' => false,
- 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
- key, value = kv
- key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
- end
- }
- end
-
- it 'creates the deploy token' do
- subject
-
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/deploy_token')
- expect(json_response).to match(expected_response)
- end
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('public_api/v4/deploy_token')
+ expect(json_response).to match(expected_response)
end
+ end
- context('a bad request') do
- let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
- let(:expected_response) { { 'message' => "Scopes can't be blank" } }
+ context('a bad request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
+ let(:expected_response) { { 'message' => "Scopes can't be blank" } }
- it 'does not create the deploy token' do
- subject
+ it 'does not create the deploy token' do
+ subject
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to match(expected_response)
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to match(expected_response)
end
+ end
- context('an invalid request') do
- let(:deploy_token_params) { good_deploy_token_params.except(:name) }
+ context('an invalid request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:name) }
- it 'raises a validation error' do
- expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
- end
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
end
end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 5bbe236077c..22a406b3197 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -42,21 +42,15 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
- shared_examples 'details view' do
- let(:namespace) { group }
+ shared_examples 'details view as atom' do
+ let!(:event) { create(:event, project: project) }
+ let(:format) { :atom }
it { is_expected.to render_template('groups/show') }
- context 'as atom' do
- let!(:event) { create(:event, project: project) }
- let(:format) { :atom }
-
- it { is_expected.to render_template('groups/show') }
-
- it 'assigns events for all the projects in the group', :sidekiq_might_not_need_inline do
- subject
- expect(assigns(:events).map(&:id)).to contain_exactly(event.id)
- end
+ it 'assigns events for all the projects in the group' do
+ subject
+ expect(assigns(:events).map(&:id)).to contain_exactly(event.id)
end
end
@@ -70,7 +64,9 @@ RSpec.describe GroupsController, factory_default: :keep do
subject { get :show, params: { id: group.to_param }, format: format }
context 'when the group is not importing' do
- it_behaves_like 'details view'
+ it { is_expected.to render_template('groups/show') }
+
+ it_behaves_like 'details view as atom'
it 'tracks page views', :snowplow do
subject
@@ -115,7 +111,9 @@ RSpec.describe GroupsController, factory_default: :keep do
subject { get :details, params: { id: group.to_param }, format: format }
- it_behaves_like 'details view'
+ it { is_expected.to redirect_to(group_path(group)) }
+
+ it_behaves_like 'details view as atom'
end
describe 'GET edit' do
@@ -672,7 +670,7 @@ RSpec.describe GroupsController, factory_default: :keep do
end
context 'when there is a conflicting group path' do
- let!(:conflict_group) { create(:group, path: SecureRandom.hex(12) ) }
+ let!(:conflict_group) { create(:group, path: SecureRandom.hex(12)) }
let!(:old_name) { group.name }
it 'does not render references to the conflicting group' do
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index fb90a70d91d..5185aa64d9f 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -213,6 +213,75 @@ RSpec.describe Oauth::AuthorizationsController do
expect(response).to redirect_to(new_user_session_path)
end
end
+
+ context 'when the user is admin' do
+ context 'when disable_admin_oauth_scopes is set' do
+ before do
+ stub_application_setting(disable_admin_oauth_scopes: true)
+ scopes = Doorkeeper::OAuth::Scopes.from_string('api')
+
+ allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes)
+ end
+
+ let(:user) { create(:user, :admin) }
+
+ it 'returns 200 and renders forbidden view' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('doorkeeper/authorizations/forbidden')
+ end
+ end
+
+ context 'when disable_admin_oauth_scopes is set and the application is trusted' do
+ before do
+ stub_application_setting(disable_admin_oauth_scopes: true)
+
+ application.update!(trusted: true)
+ end
+
+ let(:application_scopes) { 'api' }
+ let(:user) { create(:user, :admin) }
+
+ it 'returns 200 and renders redirect view' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('doorkeeper/authorizations/redirect')
+ end
+ end
+
+ context 'when disable_admin_oauth_scopes is disabled' do
+ before do
+ stub_application_setting(disable_admin_oauth_scopes: false)
+ end
+
+ let(:application_scopes) { 'api' }
+ let(:user) { create(:user, :admin) }
+
+ it 'returns 200 and renders new view' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('doorkeeper/authorizations/new')
+ end
+ end
+ end
+
+ context 'when the user is not admin' do
+ context 'when disable_admin_oauth_scopes is enabled' do
+ before do
+ stub_application_setting(disable_admin_oauth_scopes: true)
+ end
+
+ it 'returns 200 and renders new view' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('doorkeeper/authorizations/new')
+ end
+ end
+ end
end
describe 'POST #create' do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index df5da29495e..0560ccb25dd 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
let(:additional_info) { {} }
before do
- @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: additional_info )
+ @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: additional_info)
stub_omniauth_provider(provider, context: request)
end
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index e4be2fbef3c..9494f55c631 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -78,6 +78,22 @@ RSpec.describe PasswordsController do
end
end
+ context 'password is weak' do
+ let(:password) { "password" }
+
+ it 'tracks the event' do
+ subject
+
+ expect(response.body).to have_content("must not contain commonly used combinations of words and letters")
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'PasswordsController',
+ method: 'create'
+ )
+ 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)
diff --git a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
index 8dee0490fd6..044ce8f397a 100644
--- a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe Profiles::PersonalAccessTokensController do
- let(:user) { create(:user) }
+ let(:access_token_user) { create(:user) }
let(:token_attributes) { attributes_for(:personal_access_token) }
before do
- sign_in(user)
+ sign_in(access_token_user)
end
describe '#create' do
@@ -49,13 +49,27 @@ RSpec.describe Profiles::PersonalAccessTokensController do
end
end
+ describe 'GET /-/profile/personal_access_tokens' do
+ let(:get_access_tokens) do
+ get :index
+ response
+ end
+
+ subject(:get_access_tokens_with_page) do
+ get :index, params: { page: 1 }
+ response
+ end
+
+ it_behaves_like 'GET access tokens are paginated and ordered'
+ end
+
describe '#index' do
- let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
+ let!(:active_personal_access_token) { create(:personal_access_token, user: access_token_user) }
before do
# Impersonation and inactive personal tokens are ignored
- create(:personal_access_token, :impersonation, user: user)
- create(:personal_access_token, :revoked, user: user)
+ create(:personal_access_token, :impersonation, user: access_token_user)
+ create(:personal_access_token, :revoked, user: access_token_user)
get :index
end
@@ -63,7 +77,7 @@ RSpec.describe Profiles::PersonalAccessTokensController do
active_personal_access_tokens_detail =
::PersonalAccessTokenSerializer.new.represent([active_personal_access_token])
- expect(assigns(:active_personal_access_tokens).to_json).to eq(active_personal_access_tokens_detail.to_json)
+ expect(assigns(:active_access_tokens).to_json).to eq(active_personal_access_tokens_detail.to_json)
end
it "sets PAT name and scopes" do
@@ -86,73 +100,10 @@ RSpec.describe Profiles::PersonalAccessTokensController do
expect(response).to have_gitlab_http_status(:not_found)
end
- context "access_token_pagination feature flag is enabled" do
- before do
- stub_feature_flags(access_token_pagination: true)
- allow(Kaminari.config).to receive(:default_per_page).and_return(1)
- create(:personal_access_token, user: user)
- end
-
- it "returns paginated response" do
- get :index, params: { page: 1 }
- expect(assigns(:active_personal_access_tokens).count).to eq(1)
- end
-
- it 'adds appropriate headers' do
- get :index, params: { page: 1 }
- expect_header('X-Per-Page', '1')
- expect_header('X-Page', '1')
- expect_header('X-Next-Page', '2')
- expect_header('X-Total', '2')
- end
- end
-
- context "tokens returned are ordered" do
- let(:expires_1_day_from_now) { 1.day.from_now.to_date }
- let(:expires_2_day_from_now) { 2.days.from_now.to_date }
-
- before do
- create(:personal_access_token, user: user, name: "Token1", expires_at: expires_1_day_from_now)
- create(:personal_access_token, user: user, name: "Token2", expires_at: expires_2_day_from_now)
- end
-
- it "orders token list ascending on expires_at" do
- get :index
-
- first_token = assigns(:active_personal_access_tokens).first.as_json
- expect(first_token['name']).to eq("Token1")
- expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
- end
-
- it "orders tokens on id in case token has same expires_at" do
- create(:personal_access_token, user: user, name: "Token3", expires_at: expires_1_day_from_now)
-
- get :index
-
- first_token = assigns(:active_personal_access_tokens).first.as_json
- expect(first_token['name']).to eq("Token3")
- expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
-
- second_token = assigns(:active_personal_access_tokens).second.as_json
- expect(second_token['name']).to eq("Token1")
- expect(second_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
- end
- end
-
- context "access_token_pagination feature flag is disabled" do
- before do
- stub_feature_flags(access_token_pagination: false)
- create(:personal_access_token, user: user)
- end
+ it 'returns tokens for json format' do
+ get :index, params: { format: :json }
- it "returns all tokens in system" do
- get :index, params: { page: 1 }
- expect(assigns(:active_personal_access_tokens).count).to eq(2)
- end
+ expect(json_response.count).to eq(1)
end
end
-
- def expect_header(header_name, header_val)
- expect(response.headers[header_name]).to eq(header_val)
- end
end
diff --git a/spec/controllers/projects/alerting/notifications_controller_spec.rb b/spec/controllers/projects/alerting/notifications_controller_spec.rb
index b3feeb7c07b..5ce2950f95f 100644
--- a/spec/controllers/projects/alerting/notifications_controller_spec.rb
+++ b/spec/controllers/projects/alerting/notifications_controller_spec.rb
@@ -16,9 +16,6 @@ RSpec.describe Projects::Alerting::NotificationsController do
end
shared_examples 'process alert payload' do |notify_service_class|
- let(:alert_1) { build(:alert_management_alert, project: project) }
- let(:alert_2) { build(:alert_management_alert, project: project) }
- let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
let(:notify_service) { instance_double(notify_service_class, execute: service_response) }
before do
@@ -35,11 +32,14 @@ RSpec.describe Projects::Alerting::NotificationsController do
it 'responds with the alert data' do
make_request
- expect(json_response).to contain_exactly(
- { 'iid' => alert_1.iid, 'title' => alert_1.title },
- { 'iid' => alert_2.iid, 'title' => alert_2.title }
- )
- expect(response).to have_gitlab_http_status(:ok)
+ if service_response.payload.present?
+ expect(json_response).to contain_exactly(
+ { 'iid' => alert_1.iid, 'title' => alert_1.title },
+ { 'iid' => alert_2.iid, 'title' => alert_2.title }
+ )
+ end
+
+ expect(response).to have_gitlab_http_status(service_response.http_status)
end
it 'does not pass excluded parameters to the notify service' do
@@ -146,6 +146,9 @@ RSpec.describe Projects::Alerting::NotificationsController do
context 'with generic alert payload' do
it_behaves_like 'process alert payload', Projects::Alerting::NotifyService do
+ let(:alert_1) { build(:alert_management_alert, project: project) }
+ let(:alert_2) { build(:alert_management_alert, project: project) }
+ let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
let(:payload) { { title: 'Alert title' } }
end
end
@@ -154,6 +157,7 @@ RSpec.describe Projects::Alerting::NotificationsController do
include PrometheusHelpers
it_behaves_like 'process alert payload', Projects::Prometheus::Alerts::NotifyService do
+ let(:service_response) { ServiceResponse.success(http_status: :created) }
let(:payload) { prometheus_alert_payload }
end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index f79a2c6a6d0..00efd7d7b56 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -30,28 +30,10 @@ RSpec.describe Projects::ArtifactsController do
stub_feature_flags(artifacts_management_page: true)
end
- it 'sets the artifacts variable' do
+ it 'renders the page' do
subject
- expect(assigns(:artifacts)).to contain_exactly(*project.job_artifacts)
- end
-
- it 'sets the total size variable' do
- subject
-
- expect(assigns(:total_size)).to eq(project.job_artifacts.total_size)
- end
-
- describe 'pagination' do
- before do
- stub_const("#{described_class}::MAX_PER_PAGE", 1)
- end
-
- it 'paginates artifacts' do
- subject
-
- expect(assigns(:artifacts)).to contain_exactly(project.reload.job_artifacts.last)
- end
+ expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -65,18 +47,6 @@ RSpec.describe Projects::ArtifactsController do
expect(response).to have_gitlab_http_status(:no_content)
end
-
- it 'does not set the artifacts variable' do
- subject
-
- expect(assigns(:artifacts)).to eq(nil)
- end
-
- it 'does not set the total size variable' do
- subject
-
- expect(assigns(:total_size)).to eq(nil)
- end
end
end
@@ -183,12 +153,17 @@ RSpec.describe Projects::ArtifactsController do
end
context 'when file is stored remotely' do
+ let(:cdn_config) {}
+
before do
- stub_artifacts_object_storage
+ stub_artifacts_object_storage(cdn: cdn_config)
create(:ci_job_artifact, :remote_store, :codequality, job: job)
+ allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
end
it 'sends the codequality report' do
+ expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).and_call_original
+
expect(controller).to receive(:redirect_to).and_call_original
download_artifact(file_type: file_type)
@@ -201,6 +176,30 @@ RSpec.describe Projects::ArtifactsController do
download_artifact(file_type: file_type, proxy: true)
end
end
+
+ context 'when Google CDN is configured' do
+ let(:cdn_config) do
+ {
+ 'provider' => 'Google',
+ 'url' => 'https://cdn.example.org',
+ 'key_name' => 'some-key',
+ 'key' => Base64.urlsafe_encode64(SecureRandom.hex)
+ }
+ end
+
+ before do
+ request.env['action_dispatch.remote_ip'] = '18.245.0.42'
+ end
+
+ it 'redirects to a Google CDN request' do
+ expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).and_call_original
+ expect(Gitlab::ApplicationContext).to receive(:push).with(artifact_used_cdn: true).and_call_original
+
+ download_artifact(file_type: file_type)
+
+ expect(response.redirect_url).to start_with("https://cdn.example.org/")
+ end
+ end
end
end
end
@@ -228,8 +227,9 @@ RSpec.describe Projects::ArtifactsController do
expect(response).to have_gitlab_http_status(:forbidden)
expect(response.body).to include(
'You must have developer or higher permissions in the associated project to view job logs when debug trace is enabled. ' \
- 'To disable debug trace, set the &#39;CI_DEBUG_TRACE&#39; variable to &#39;false&#39; in your pipeline configuration or CI/CD settings. ' \
- 'If you need to view this job log, a project maintainer or owner must add you to the project with developer permissions or higher.'
+ 'To disable debug trace, set the &#39;CI_DEBUG_TRACE&#39; and &#39;CI_DEBUG_SERVICES&#39; variables to &#39;false&#39; ' \
+ 'in your pipeline configuration or CI/CD settings. If you must view this job log, a project maintainer or owner must ' \
+ 'add you to the project with developer permissions or higher.'
)
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 16a43bae674..5927f20df97 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -76,7 +76,7 @@ RSpec.describe Projects::EnvironmentsController do
it 'handles search option properly' do
get :index, params: environment_params(format: :json, search: 'staging/r')
- expect(environments.map { |env| env['name'] } ).to contain_exactly('staging/review-1', 'staging/review-2')
+ expect(environments.map { |env| env['name'] }).to contain_exactly('staging/review-1', 'staging/review-2')
expect(json_response['available_count']).to eq 2
expect(json_response['stopped_count']).to eq 1
end
@@ -84,7 +84,7 @@ RSpec.describe Projects::EnvironmentsController do
it 'ignores search option if is shorter than a minimum' do
get :index, params: environment_params(format: :json, search: 'st')
- expect(environments.map { |env| env['name'] } ).to contain_exactly('production',
+ expect(environments.map { |env| env['name'] }).to contain_exactly('production',
'staging/review-1',
'staging/review-2')
expect(json_response['available_count']).to eq 3
@@ -233,7 +233,7 @@ RSpec.describe Projects::EnvironmentsController do
search: 'staging-1.0/z'
}, format: :json)
- expect(environments.map { |env| env['name'] } ).to eq(['staging-1.0/zzz'])
+ expect(environments.map { |env| env['name'] }).to eq(['staging-1.0/zzz'])
expect(json_response['available_count']).to eq 1
expect(json_response['stopped_count']).to eq 0
end
@@ -705,7 +705,7 @@ RSpec.describe Projects::EnvironmentsController do
expect(json_response).to have_key('all_dashboards')
expect(json_response['all_dashboards']).to be_an_instance_of(Array)
- expect(json_response['all_dashboards']).to all( include('path', 'default', 'display_name') )
+ expect(json_response['all_dashboards']).to all(include('path', 'default', 'display_name'))
end
end
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index ba7b712964c..18f16937505 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -29,6 +29,22 @@ RSpec.describe Projects::HooksController do
{ namespace_id: project.namespace, project_id: project, id: hook.id }
end
+ context 'with an existing token' do
+ hook_params = {
+ token: WebHook::SECRET_MASK,
+ url: "http://example.com"
+ }
+
+ it 'does not change a token' do
+ expect do
+ post :update, params: params.merge({ hook: hook_params })
+ end.not_to change { hook.reload.token }
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:alert]).to be_blank
+ end
+ end
+
it 'adds, updates and deletes URL variables' do
hook.update!(url_variables: { 'a' => 'bar', 'b' => 'woo' })
@@ -106,8 +122,9 @@ RSpec.describe Projects::HooksController do
it 'sets all parameters' do
hook_params = {
enable_ssl_verification: true,
- token: "TEST TOKEN",
- url: "http://example.com",
+ token: 'TEST TOKEN',
+ url: 'http://example.com',
+ branch_filter_strategy: 'regex',
push_events: true,
tag_push_events: true,
@@ -124,13 +141,39 @@ RSpec.describe Projects::HooksController do
url_variables: [{ key: 'token', value: 'some secret value' }]
}
- post :create, params: { namespace_id: project.namespace, project_id: project, hook: hook_params }
+ params = { namespace_id: project.namespace, project_id: project, hook: hook_params }
+
+ expect { post :create, params: params }.to change(ProjectHook, :count).by(1)
+
+ project_hook = ProjectHook.order_id_desc.take
+
+ expect(project_hook).to have_attributes(
+ **hook_params.merge(url_variables: { 'token' => 'some secret value' })
+ )
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:alert]).to be_blank
+ end
+
+ it 'ignores branch_filter_strategy when flag is disabled' do
+ stub_feature_flags(enhanced_webhook_support_regex: false)
+ hook_params = {
+ url: 'http://example.com',
+ branch_filter_strategy: 'regex',
+ push_events: true
+ }
+ params = { namespace_id: project.namespace, project_id: project, hook: hook_params }
+
+ expect { post :create, params: params }.to change(ProjectHook, :count).by(1)
+
+ project_hook = ProjectHook.order_id_desc.take
+
+ expect(project_hook).to have_attributes(
+ url: 'http://example.com',
+ branch_filter_strategy: 'wildcard'
+ )
expect(response).to have_gitlab_http_status(:found)
expect(flash[:alert]).to be_blank
- expect(ProjectHook.count).to eq(1)
- expect(ProjectHook.first).to have_attributes(hook_params.except(:url_variables))
- expect(ProjectHook.first).to have_attributes(url_variables: { 'token' => 'some secret value' })
end
it 'alerts the user if the new hook is invalid' do
@@ -186,7 +229,7 @@ RSpec.describe Projects::HooksController do
context 'when the hook fails completely' do
before do
allow_next(::TestHooks::ProjectService)
- .to receive(:execute).and_return({ message: 'All is woe' })
+ .to receive(:execute).and_return(ServiceResponse.error(message: 'All is woe'))
end
it 'informs the user' do
@@ -204,7 +247,7 @@ RSpec.describe Projects::HooksController do
it 'prevents making test requests' do
expect_next_instance_of(TestHooks::ProjectService) do |service|
- expect(service).to receive(:execute).and_return(http_status: 200)
+ expect(service).to receive(:execute).and_return(ServiceResponse.success(payload: { http_status: 200 }))
end
2.times { post :test, params: { namespace_id: project.namespace, project_id: project, id: hook } }
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 0c3795540e0..8f26be442a7 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -168,75 +168,56 @@ RSpec.describe Projects::IssuesController do
let_it_be(:task) { create(:issue, :task, project: project) }
- context 'when work_items feature flag is enabled' do
- shared_examples 'redirects to show work item page' do
- it 'redirects to work item page' do
- expect(response).to redirect_to(project_work_items_path(project, task.id, query))
- end
- end
-
- context 'show action' do
- let(:query) { { query: 'any' } }
-
+ shared_examples 'redirects to show work item page' do
+ context 'when use_iid_in_work_items_path feature flag is disabled' do
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: task.iid, **query }
+ stub_feature_flags(use_iid_in_work_items_path: false)
end
- it_behaves_like 'redirects to show work item page'
- end
-
- context 'edit action' do
- let(:query) { { query: 'any' } }
+ it 'redirects to work item page' do
+ make_request
- before do
- get :edit, params: { namespace_id: project.namespace, project_id: project, id: task.iid, **query }
+ expect(response).to redirect_to(project_work_items_path(project, task.id, query))
end
-
- it_behaves_like 'redirects to show work item page'
end
- context 'update action' do
- before do
- put :update, params: { namespace_id: project.namespace, project_id: project, id: task.iid, issue: { title: 'New title' } }
- end
+ it 'redirects to work item page using iid' do
+ make_request
- it_behaves_like 'redirects to show work item page'
+ expect(response).to redirect_to(project_work_items_path(project, task.iid, query.merge(iid_path: true)))
end
end
- context 'when work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
+ context 'show action' do
+ let(:query) { { query: 'any' } }
- shared_examples 'renders 404' do
- it 'renders 404 for show action' do
- expect(response).to have_gitlab_http_status(:not_found)
+ it_behaves_like 'redirects to show work item page' do
+ subject(:make_request) do
+ get :show, params: { namespace_id: project.namespace, project_id: project, id: task.iid, **query }
end
end
+ end
- context 'show action' do
- before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: task.iid }
- end
-
- it_behaves_like 'renders 404'
- end
+ context 'edit action' do
+ let(:query) { { query: 'any' } }
- context 'edit action' do
- before do
- get :edit, params: { namespace_id: project.namespace, project_id: project, id: task.iid }
+ it_behaves_like 'redirects to show work item page' do
+ subject(:make_request) do
+ get :edit, params: { namespace_id: project.namespace, project_id: project, id: task.iid, **query }
end
-
- it_behaves_like 'renders 404'
end
+ end
- context 'update action' do
- before do
- put :update, params: { namespace_id: project.namespace, project_id: project, id: task.iid, issue: { title: 'New title' } }
+ context 'update action' do
+ it_behaves_like 'redirects to show work item page' do
+ subject(:make_request) do
+ put :update, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: task.iid,
+ issue: { title: 'New title' }
+ }
end
-
- it_behaves_like 'renders 404'
end
end
end
@@ -1107,6 +1088,24 @@ RSpec.describe Projects::IssuesController do
end
end
+ context 'when trying to create a objective' do
+ it 'defaults to issue type' do
+ issue = post_new_issue(issue_type: 'objective')
+
+ expect(issue.issue_type).to eq('issue')
+ expect(issue.work_item_type.base_type).to eq('issue')
+ end
+ end
+
+ context 'when trying to create a key_result' do
+ it 'defaults to issue type' do
+ issue = post_new_issue(issue_type: 'key_result')
+
+ expect(issue.issue_type).to eq('issue')
+ expect(issue.work_item_type.base_type).to eq('issue')
+ end
+ end
+
context 'when create service return an unrecoverable error with http_status' do
let(:http_status) { 403 }
@@ -1291,7 +1290,7 @@ RSpec.describe Projects::IssuesController do
let!(:last_spam_log) { spam_logs.last }
def post_verified_issue
- post_new_issue({}, { spam_log_id: last_spam_log.id, 'g-recaptcha-response': 'abc123' } )
+ post_new_issue({}, { spam_log_id: last_spam_log.id, 'g-recaptcha-response': 'abc123' })
end
before do
@@ -1311,7 +1310,7 @@ RSpec.describe Projects::IssuesController do
it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
spam_log = create(:spam_log)
- expect { post_new_issue({}, { spam_log_id: spam_log.id, 'g-recaptcha-response': true } ) }
+ expect { post_new_issue({}, { spam_log_id: spam_log.id, 'g-recaptcha-response': true }) }
.not_to change { last_spam_log.recaptcha_verified }
end
end
@@ -1709,19 +1708,6 @@ RSpec.describe Projects::IssuesController do
expect(response).to redirect_to(project_issues_path(project))
expect(controller).to set_flash[:notice].to match(/\AYour CSV export has started/i)
end
-
- context 'when work_items is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does not include tasks in CSV export' do
- expect(IssuableExportCsvWorker).to receive(:perform_async)
- .with(:issue, viewer.id, project.id, hash_including('issue_types' => Issue::TYPES_FOR_LIST.excluding('task')))
-
- request_csv
- end
- end
end
context 'when not logged in' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 556dd23c135..3dc89365530 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -660,6 +660,38 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
end
+
+ context 'when CI_DEBUG_SERVICES enabled' do
+ let!(:variable) { create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: 'true') }
+
+ context 'with proper permissions on a project' do
+ let(:user) { developer }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'returns response ok' do
+ get_trace
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'without proper permissions for debug logging' do
+ let(:user) { guest }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'returns response forbidden' do
+ get_trace
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
end
context 'when job has a live trace' do
@@ -1184,36 +1216,51 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
end
- context 'when CI_DEBUG_TRACE enabled' do
- before do
- create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true')
+ context 'when CI_DEBUG_TRACE and/or CI_DEBUG_SERVICES are enabled' do
+ using RSpec::Parameterized::TableSyntax
+ where(:ci_debug_trace, :ci_debug_services) do
+ 'true' | 'true'
+ 'true' | 'false'
+ 'false' | 'true'
+ 'false' | 'false'
end
- context 'with proper permissions for debug logging on a project' do
- let(:user) { developer }
-
+ with_them do
before do
- sign_in(user)
+ create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: ci_debug_trace)
+ create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: ci_debug_services)
end
- it 'returns response ok' do
- response = subject
+ context 'with proper permissions for debug logging on a project' do
+ let(:user) { developer }
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
+ before do
+ sign_in(user)
+ end
- context 'without proper permissions for debug logging on a project' do
- let(:user) { reporter }
+ it 'returns response ok' do
+ response = subject
- before do
- sign_in(user)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
- it 'returns response forbidden' do
- response = subject
+ context 'without proper permissions for debug logging on a project' do
+ let(:user) { reporter }
- expect(response).to have_gitlab_http_status(:forbidden)
+ before do
+ sign_in(user)
+ end
+
+ it 'returns response forbidden if dev mode enabled' do
+ response = subject
+
+ if ci_debug_trace == 'true' || ci_debug_services == 'true'
+ expect(response).to have_gitlab_http_status(:forbidden)
+ else
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
end
end
@@ -1380,7 +1427,7 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
{
'Channel' => {
'Subprotocols' => ["terminal.gitlab.com"],
- 'Url' => 'wss://localhost/proxy/build/default_port/',
+ 'Url' => 'wss://gitlab.example.com/proxy/build/default_port/',
'Header' => {
'Authorization' => [nil]
},
@@ -1536,7 +1583,8 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
expect(job.runner_session_url).to start_with('https://')
- expect(Gitlab::Workhorse).to receive(:channel_websocket).with(a_hash_including(url: "wss://localhost/proxy/build/default_port/"))
+ expect(Gitlab::Workhorse).to receive(:channel_websocket)
+ .with(a_hash_including(url: "wss://gitlab.example.com/proxy/build/default_port/"))
make_request
end
diff --git a/spec/controllers/projects/learn_gitlab_controller_spec.rb b/spec/controllers/projects/learn_gitlab_controller_spec.rb
index 2d00fcbccf3..a93da82d948 100644
--- a/spec/controllers/projects/learn_gitlab_controller_spec.rb
+++ b/spec/controllers/projects/learn_gitlab_controller_spec.rb
@@ -34,8 +34,15 @@ RSpec.describe Projects::LearnGitlabController do
it { is_expected.to have_gitlab_http_status(:not_found) }
end
- it_behaves_like 'tracks assignment and records the subject', :invite_for_help_continuous_onboarding, :namespace do
- subject { project.namespace }
+ context 'with invite_for_help_continuous_onboarding experiment' do
+ it 'tracks the assignment', :experiment do
+ stub_experiments(invite_for_help_continuous_onboarding: true)
+
+ expect(experiment(:invite_for_help_continuous_onboarding))
+ .to track(:assignment).with_context(namespace: project.namespace).on_next_instance
+
+ action
+ end
end
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 367781c0e76..613d82efd06 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -213,7 +213,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
commit: nil,
latest_diff: true,
only_context_commits: false,
- allow_tree_conflicts: true,
+ merge_conflicts_in_diff: true,
merge_ref_head_diff: false
}
end
@@ -281,7 +281,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
commit: nil,
latest_diff: true,
only_context_commits: false,
- allow_tree_conflicts: true,
+ merge_conflicts_in_diff: true,
merge_ref_head_diff: nil
}
end
@@ -303,7 +303,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
commit: merge_request.diff_head_commit,
latest_diff: nil,
only_context_commits: false,
- allow_tree_conflicts: true,
+ merge_conflicts_in_diff: true,
merge_ref_head_diff: nil
}
end
@@ -329,7 +329,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
commit: nil,
latest_diff: true,
only_context_commits: false,
- allow_tree_conflicts: false,
+ merge_conflicts_in_diff: false,
merge_ref_head_diff: nil
}
end
@@ -488,7 +488,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
commit: nil,
diff_view: :inline,
merge_ref_head_diff: nil,
- allow_tree_conflicts: true,
+ merge_conflicts_in_diff: true,
pagination_data: {
total_pages: nil
}.merge(pagination_data)
@@ -616,7 +616,7 @@ RSpec.describe Projects::MergeRequests::DiffsController do
it_behaves_like 'serializes diffs with expected arguments' do
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) }
+ let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_conflicts_in_diff: false) }
end
it_behaves_like 'successful request'
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index a41abd8c16d..026cf19bde5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequestsController do
include ProjectForksHelper
include Gitlab::Routing
+ using RSpec::Parameterized::TableSyntax
let_it_be_with_refind(:project) { create(:project, :repository) }
let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) }
@@ -708,12 +709,14 @@ RSpec.describe Projects::MergeRequestsController do
end
describe 'GET commits' do
- def go(format: 'html')
+ def go(page: nil, per_page: 1, format: 'html')
get :commits,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
- id: merge_request.iid
+ id: merge_request.iid,
+ page: page,
+ per_page: per_page
},
format: format
end
@@ -723,6 +726,27 @@ RSpec.describe Projects::MergeRequestsController do
expect(response).to render_template('projects/merge_requests/_commits')
expect(json_response).to have_key('html')
+ expect(json_response).to have_key('next_page')
+ expect(json_response['next_page']).to eq(2)
+ end
+
+ describe 'pagination' do
+ where(:page, :next_page) do
+ 1 | 2
+ 2 | 3
+ 3 | nil
+ end
+
+ with_them do
+ it "renders the commits for page #{params[:page]}" do
+ go format: 'json', page: page, per_page: 10
+
+ expect(response).to render_template('projects/merge_requests/_commits')
+ expect(json_response).to have_key('html')
+ expect(json_response).to have_key('next_page')
+ expect(json_response['next_page']).to eq(next_page)
+ end
+ end
end
end
@@ -1756,7 +1780,7 @@ RSpec.describe Projects::MergeRequestsController do
end
it 'renders MergeRequest as JSON' do
- expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'merge_status', 'can_be_merged', 'current_user')
+ expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'current_user')
end
end
@@ -1790,7 +1814,7 @@ RSpec.describe Projects::MergeRequestsController do
it 'renders MergeRequest as JSON' do
subject
- expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'merge_status', 'can_be_merged', 'current_user')
+ expect(json_response.keys).to include('id', 'iid', 'title', 'has_ci', 'current_user')
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index b132c0b5a69..f66e4b133ca 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -20,23 +20,11 @@ RSpec.describe Projects::PipelinesController do
end
shared_examples 'the show page' do |param|
- it 'redirects to pipeline path with param' do
+ it 'renders the show template' do
get param, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
- expect(response).to redirect_to(pipeline_path(pipeline, tab: param))
- end
-
- context 'when the FF pipeline_tabs_vue is disabled' do
- before do
- stub_feature_flags(pipeline_tabs_vue: false)
- end
-
- it 'renders the show template' do
- get param, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template :show
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
end
end
@@ -311,14 +299,15 @@ RSpec.describe Projects::PipelinesController do
stub_application_setting(auto_devops_enabled: false)
end
- def action
- get :index, params: { namespace_id: project.namespace, project_id: project }
- end
+ context 'with runners_availability_section experiment' do
+ it 'tracks the assignment', :experiment do
+ stub_experiments(runners_availability_section: true)
- subject { project.namespace }
+ expect(experiment(:runners_availability_section))
+ .to track(:assignment).with_context(namespace: project.namespace).on_next_instance
- context 'runners_availability_section experiment' do
- it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+ end
end
end
@@ -710,37 +699,25 @@ RSpec.describe Projects::PipelinesController do
describe 'GET failures' do
let(:pipeline) { create(:ci_pipeline, project: project) }
- context 'with ff `pipeline_tabs_vue` disabled' do
+ context 'with failed jobs' do
before do
- stub_feature_flags(pipeline_tabs_vue: false)
+ create(:ci_build, :failed, pipeline: pipeline, name: 'hello')
end
- context 'with failed jobs' do
- before do
- create(:ci_build, :failed, pipeline: pipeline, name: 'hello')
- end
-
- it 'shows the page' do
- get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template :show
- end
- end
-
- context 'without failed jobs' do
- it 'redirects to the main pipeline page' do
- get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
+ it 'shows the page' do
+ get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
- expect(response).to redirect_to(pipeline_path(pipeline))
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template :show
end
end
- it 'redirects to the pipeline page with `failures` query param' do
- get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
+ context 'without failed jobs' do
+ it 'redirects to the main pipeline page' do
+ get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
- expect(response).to redirect_to(pipeline_path(pipeline, tab: 'failures'))
+ expect(response).to redirect_to(pipeline_path(pipeline))
+ end
end
end
diff --git a/spec/controllers/projects/product_analytics_controller_spec.rb b/spec/controllers/projects/product_analytics_controller_spec.rb
deleted file mode 100644
index 47f1d96c70b..00000000000
--- a/spec/controllers/projects/product_analytics_controller_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::ProductAnalyticsController do
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
-
- before(:all) do
- project.add_maintainer(user)
- end
-
- before do
- sign_in(user)
- stub_feature_flags(product_analytics: true)
- end
-
- describe 'GET #index' do
- it 'renders index with 200 status code' do
- get :index, params: project_params
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:index)
- end
-
- context 'with an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to sign-in page' do
- get :index, params: project_params
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'feature flag disabled' do
- before do
- stub_feature_flags(product_analytics: false)
- end
-
- it 'returns not found' do
- get :index, params: project_params
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET #test' do
- it 'renders test with 200 status code' do
- get :test, params: project_params
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:test)
- end
- end
-
- describe 'GET #setup' do
- it 'renders setup with 200 status code' do
- get :setup, params: project_params
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:setup)
- end
- end
-
- describe 'GET #graphs' do
- it 'renders graphs with 200 status code' do
- get :graphs, params: project_params
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:graphs)
- end
-
- context 'feature flag disabled' do
- before do
- stub_feature_flags(product_analytics: false)
- end
-
- it 'returns not found' do
- get :graphs, params: project_params
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- private
-
- def project_params(opts = {})
- opts.reverse_merge(namespace_id: project.namespace, project_id: project)
- end
-end
diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
index 2c2c8180143..09b9f25c0c6 100644
--- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe Projects::Prometheus::AlertsController do
describe 'POST #notify' do
let(:alert_1) { build(:alert_management_alert, :prometheus, project: project) }
let(:alert_2) { build(:alert_management_alert, :prometheus, project: project) }
- let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) }
+ let(:service_response) { ServiceResponse.success(http_status: :created) }
let(:notify_service) { instance_double(Projects::Prometheus::Alerts::NotifyService, execute: service_response) }
before do
@@ -68,17 +68,12 @@ RSpec.describe Projects::Prometheus::AlertsController do
.and_return(notify_service)
end
- it 'returns ok if notification succeeds' do
+ it 'returns created if notification succeeds' do
expect(notify_service).to receive(:execute).and_return(service_response)
post :notify, params: project_params, session: { as: :json }
- expect(json_response).to contain_exactly(
- { 'iid' => alert_1.iid, 'title' => alert_1.title },
- { 'iid' => alert_2.iid, 'title' => alert_2.title }
- )
-
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:created)
end
it 'returns unprocessable entity if notification fails' do
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index a5faaaf5969..f4f5c182850 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -103,10 +103,11 @@ RSpec.describe Projects::Registry::RepositoriesController do
stub_container_registry_tags(repository: :any, tags: [])
end
- it 'schedules a job to delete a repository' do
- expect(DeleteContainerRepositoryWorker).to receive(:perform_async).with(user.id, repository.id)
+ it 'marks the repository as delete_scheduled' do
+ expect(DeleteContainerRepositoryWorker).not_to receive(:perform_async).with(user.id, repository.id)
- delete_repository(repository)
+ expect { delete_repository(repository) }
+ .to change { repository.reload.status }.from(nil).to('delete_scheduled')
expect(repository.reload).to be_delete_scheduled
expect(response).to have_gitlab_http_status(:no_content)
@@ -119,6 +120,22 @@ RSpec.describe Projects::Registry::RepositoriesController do
expect_snowplow_event(category: anything, action: 'delete_repository')
end
+
+ context 'with container_registry_delete_repository_with_cron_worker disabled' do
+ before do
+ stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
+ end
+
+ it 'schedules a job to delete a repository' do
+ expect(DeleteContainerRepositoryWorker).to receive(:perform_async).with(user.id, repository.id)
+
+ expect { delete_repository(repository) }
+ .to change { repository.reload.status }.from(nil).to('delete_scheduled')
+
+ expect(repository.reload).to be_delete_scheduled
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
end
end
end
@@ -137,7 +154,7 @@ RSpec.describe Projects::Registry::RepositoriesController do
end
end
- def go_to_index(format: :html, params: {} )
+ def go_to_index(format: :html, params: {})
get :index, params: params.merge({
namespace_id: project.namespace,
project_id: project
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index b307bb357fa..2afd080344d 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe Projects::ReleasesController do
it "returns the project's releases as JSON, ordered by released_at" do
get_index
- expect(json_response.map { |release| release["id"] } ).to eq([release_2.id, release_1.id])
+ expect(json_response.map { |release| release["id"] }).to eq([release_2.id, release_1.id])
end
it_behaves_like 'common access controls'
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index 57d1695b842..1066c4ec9f6 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Projects::RunnersController do
new_desc = runner.description.swapcase
expect do
- post :update, params: params.merge(runner: { description: new_desc } )
+ post :update, params: params.merge(runner: { description: new_desc })
end.to change { runner.ensure_runner_queue_value }
runner.reload
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index b76269f6f93..2b23f177a9d 100644
--- a/spec/controllers/projects/settings/integrations_controller_spec.rb
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -334,6 +334,23 @@ RSpec.describe Projects::Settings::IntegrationsController do
)
end
end
+
+ context 'with chat notification integration' do
+ let_it_be(:integration) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') }
+ let(:message) { 'Microsoft Teams notifications settings saved and active.' }
+
+ it_behaves_like 'integration update'
+
+ context 'with masked token' do
+ let(:integration_params) { { active: true, webhook: '************' } }
+
+ it_behaves_like 'integration update'
+
+ it 'does not update the webhook' do
+ expect(integration.reload.webhook).to eq('http://webhook.com')
+ end
+ end
+ end
end
describe 'as JSON' do
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 22287fea82c..ea50ff6caa0 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::RepositoryController do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
+ let(:base_params) { { namespace_id: project.namespace, project_id: project } }
before do
project.add_maintainer(user)
@@ -13,7 +14,7 @@ RSpec.describe Projects::Settings::RepositoryController do
describe 'GET show' do
it 'renders show with 200 status code' do
- get :show, params: { namespace_id: project.namespace, project_id: project }
+ get :show, params: base_params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
@@ -29,7 +30,7 @@ RSpec.describe Projects::Settings::RepositoryController do
.with(project, user, anything)
.and_return(status: :success)
- put :cleanup, params: { namespace_id: project.namespace, project_id: project, project: { bfg_object_map: object_map } }
+ put :cleanup, params: base_params.merge({ project: { bfg_object_map: object_map } })
expect(response).to redirect_to project_settings_repository_path(project)
end
@@ -41,7 +42,7 @@ RSpec.describe Projects::Settings::RepositoryController do
.with(project, user, anything)
.and_return(status: :error, message: 'error message')
- put :cleanup, params: { namespace_id: project.namespace, project_id: project, project: { bfg_object_map: object_map } }
+ put :cleanup, params: base_params.merge({ project: { bfg_object_map: object_map } })
expect(controller).to set_flash[:alert].to('error message')
expect(response).to redirect_to project_settings_repository_path(project)
@@ -50,83 +51,138 @@ RSpec.describe Projects::Settings::RepositoryController do
end
describe 'POST create_deploy_token' do
- context 'when ajax_new_deploy_token feature flag is disabled for the project' do
- before do
- stub_feature_flags(ajax_new_deploy_token: false)
+ let(:good_deploy_token_params) do
+ {
+ name: 'name',
+ expires_at: 1.day.from_now.to_s,
+ username: 'deployer',
+ read_repository: '1',
+ deploy_token_type: DeployToken.deploy_token_types[:project_type]
+ }
+ end
+
+ let(:request_params) { base_params.merge({ deploy_token: deploy_token_params }) }
+
+ subject { post :create_deploy_token, params: request_params, format: :json }
+
+ context('a good request') do
+ let(:deploy_token_params) { good_deploy_token_params }
+ let(:expected_response) do
+ {
+ 'id' => be_a(Integer),
+ 'name' => deploy_token_params[:name],
+ 'username' => deploy_token_params[:username],
+ 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
+ 'token' => be_a(String),
+ 'expired' => false,
+ 'revoked' => false,
+ 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
+ key, value = kv
+ key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
+ end
+ }
end
- it_behaves_like 'a created deploy token' do
- let(:entity) { project }
- let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } }
- let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] }
+ it 'creates the deploy token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('public_api/v4/deploy_token')
+ expect(json_response).to match(expected_response)
end
end
- context 'when ajax_new_deploy_token feature flag is enabled for the project' do
- let(:good_deploy_token_params) do
- {
- name: 'name',
- expires_at: 1.day.from_now.to_s,
- username: 'deployer',
- read_repository: '1',
- deploy_token_type: DeployToken.deploy_token_types[:project_type]
- }
+ context('a bad request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
+ let(:expected_response) { { 'message' => "Scopes can't be blank" } }
+
+ it 'does not create the deploy token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to match(expected_response)
end
+ end
- let(:request_params) do
- {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- deploy_token: deploy_token_params
- }
+ context('an invalid request') do
+ let(:deploy_token_params) { good_deploy_token_params.except(:name) }
+
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
end
+ end
+ end
- subject { post :create_deploy_token, params: request_params, format: :json }
-
- context('a good request') do
- let(:deploy_token_params) { good_deploy_token_params }
- let(:expected_response) do
- {
- 'id' => be_a(Integer),
- 'name' => deploy_token_params[:name],
- 'username' => deploy_token_params[:username],
- 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
- 'token' => be_a(String),
- 'expired' => false,
- 'revoked' => false,
- 'scopes' => deploy_token_params.inject([]) do |scopes, kv|
- key, value = kv
- key.to_s.start_with?('read_') && value.to_i != 0 ? scopes << key.to_s : scopes
- end
- }
+ describe 'PUT update' do
+ let(:project) { create(:project, :repository) }
+
+ context 'when updating default branch' do
+ let!(:previous_default_branch) { project.default_branch }
+
+ let(:new_default_branch) { 'feature' }
+ let(:request_params) { base_params.merge({ project: project_params_attributes }) }
+
+ subject { put :update, params: request_params }
+
+ context('with a good request') do
+ let(:project_params_attributes) { { default_branch: new_default_branch } }
+
+ it "updates default branch and redirect to project_settings_repository_path" do
+ expect do
+ subject
+ end.to change {
+ Project.find(project.id).default_branch # refind to reset the default branch cache
+ }.from(previous_default_branch).to(new_default_branch)
+
+ expect(response).to redirect_to project_settings_repository_path(project)
+ expect(controller).to set_flash[:notice].to("Project settings were successfully updated.")
end
+ end
- it 'creates the deploy token' do
- subject
+ context('with a bad input') do
+ let(:project_params_attributes) { { default_branch: 'non_existent_branch' } }
- expect(response).to have_gitlab_http_status(:created)
- expect(response).to match_response_schema('public_api/v4/deploy_token')
- expect(json_response).to match(expected_response)
+ it "does not update default branch and shows an alert" do
+ expect do
+ subject
+ end.not_to change {
+ Project.find(project.id).default_branch # refind to reset the default branch cache
+ }
+
+ expect(response).to redirect_to project_settings_repository_path(project)
+ expect(controller).to set_flash[:alert].to("Could not set the default branch")
end
end
+ end
+
+ context 'when updating branch names template from issues' do
+ let(:branch_name_template) { 'feat/GL-%{id}-%{title}' }
- context('a bad request') do
- let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
- let(:expected_response) { { 'message' => "Scopes can't be blank" } }
+ let(:request_params) { base_params.merge({ project: project_params_attributes }) }
- it 'does not create the deploy token' do
+ subject { put :update, params: request_params }
+
+ context('with a good request') do
+ let(:project_params_attributes) { { issue_branch_template: branch_name_template } }
+
+ it "updates issue_branch_template and redirect to project_settings_repository_path" do
subject
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to match(expected_response)
+ expect(response).to redirect_to project_settings_repository_path(project)
+ expect(controller).to set_flash[:notice].to("Project settings were successfully updated.")
+ expect(project.reload.issue_branch_template).to eq(branch_name_template)
end
end
- context('an invalid request') do
- let(:deploy_token_params) { good_deploy_token_params.except(:name) }
+ context('with a bad input') do
+ let(:project_params_attributes) { { issue_branch_template: 'a' * 260 } }
+
+ it "updates issue_branch_template and redirect to project_settings_repository_path" do
+ subject
- it 'raises a validation error' do
- expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
+ expect(response).to redirect_to project_settings_repository_path(project)
+ expect(controller).to set_flash[:alert].to("Project setting issue branch template is too long (maximum is 255 characters)")
+ expect(project.reload.issue_branch_template).to eq(nil)
end
end
end
diff --git a/spec/controllers/projects/starrers_controller_spec.rb b/spec/controllers/projects/starrers_controller_spec.rb
index 8d03600cd58..2148f495c31 100644
--- a/spec/controllers/projects/starrers_controller_spec.rb
+++ b/spec/controllers/projects/starrers_controller_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Projects::StarrersController do
let(:user_1) { create(:user, name: 'John') }
let(:user_2) { create(:user, name: 'Michael') }
let(:private_user) { create(:user, name: 'Michael Douglas', private_profile: true) }
+ let(:blocked_user) { create(:user, state: 'blocked') }
let(:admin) { create(:user, admin: true) }
let(:project) { create(:project, :public) }
@@ -13,6 +14,7 @@ RSpec.describe Projects::StarrersController do
user_1.toggle_star(project)
user_2.toggle_star(project)
private_user.toggle_star(project)
+ blocked_user.toggle_star(project)
end
describe 'GET index' do
@@ -61,6 +63,10 @@ RSpec.describe Projects::StarrersController do
expect(user_ids).to contain_exactly(user_1.id, user_2.id)
end
+ it 'non-active users are not visible' do
+ expect(user_ids).not_to include(blocked_user.id)
+ end
+
include_examples 'starrers counts'
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index b5797e374f3..446e5e38865 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -921,6 +921,7 @@ RSpec.describe ProjectsController do
feature_flags_access_level
releases_access_level
monitor_access_level
+ infrastructure_access_level
]
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 637c774c38b..8775f68a5de 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe RegistrationsController do
include TermsHelper
+ include FullNameHelper
before do
stub_application_setting(require_admin_approval_after_user_signup: false)
@@ -18,6 +19,8 @@ RSpec.describe RegistrationsController do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:resource)).to be_a(User)
end
+
+ it_behaves_like "switches to user preferred language", 'Sign up'
end
describe '#create' do
@@ -463,7 +466,7 @@ RSpec.describe RegistrationsController do
expect(User.last.first_name).to eq(base_user_params[:first_name])
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]}")
+ expect(User.last.name).to eq full_name(base_user_params[:first_name], base_user_params[:last_name])
end
it 'sets the caller_id in the context' do
@@ -477,28 +480,6 @@ RSpec.describe RegistrationsController do
subject
end
- describe 'logged_out_marketing_header experiment', :experiment do
- before do
- stub_experiments(logged_out_marketing_header: :candidate)
- end
-
- it 'tracks signed_up event' do
- expect(experiment(:logged_out_marketing_header)).to track(:signed_up).on_next_instance
-
- subject
- end
-
- context 'when registration fails' do
- let_it_be(:user_params) { { user: base_user_params.merge({ username: '' }) } }
-
- it 'does not track signed_up event' do
- expect(experiment(:logged_out_marketing_header)).not_to track(:signed_up)
-
- subject
- end
- end
- end
-
context 'when the password is weak' do
render_views
let_it_be(:new_user_params) { { new_user: base_user_params.merge({ password: "password" }) } }
@@ -513,6 +494,16 @@ RSpec.describe RegistrationsController do
expect(response).to render_template(:new)
expect(response.body).to include(_('Password must not contain commonly used combinations of words and letters'))
end
+
+ it 'tracks the error' do
+ subject
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'RegistrationsController',
+ method: 'create'
+ )
+ end
end
context 'when block_weak_passwords is disabled' do
@@ -525,6 +516,42 @@ RSpec.describe RegistrationsController do
end
end
end
+
+ context 'when the password is not weak' do
+ it 'does not track a weak password error' do
+ subject
+ expect_no_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error'
+ )
+ end
+ end
+
+ context 'with preferred language' do
+ let(:user_preferred_language) { nil }
+
+ before do
+ cookies['preferred_language'] = user_preferred_language
+
+ post :create, params: { new_user: base_user_params }
+ end
+
+ subject { User.last.preferred_language }
+
+ context 'with default behavior' do
+ it 'sets preferred language to default' do
+ is_expected.to eq(Gitlab::CurrentSettings.default_preferred_language)
+ end
+ end
+
+ context 'when user sets preferred language' do
+ let(:user_preferred_language) { 'zh_CN' }
+
+ it 'sets name from first and last name' do
+ is_expected.to eq(user_preferred_language)
+ end
+ end
+ end
end
describe '#destroy' do
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 392dc2229aa..21df53fb074 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -223,7 +223,14 @@ RSpec.describe SearchController do
let(:project) { nil }
let(:category) { described_class.to_s }
- let(:action) { 'i_search_total' }
+ let(:action) { 'executed' }
+ let(:label) { 'redis_hll_counters.search.search_total_unique_counts_monthly' }
+ let(:property) { 'i_search_total' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: property).to_context]
+ end
+
let(:namespace) { create(:group) }
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 80cf060bc45..69282f951f9 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -69,6 +69,8 @@ RSpec.describe SessionsController do
expect(controller.stored_location_for(:redirect)).to eq(search_path)
end
+
+ it_behaves_like "switches to user preferred language", 'Sign in'
end
describe '#create' do
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 4aeafed5712..ad49a763361 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -14,8 +14,10 @@ RSpec.describe 'Database schema' do
issues: %w[work_item_type_id]
}.with_indifferent_access.freeze
+ TABLE_PARTITIONS = %w[ci_builds_metadata].freeze
+
# List of columns historically missing a FK, don't add more columns
- # See: https://docs.gitlab.com/ee/development/foreign_keys.html#naming-foreign-keys
+ # See: https://docs.gitlab.com/ee/development/database/foreign_keys.html#naming-foreign-keys
IGNORED_FK_COLUMNS = {
abuse_reports: %w[reporter_id user_id],
application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_app_id eks_account_id eks_access_key_id],
@@ -32,7 +34,7 @@ RSpec.describe 'Database schema' do
chat_names: %w[chat_id team_id user_id],
chat_teams: %w[team_id],
ci_builds: %w[erased_by_id trigger_request_id partition_id],
- ci_builds_metadata: %w[partition_id],
+ p_ci_builds_metadata: %w[partition_id],
ci_job_artifacts: %w[partition_id],
ci_namespace_monthly_usages: %w[namespace_id],
ci_pipeline_variables: %w[partition_id],
@@ -107,7 +109,7 @@ RSpec.describe 'Database schema' do
}.with_indifferent_access.freeze
context 'for table' do
- ActiveRecord::Base.connection.tables.sort.each do |table|
+ (ActiveRecord::Base.connection.tables - TABLE_PARTITIONS).sort.each do |table|
describe table do
let(:indexes) { connection.indexes(table) }
let(:columns) { connection.columns(table) }
@@ -213,6 +215,7 @@ RSpec.describe 'Database schema' do
"ApplicationSetting" => %w[repository_storages_weighted],
"AlertManagement::Alert" => %w[payload],
"Ci::BuildMetadata" => %w[config_options config_variables],
+ "Ci::BuildMetadata::Partitioned" => %w[config_options config_variables id_tokens runtime_runner_features secrets],
"ExperimentSubject" => %w[context],
"ExperimentUser" => %w[context],
"Geo::Event" => %w[payload],
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index b144e6f77d2..7aca5e492f4 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -43,72 +43,6 @@ RSpec.describe ApplicationExperiment, :experiment do
variant: 'control'
)
end
-
- describe '#publish_to_database' do
- using RSpec::Parameterized::TableSyntax
-
- let(:publish_to_database) { ActiveSupport::Deprecation.silence { application_experiment.publish_to_database } }
-
- shared_examples 'does not record to the database' do
- it 'does not create an experiment record' do
- expect { publish_to_database }.not_to change(Experiment, :count)
- end
-
- it 'does not create an experiment subject record' do
- expect { publish_to_database }.not_to change(ExperimentSubject, :count)
- end
- end
-
- context 'when there is a usable subject' do
- let(:context) { { context_key => context_value } }
-
- where(:context_key, :context_value, :object_type) do
- :namespace | build(:namespace, id: non_existing_record_id) | :namespace
- :group | build(:namespace, id: non_existing_record_id) | :namespace
- :project | build(:project, id: non_existing_record_id) | :project
- :user | build(:user, id: non_existing_record_id) | :user
- :actor | build(:user, id: non_existing_record_id) | :user
- end
-
- with_them do
- it 'creates an experiment and experiment subject record' do
- expect { publish_to_database }.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 experiment hasn't ran" do
- let(:context) { { user: create(:user) } }
-
- it 'sets a variant on the experiment subject' do
- publish_to_database
-
- expect(ExperimentSubject.last.variant).to eq('control')
- end
- end
-
- context 'when there is not a usable subject' do
- let(:context) { { context_key => context_value } }
-
- where(:context_key, :context_value) do
- :namespace | nil
- :foo | :bar
- end
-
- with_them do
- include_examples 'does not record to the database'
- end
- end
-
- context 'but we should not track' do
- let(:should_track) { false }
-
- include_examples 'does not record to the database'
- end
- end
end
describe "#track", :snowplow do
diff --git a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
index 269b6222020..c91a8f1950e 100644
--- a/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
+++ b/spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb
@@ -30,34 +30,6 @@ RSpec.describe RequireVerificationForNamespaceCreationExperiment, :experiment do
end
end
- describe '#record_conversion' do
- let_it_be(:namespace) { create(:namespace) }
-
- context 'when should_track? is false' do
- before do
- allow(experiment).to receive(:should_track?).and_return(false)
- end
-
- it 'does not record a conversion event' do
- expect(experiment.publish_to_database).to be_nil
- expect(experiment.record_conversion(namespace)).to be_nil
- end
- end
-
- context 'when should_track? is true' do
- before do
- allow(experiment).to receive(:should_track?).and_return(true)
- end
-
- it 'records a conversion event' do
- experiment_subject = experiment.publish_to_database
-
- expect { experiment.record_conversion(namespace) }.to change { experiment_subject.reload.converted_at }.from(nil)
- .and change { experiment_subject.context }.to include('namespace_id' => namespace.id)
- end
- end
- end
-
describe 'exclusions' do
context 'when user is new' do
it 'is not excluded' do
diff --git a/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb b/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb
index 4328ff12d42..ee02fa5f1f2 100644
--- a/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb
+++ b/spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb
@@ -6,10 +6,4 @@ RSpec.describe SecurityReportsMrWidgetPromptExperiment do
it "defines a control and candidate" do
expect(subject.behaviors.keys).to match_array(%w[control candidate])
end
-
- it "publishes to the database" do
- expect(subject).to receive(:publish_to_database)
-
- subject.publish
- end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 9a3b2837ab8..b88d6b5fda4 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -24,6 +24,16 @@ FactoryBot.define do
project { pipeline.project }
+ trait :with_token do
+ transient do
+ generate_token { true }
+ end
+
+ after(:build) do |build, evaluator|
+ build.ensure_token if evaluator.generate_token
+ end
+ end
+
trait :degenerated do
options { nil }
yaml_variables { nil }
@@ -93,6 +103,7 @@ FactoryBot.define do
end
trait :pending do
+ with_token
queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
status { 'pending' }
@@ -100,6 +111,7 @@ FactoryBot.define do
trait :created do
status { 'created' }
+ generate_token { false }
end
trait :preparing do
@@ -303,14 +315,11 @@ FactoryBot.define do
# Build deployment/environment relations if environment name is set
# to the job. If `build.deployment` has already been set, it doesn't
# build a new instance.
- environment = Gitlab::Ci::Pipeline::Seed::Environment.new(build).to_resource
+ Environments::CreateForBuildService.new.execute(build)
+ end
- build.assign_attributes(
- deployment: Gitlab::Ci::Pipeline::Seed::Deployment.new(build, environment).to_resource,
- metadata_attributes: {
- expanded_environment_name: environment.name
- }
- )
+ after(:create) do |build, evaluator|
+ Deployments::CreateForBuildService.new.execute(build)
end
end
@@ -716,7 +725,7 @@ FactoryBot.define do
trait :with_runner_session do
after(:build) do |build|
- build.build_runner_session(url: 'https://localhost')
+ build.build_runner_session(url: 'https://gitlab.example.com')
end
end
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 304d77e8521..7569e832c60 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -20,6 +20,8 @@ FactoryBot.define do
after :build do |artifact|
artifact.project ||= artifact.job.project
+
+ artifact.job&.valid?
end
trait :raw do
diff --git a/spec/factories/ci/pipeline_metadata.rb b/spec/factories/ci/pipeline_metadata.rb
index 600cfaa92c6..7849fa1fd4b 100644
--- a/spec/factories/ci/pipeline_metadata.rb
+++ b/spec/factories/ci/pipeline_metadata.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :ci_pipeline_metadata, class: 'Ci::PipelineMetadata' do
- title { 'Pipeline title' }
+ name { 'Pipeline name' }
pipeline factory: :ci_empty_pipeline
project
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 650b8647237..891628a0fc2 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -8,7 +8,7 @@ FactoryBot.define do
sha { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
status { 'pending' }
add_attribute(:protected) { false }
- partition_id { 1234 }
+ partition_id { 100 }
project
@@ -19,7 +19,7 @@ FactoryBot.define do
transient { child_of { nil } }
transient { upstream_of { nil } }
- transient { title { nil } }
+ transient { name { nil } }
after(:build) do |pipeline, evaluator|
if evaluator.child_of
@@ -29,8 +29,8 @@ FactoryBot.define do
pipeline.ensure_project_iid!
- if evaluator.title
- pipeline.pipeline_metadata = build(:ci_pipeline_metadata, title: evaluator.title, project: pipeline.project, pipeline: pipeline)
+ if evaluator.name
+ pipeline.pipeline_metadata = build(:ci_pipeline_metadata, name: evaluator.name, project: pipeline.project, pipeline: pipeline)
end
end
@@ -54,7 +54,7 @@ FactoryBot.define do
end
factory :ci_pipeline do
- partition_id { 1234 }
+ partition_id { 100 }
transient { ci_ref_presence { true } }
before(:create) do |pipeline, evaluator|
diff --git a/spec/factories/ci/processable.rb b/spec/factories/ci/processable.rb
index 0550f4c23fa..76c7376d24a 100644
--- a/spec/factories/ci/processable.rb
+++ b/spec/factories/ci/processable.rb
@@ -4,7 +4,7 @@ FactoryBot.define do
factory :ci_processable, class: 'Ci::Processable' do
name { 'processable' }
stage { 'test' }
- stage_idx { 0 }
+ stage_idx { ci_stage.try(:position) || 0 }
ref { 'master' }
tag { false }
pipeline factory: :ci_pipeline
diff --git a/spec/factories/ci/reports/codequality_degradations.rb b/spec/factories/ci/reports/codequality_degradations.rb
index 8b53f2bf46e..632f5a3ecaa 100644
--- a/spec/factories/ci/reports/codequality_degradations.rb
+++ b/spec/factories/ci/reports/codequality_degradations.rb
@@ -26,7 +26,8 @@ FactoryBot.define do
"remediation_points": 900000,
"severity": "major",
"type": "issue",
- "engine_name": "structure"
+ "engine_name": "structure",
+ "web_url": "http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10"
}.with_indifferent_access
end
end
@@ -56,7 +57,8 @@ FactoryBot.define do
"remediation_points": 900000,
"severity": "major",
"type": "issue",
- "engine_name": "structure"
+ "engine_name": "structure",
+ "web_url": "http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10"
}.with_indifferent_access
end
end
@@ -91,7 +93,8 @@ FactoryBot.define do
},
"engine_name": "rubocop",
"fingerprint": "ab5f8b935886b942d621399f5a2ca16e",
- "severity": "minor"
+ "severity": "minor",
+ "web_url": "http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10"
}.with_indifferent_access
end
end
diff --git a/spec/factories/ci/reports/sbom/components.rb b/spec/factories/ci/reports/sbom/components.rb
index fd9b4386130..8f2c00b695a 100644
--- a/spec/factories/ci/reports/sbom/components.rb
+++ b/spec/factories/ci/reports/sbom/components.rb
@@ -3,15 +3,29 @@
FactoryBot.define do
factory :ci_reports_sbom_component, class: '::Gitlab::Ci::Reports::Sbom::Component' do
type { "library" }
+
sequence(:name) { |n| "component-#{n}" }
sequence(:version) { |n| "v0.0.#{n}" }
+ transient do
+ purl_type { 'npm' }
+ end
+
+ purl do
+ ::Sbom::PackageUrl.new(
+ type: purl_type,
+ name: name,
+ version: version
+ ).to_s
+ end
+
skip_create
initialize_with do
::Gitlab::Ci::Reports::Sbom::Component.new(
type: type,
name: name,
+ purl: purl,
version: version
)
end
diff --git a/spec/factories/ci/reports/sbom/reports.rb b/spec/factories/ci/reports/sbom/reports.rb
index 4a83b5898ef..7a076282915 100644
--- a/spec/factories/ci/reports/sbom/reports.rb
+++ b/spec/factories/ci/reports/sbom/reports.rb
@@ -8,6 +8,12 @@ FactoryBot.define do
source { association :ci_reports_sbom_source }
end
+ trait :invalid do
+ after(:build) do |report, options|
+ report.add_error('This report is invalid because it contains errors.')
+ end
+ end
+
after(:build) do |report, options|
options.components.each { |component| report.add_component(component) }
report.set_source(options.source)
diff --git a/spec/factories/ci/secure_files.rb b/spec/factories/ci/secure_files.rb
index 74988202c71..31dbcd15cb1 100644
--- a/spec/factories/ci/secure_files.rb
+++ b/spec/factories/ci/secure_files.rb
@@ -13,4 +13,13 @@ FactoryBot.define do
end
end
end
+
+ factory :ci_secure_file_with_metadata, class: 'Ci::SecureFile' do
+ sequence(:name) { |n| "file#{n}.cer" }
+ file { fixture_file_upload('spec/fixtures/ci_secure_files/sample.cer', 'application/octet-stream') }
+ checksum { 'foo1234' }
+ project
+
+ after(:create, &:update_metadata!)
+ end
end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index 41297b01f92..d9dff4d9a86 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :ci_stage, class: 'Ci::Stage' do
- project factory: :project
+ project { pipeline.project }
pipeline factory: :ci_empty_pipeline
name { 'test' }
diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb
index 210441430b0..66ac72fb5d7 100644
--- a/spec/factories/container_repositories.rb
+++ b/spec/factories/container_repositories.rb
@@ -21,6 +21,10 @@ FactoryBot.define do
status { :delete_failed }
end
+ trait :status_delete_ongoing do
+ status { :delete_ongoing }
+ end
+
trait :cleanup_scheduled do
expiration_policy_cleanup_status { :cleanup_scheduled }
end
diff --git a/spec/factories/dependency_proxy.rb b/spec/factories/dependency_proxy.rb
index afa6c61116a..33356a701df 100644
--- a/spec/factories/dependency_proxy.rb
+++ b/spec/factories/dependency_proxy.rb
@@ -4,13 +4,20 @@ FactoryBot.define do
factory :dependency_proxy_blob, class: 'DependencyProxy::Blob' do
group
size { 1234 }
- file { fixture_file_upload('spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz') }
file_name { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz' }
status { :default }
+ after(:build) do |blob, _evaluator|
+ blob.file = fixture_file_upload('spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz')
+ end
+
trait :pending_destruction do
status { :pending_destruction }
end
+
+ trait :remote_store do
+ file_store { DependencyProxy::FileUploader::Store::REMOTE }
+ end
end
factory :dependency_proxy_manifest, class: 'DependencyProxy::Manifest' do
diff --git a/spec/factories/experiment_subjects.rb b/spec/factories/experiment_subjects.rb
deleted file mode 100644
index c35bc370bad..00000000000
--- a/spec/factories/experiment_subjects.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :experiment_subject do
- experiment
- user
- variant { :control }
- end
-end
diff --git a/spec/factories/experiment_users.rb b/spec/factories/experiment_users.rb
deleted file mode 100644
index 66c39d684eb..00000000000
--- a/spec/factories/experiment_users.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :experiment_user do
- experiment
- user
- group_type { :control }
- converted_at { nil }
- end
-end
diff --git a/spec/factories/experiments.rb b/spec/factories/experiments.rb
deleted file mode 100644
index 2c51a6585f4..00000000000
--- a/spec/factories/experiments.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :experiment do
- name { generate(:title) }
- end
-end
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index 5ac26b7a260..ebbf1b560e5 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -43,6 +43,19 @@ FactoryBot.define do
end
end
+ factory :packagist_integration, class: 'Integrations::Packagist' do
+ project
+ type { 'Integrations::Packagist' }
+ active { true }
+ properties do
+ {
+ username: 'username',
+ token: 'test',
+ server: 'https://packagist.example.com'
+ }
+ end
+ end
+
factory :prometheus_integration, class: 'Integrations::Prometheus' do
project
active { true }
diff --git a/spec/factories/member_roles.rb b/spec/factories/member_roles.rb
index bd211844f5a..08df45a85f8 100644
--- a/spec/factories/member_roles.rb
+++ b/spec/factories/member_roles.rb
@@ -4,5 +4,7 @@ FactoryBot.define do
factory :member_role do
namespace { association(:group) }
base_access_level { Gitlab::Access::DEVELOPER }
+
+ trait(:guest) { base_access_level { GroupMember::GUEST } }
end
end
diff --git a/spec/factories/merge_request_reviewers.rb b/spec/factories/merge_request_reviewers.rb
new file mode 100644
index 00000000000..26e047a3fbf
--- /dev/null
+++ b/spec/factories/merge_request_reviewers.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :merge_request_reviewer do
+ merge_request
+ reviewer { association(:user) }
+ state { 'unreviewed' }
+ end
+end
diff --git a/spec/factories/packages/rpm/rpm_repository_files.rb b/spec/factories/packages/rpm/rpm_repository_files.rb
index 079d32b3995..00755f49d98 100644
--- a/spec/factories/packages/rpm/rpm_repository_files.rb
+++ b/spec/factories/packages/rpm/rpm_repository_files.rb
@@ -4,9 +4,10 @@ FactoryBot.define do
factory :rpm_repository_file, class: 'Packages::Rpm::RepositoryFile' do
project
- file_name { 'repomd.xml' }
+ file_name { '364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml' }
file_sha1 { 'efae869b4e95d54796a46481f3a211d6a88d0323' }
file_md5 { 'ddf8a75330c896a8d7709e75f8b5982a' }
+ file_sha256 { '364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3' }
size { 3127.kilobytes }
status { :default }
@@ -15,7 +16,11 @@ FactoryBot.define do
end
transient do
- file_fixture { 'spec/fixtures/packages/rpm/repodata/repomd.xml' }
+ file_fixture do
+ # rubocop:disable Layout/LineLength
+ 'spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml'
+ # rubocop:enable Layout/LineLength
+ end
end
after(:build) do |package_file, evaluator|
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index dbb5c357acb..946b3925ee9 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -6,6 +6,10 @@ FactoryBot.define do
enable_ssl_verification { false }
project
+ trait :url_variables do
+ url_variables { { 'abc' => 'supers3cret' } }
+ end
+
trait :token do
token { generate(:token) }
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index b62995dce42..6e3a7a3f5ef 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -41,6 +41,7 @@ FactoryBot.define do
environments_access_level { ProjectFeature::ENABLED }
feature_flags_access_level { ProjectFeature::ENABLED }
releases_access_level { ProjectFeature::ENABLED }
+ infrastructure_access_level { ProjectFeature::ENABLED }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first
@@ -251,6 +252,7 @@ FactoryBot.define do
transient do
create_templates { nil }
create_branch { nil }
+ create_tag { nil }
end
after :create do |project, evaluator|
@@ -287,6 +289,13 @@ FactoryBot.define do
end
+ if evaluator.create_tag
+ project.repository.add_tag(
+ project.creator,
+ evaluator.create_tag,
+ project.repository.commit.sha)
+ end
+
project.track_project_repository
end
end
@@ -467,6 +476,10 @@ FactoryBot.define do
end
end
+ trait :in_group do
+ namespace factory: [:group]
+ end
+
trait :in_subgroup do
namespace factory: [:group, :nested]
end
diff --git a/spec/factories/projects/import_export/export_relation.rb b/spec/factories/projects/import_export/export_relation.rb
deleted file mode 100644
index 2b6419dcecb..00000000000
--- a/spec/factories/projects/import_export/export_relation.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :project_relation_export, class: 'Projects::ImportExport::RelationExport' do
- project_export_job factory: :project_export_job
-
- relation { 'labels' }
- status { 0 }
- sequence(:jid) { |n| "project_relation_export_#{n}" }
- end
-end
diff --git a/spec/factories/projects/import_export/relation_export.rb b/spec/factories/projects/import_export/relation_export.rb
new file mode 100644
index 00000000000..7fab5808d2f
--- /dev/null
+++ b/spec/factories/projects/import_export/relation_export.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_relation_export, class: 'Projects::ImportExport::RelationExport' do
+ project_export_job factory: :project_export_job
+
+ relation { 'labels' }
+ status { Projects::ImportExport::RelationExport::STATUS[:queued] }
+ sequence(:jid) { |n| "project_relation_export_#{n}" }
+
+ trait :queued do
+ status { Projects::ImportExport::RelationExport::STATUS[:queued] }
+ end
+
+ trait :started do
+ status { Projects::ImportExport::RelationExport::STATUS[:started] }
+ end
+
+ trait :finished do
+ status { Projects::ImportExport::RelationExport::STATUS[:finished] }
+ end
+
+ trait :failed do
+ status { Projects::ImportExport::RelationExport::STATUS[:failed] }
+ end
+ end
+end
diff --git a/spec/factories/projects/import_export/relation_export_upload.rb b/spec/factories/projects/import_export/relation_export_upload.rb
new file mode 100644
index 00000000000..eaa57d6ee59
--- /dev/null
+++ b/spec/factories/projects/import_export/relation_export_upload.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_relation_export_upload, class: 'Projects::ImportExport::RelationExportUpload' do
+ relation_export factory: :project_relation_export
+ export_file { fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz") }
+ end
+end
diff --git a/spec/factories/projects/wiki_repositories.rb b/spec/factories/projects/wiki_repositories.rb
new file mode 100644
index 00000000000..78e02ff297b
--- /dev/null
+++ b/spec/factories/projects/wiki_repositories.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_wiki_repository, class: 'Projects::WikiRepository' do
+ project
+ end
+end
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index 425352783dd..75b375733a6 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -13,7 +13,8 @@ FactoryBot.define do
end
after(:create) do |protected_branch, evaluator|
- break unless protected_branch.project&.persisted?
+ # Do not use `break` because it will cause `LocalJumpError`
+ next unless protected_branch.project&.persisted?
ProtectedBranches::CacheService.new(protected_branch.project).refresh
end
@@ -39,63 +40,63 @@ FactoryBot.define do
end
end
- trait :maintainers_can_push do
+ trait :no_one_can_merge do
transient do
- default_push_level { false }
+ default_merge_level { false }
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
end
end
- trait :maintainers_can_merge do
+ trait :developers_can_merge do
transient do
- default_push_level { false }
+ default_merge_level { false }
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
end
end
- trait :developers_can_push do
+ trait :maintainers_can_merge do
transient do
- default_push_level { false }
+ default_merge_level { false }
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
- trait :developers_can_merge do
+ trait :no_one_can_push do
transient do
- default_merge_level { false }
+ default_push_level { false }
end
after(:build) do |protected_branch|
- protected_branch.merge_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
end
end
- trait :no_one_can_push do
+ trait :developers_can_push do
transient do
default_push_level { false }
end
after(:build) do |protected_branch|
- protected_branch.push_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::DEVELOPER)
end
end
- trait :no_one_can_merge do
+ trait :maintainers_can_push do
transient do
- default_merge_level { false }
+ default_push_level { false }
end
after(:build) do |protected_branch|
- protected_branch.merge_access_levels.new(access_level: Gitlab::Access::NO_ACCESS)
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MAINTAINER)
end
end
end
diff --git a/spec/factories/user_statuses.rb b/spec/factories/user_statuses.rb
index dbed6031ce1..79dc1eb7931 100644
--- a/spec/factories/user_statuses.rb
+++ b/spec/factories/user_statuses.rb
@@ -5,5 +5,9 @@ FactoryBot.define do
user
emoji { 'coffee' }
message { 'I crave coffee' }
+
+ trait :busy do
+ availability { 'busy' }
+ end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 2e7c6116fe6..2b53a469841 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -27,6 +27,10 @@ FactoryBot.define do
after(:build) { |user, _| user.block! }
end
+ trait :locked do
+ after(:build) { |user, _| user.lock_access! }
+ end
+
trait :disallowed_password do
password { User::DISALLOWED_PASSWORDS.first }
end
diff --git a/spec/factories/users/ghost_user_migrations.rb b/spec/factories/users/ghost_user_migrations.rb
index 0fe7cded4f3..77b7f7e6df4 100644
--- a/spec/factories/users/ghost_user_migrations.rb
+++ b/spec/factories/users/ghost_user_migrations.rb
@@ -5,5 +5,6 @@ FactoryBot.define do
association :user
initiator_user { association(:user) }
hard_delete { false }
+ consume_after { Time.current }
end
end
diff --git a/spec/factories/users/namespace_commit_emails.rb b/spec/factories/users/namespace_commit_emails.rb
new file mode 100644
index 00000000000..2f7e89bf766
--- /dev/null
+++ b/spec/factories/users/namespace_commit_emails.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :namespace_commit_email, class: 'Users::NamespaceCommitEmail' do
+ email
+ user { email.user }
+ namespace
+ end
+end
diff --git a/spec/features/admin/admin_dev_ops_reports_spec.rb b/spec/features/admin/admin_dev_ops_reports_spec.rb
index bf32819cb52..f65862c568f 100644
--- a/spec/features/admin/admin_dev_ops_reports_spec.rb
+++ b/spec/features/admin/admin_dev_ops_reports_spec.rb
@@ -9,9 +9,9 @@ RSpec.describe 'DevOps Report page', :js do
gitlab_enable_admin_mode_sign_in(admin)
end
- context 'with devops_adoption feature flag disabled' do
+ context 'without licensed feature devops adoption' do
before do
- stub_feature_flags(devops_adoption: false)
+ stub_licensed_features(devops_adoption: false)
end
it 'has dismissable intro callout' do
diff --git a/spec/features/admin/admin_hook_logs_spec.rb b/spec/features/admin/admin_hook_logs_spec.rb
index 6caf2b24555..a2ee6343886 100644
--- a/spec/features/admin/admin_hook_logs_spec.rb
+++ b/spec/features/admin/admin_hook_logs_spec.rb
@@ -3,12 +3,11 @@
require 'spec_helper'
RSpec.describe 'Admin::HookLogs' do
- let(:project) { create(:project) }
- let(:system_hook) { create(:system_hook) }
- let(:hook_log) { create(:web_hook_log, web_hook: system_hook, internal_error_message: 'some error') }
+ let_it_be(:system_hook) { create(:system_hook) }
+ let_it_be(:hook_log) { create(:web_hook_log, web_hook: system_hook, internal_error_message: 'some error') }
+ let_it_be(:admin) { create(:admin) }
before do
- admin = create(:admin)
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 901315752d6..dc5b0ae009e 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Admin::Hooks' do
include Spec::Support::Helpers::ModalHelpers
- let(:user) { create(:admin) }
+ let_it_be(:user) { create(:admin) }
before do
sign_in(user)
diff --git a/spec/features/admin/admin_mode/workers_spec.rb b/spec/features/admin/admin_mode/workers_spec.rb
index 12f5e20e176..8405e9132b6 100644
--- a/spec/features/admin/admin_mode/workers_spec.rb
+++ b/spec/features/admin/admin_mode/workers_spec.rb
@@ -37,56 +37,26 @@ RSpec.describe 'Admin mode for workers', :request_store do
gitlab_enable_admin_mode_sign_in(user)
end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'can delete user', :js do
- visit admin_user_path(user_to_delete)
-
- click_action_in_user_dropdown(user_to_delete.id, 'Delete user')
-
- page.within '.modal-dialog' do
- find("input[name='username']").send_keys(user_to_delete.name)
- click_button 'Delete user'
-
- wait_for_requests
- end
-
- expect(page).to have_content('The user is being deleted.')
-
- # Perform jobs while logged out so that admin mode is only enabled in job metadata
- execute_jobs_signed_out(user)
+ it 'can delete user', :js do
+ visit admin_user_path(user_to_delete)
- visit admin_user_path(user_to_delete)
+ click_action_in_user_dropdown(user_to_delete.id, 'Delete user')
- expect(find('h1.page-title')).to have_content('(Blocked)')
- end
- end
+ page.within '.modal-dialog' do
+ find("input[name='username']").send_keys(user_to_delete.name)
+ click_button 'Delete user'
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ wait_for_requests
end
- it 'can delete user', :js do
- visit admin_user_path(user_to_delete)
-
- click_action_in_user_dropdown(user_to_delete.id, 'Delete user')
-
- page.within '.modal-dialog' do
- find("input[name='username']").send_keys(user_to_delete.name)
- click_button 'Delete user'
-
- wait_for_requests
- end
+ expect(page).to have_content('The user is being deleted.')
- expect(page).to have_content('The user is being deleted.')
+ # Perform jobs while logged out so that admin mode is only enabled in job metadata
+ execute_jobs_signed_out(user)
- # Perform jobs while logged out so that admin mode is only enabled in job metadata
- execute_jobs_signed_out(user)
-
- visit admin_user_path(user_to_delete)
+ visit admin_user_path(user_to_delete)
- expect(page).to have_title('Not Found')
- end
+ expect(find('h1.page-title')).to have_content('(Blocked)')
end
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 35e57213bdb..92a3b388994 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -72,19 +72,8 @@ RSpec.describe "Admin Runners" do
expect(page).to have_text "#{s_('Runners|Stale')} 1"
end
- describe 'delete all runners in bulk' do
- before do
- check s_('Runners|Select all')
- click_button s_('Runners|Delete selected')
-
- within_modal do
- click_on 'Permanently delete 3 runners'
- end
-
- wait_for_requests
- end
-
- it_behaves_like 'shows no runners registered'
+ it_behaves_like 'deletes runners in bulk' do
+ let(:runner_count) { '3' }
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 94c5f397670..72c9053ba49 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -205,6 +205,22 @@ RSpec.describe 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
end
+
+ context 'Email confirmation settings' do
+ it "is set to 'hard' by default" do
+ expect(current_settings.email_confirmation_setting).to eq('off')
+ end
+
+ it 'changes the setting', :js do
+ page.within('.as-signup') do
+ choose 'Hard'
+ click_button 'Save changes'
+ end
+
+ expect(current_settings.email_confirmation_setting).to eq('hard')
+ expect(page).to have_content "Application settings saved successfully"
+ end
+ end
end
it 'change Sign-in restrictions' do
@@ -304,10 +320,12 @@ RSpec.describe 'Admin updates settings' do
it 'changes the setting' do
page.within('#js-jira_connect-settings') do
fill_in 'Jira Connect Application ID', with: '1234'
+ fill_in 'Jira Connect Proxy URL', with: 'https://example.com'
click_button 'Save changes'
end
expect(current_settings.jira_connect_application_key).to eq('1234')
+ expect(current_settings.jira_connect_proxy_url).to eq('https://example.com')
expect(page).to have_content "Application settings saved successfully"
end
end
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 45dccf9921f..d93dac4834e 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -4,18 +4,11 @@ require 'spec_helper'
RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
include Spec::Support::Helpers::ModalHelpers
+ include Spec::Support::Helpers::AccessTokenHelpers
let(:admin) { create(:admin) }
let!(:user) { create(:user) }
- def active_impersonation_tokens
- find("[data-testid='active-tokens']")
- end
-
- def created_impersonation_token
- find_field('new-access-token').value
- end
-
before do
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
@@ -39,12 +32,12 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
click_on "Create impersonation token"
- expect(active_impersonation_tokens).to have_text(name)
- expect(active_impersonation_tokens).to have_text('in')
- expect(active_impersonation_tokens).to have_text('read_api')
- expect(active_impersonation_tokens).to have_text('read_user')
+ expect(active_access_tokens).to have_text(name)
+ expect(active_access_tokens).to have_text('in')
+ expect(active_access_tokens).to have_text('read_api')
+ expect(active_access_tokens).to have_text('read_user')
expect(PersonalAccessTokensFinder.new(impersonation: true).execute.count).to equal(1)
- expect(created_impersonation_token).not_to be_empty
+ expect(created_access_token).to match(/[\w-]{20}/)
end
end
@@ -55,16 +48,16 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
it 'only shows impersonation tokens' do
visit admin_user_impersonation_tokens_path(user_id: user.username)
- expect(active_impersonation_tokens).to have_text(impersonation_token.name)
- expect(active_impersonation_tokens).not_to have_text(personal_access_token.name)
- expect(active_impersonation_tokens).to have_text('in')
+ expect(active_access_tokens).to have_text(impersonation_token.name)
+ expect(active_access_tokens).not_to have_text(personal_access_token.name)
+ expect(active_access_tokens).to have_text('in')
end
it 'shows absolute times' do
admin.update!(time_display_relative: false)
visit admin_user_impersonation_tokens_path(user_id: user.username)
- expect(active_impersonation_tokens).to have_text(personal_access_token.expires_at.strftime('%b %-d'))
+ expect(active_access_tokens).to have_text(personal_access_token.expires_at.strftime('%b %-d'))
end
end
@@ -76,7 +69,7 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
accept_gl_confirm(button_text: 'Revoke') { click_on "Revoke" }
- expect(active_impersonation_tokens).to have_text("This user has no active impersonation tokens.")
+ expect(active_access_tokens).to have_text("This user has no active impersonation tokens.")
end
it "removes expired tokens from 'active' section" do
@@ -84,7 +77,7 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
visit admin_user_impersonation_tokens_path(user_id: user.username)
- expect(active_impersonation_tokens).to have_text("This user has no active impersonation tokens.")
+ expect(active_access_tokens).to have_text("This user has no active impersonation tokens.")
end
end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index 86acf5a05d4..35b5c755b66 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -150,13 +150,32 @@ RSpec.describe 'Admin::Users::User' do
context 'before impersonating' do
subject { visit admin_user_path(user_to_visit) }
- let(:user_to_visit) { another_user }
+ let_it_be(:user_to_visit) { another_user }
+
+ shared_examples "user that cannot be impersonated" do
+ it 'disables impersonate button' do
+ subject
+
+ impersonate_btn = find('[data-testid="impersonate_user_link"]')
+
+ expect(impersonate_btn).not_to be_nil
+ expect(impersonate_btn['disabled']).not_to be_nil
+ end
+
+ it "shows tooltip with correct error message" do
+ subject
+
+ expect(find("span[title='#{impersonation_error_msg}']")).not_to be_nil
+ end
+ end
context 'for other users' do
it 'shows impersonate button for other users' do
subject
expect(page).to have_content('Impersonate')
+ impersonate_btn = find('[data-testid="impersonate_user_link"]')
+ expect(impersonate_btn['disabled']).to be_nil
end
end
@@ -171,15 +190,51 @@ RSpec.describe 'Admin::Users::User' do
end
context 'for blocked user' do
- before do
- another_user.block
+ let_it_be(:blocked_user) { create(:user, :blocked) }
+ let(:user_to_visit) { blocked_user }
+ let(:impersonation_error_msg) { _('You cannot impersonate a blocked user') }
+
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'for user with expired password' do
+ let(:user_to_visit) do
+ another_user.update!(password_expires_at: Time.zone.now - 5.minutes)
+ another_user
end
- it 'does not show impersonate button for blocked user' do
- subject
+ let(:impersonation_error_msg) { _("You cannot impersonate a user with an expired password") }
- expect(page).not_to have_content('Impersonate')
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'for internal user' do
+ let_it_be(:internal_user) { create(:user, :bot) }
+ let(:user_to_visit) { internal_user }
+ let(:impersonation_error_msg) { _("You cannot impersonate an internal user") }
+
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'for locked user' do
+ let_it_be(:locked_user) { create(:user, :locked) }
+ let(:user_to_visit) { locked_user }
+ let(:impersonation_error_msg) { _("You cannot impersonate a user who cannot log in") }
+
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'when already impersonating another user' do
+ let_it_be(:admin_user) { create(:user, :admin) }
+ let(:impersonation_error_msg) { _("You are already impersonating another user") }
+
+ subject do
+ visit admin_user_path(admin_user)
+ click_link 'Impersonate'
+ visit admin_user_path(another_user)
end
+
+ it_behaves_like "user that cannot be impersonated"
end
context 'when impersonation is disabled' do
@@ -216,18 +271,6 @@ RSpec.describe 'Admin::Users::User' do
icon = first('[data-testid="incognito-icon"]')
expect(icon).not_to be nil
end
-
- context 'a user with an expired password' do
- before do
- another_user.update!(password_expires_at: Time.zone.now - 5.minutes)
- end
-
- it 'does not redirect to password change page' do
- subject
-
- expect(page).to have_current_path('/')
- end
- end
end
context 'ending impersonation' do
diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb
index 174d4567520..9ec22bbe948 100644
--- a/spec/features/admin_variables_spec.rb
+++ b/spec/features/admin_variables_spec.rb
@@ -12,23 +12,9 @@ RSpec.describe 'Instance variables', :js do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
+ visit page_path
wait_for_requests
end
- context 'with disabled ff `ci_variable_settings_graphql' do
- before do
- stub_feature_flags(ci_variable_settings_graphql: false)
- visit page_path
- end
-
- it_behaves_like 'variable list', isAdmin: true
- end
-
- context 'with enabled ff `ci_variable_settings_graphql' do
- before do
- visit page_path
- end
-
- it_behaves_like 'variable list', isAdmin: true
- end
+ it_behaves_like 'variable list', isAdmin: true
end
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index 2e4dc4a29fc..eab92de7e8a 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe 'Issue board filters', :js do
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:project_label) { create(:label, project: project, title: 'Label') }
- let_it_be(:milestone_1) { create(:milestone, project: project, due_date: 3.days.from_now ) }
- let_it_be(:milestone_2) { create(:milestone, project: project, due_date: Date.tomorrow ) }
+ let_it_be(:milestone_1) { create(:milestone, project: project, due_date: 3.days.from_now) }
+ let_it_be(:milestone_2) { create(:milestone, project: project, due_date: Date.tomorrow) }
let_it_be(:release) { create(:release, tag: 'v1.0', project: project, milestones: [milestone_1]) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project, milestones: [milestone_2]) }
let_it_be(:issue_1) { create(:issue, project: project, milestone: milestone_1, author: user) }
@@ -22,6 +22,7 @@ RSpec.describe 'Issue board filters', :js do
let(:filter_submit) { find('.gl-search-box-by-click-search-button') }
before do
+ stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index a09c9d258dc..fee9b5b378e 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'Project issue boards', :js do
context 'signed in user' do
before do
+ stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
project.add_maintainer(user2)
@@ -29,7 +30,7 @@ RSpec.describe 'Project issue boards', :js do
context 'no lists' do
before do
- visit_project_board_path_without_query_limit(project, board)
+ visit_project_board(project, board)
end
it 'creates default lists' do
@@ -73,7 +74,7 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) }
before do
- visit_project_board_path_without_query_limit(project, board)
+ visit_project_board(project, board)
end
it 'shows description tooltip on list title', :quarantine do
@@ -124,7 +125,7 @@ RSpec.describe 'Project issue boards', :js do
it 'infinite scrolls list' do
create_list(:labeled_issue, 30, project: project, labels: [planning])
- visit_project_board_path_without_query_limit(project, board)
+ visit_project_board(project, board)
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('38')
@@ -203,7 +204,7 @@ RSpec.describe 'Project issue boards', :js do
expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title)
# Make sure list positions are preserved after a reload
- visit_project_board_path_without_query_limit(project, board)
+ visit_project_board(project, board)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title)
@@ -215,15 +216,19 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
it 'changes position of list' do
- visit_project_board_path_without_query_limit(project, board)
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ visit_project_board(project, board)
+ end
drag(list_from_index: 0, list_to_index: 1, selector: '.board-header')
expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title)
- # Make sure list positions are preserved after a reload
- visit_project_board_path_without_query_limit(project, board)
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ # Make sure list positions are preserved after a reload
+ visit_project_board(project, board)
+ end
expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title)
@@ -234,7 +239,9 @@ RSpec.describe 'Project issue boards', :js do
selector = '.board:not(.is-ghost) .board-header'
expect(page).to have_selector(selector, text: development.title, count: 1)
- drag(list_from_index: 2, list_to_index: 1, selector: '.board-header', perform_drop: false)
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ drag(list_from_index: 2, list_to_index: 1, selector: '.board-header', perform_drop: false)
+ end
expect(page).to have_selector(selector, text: development.title, count: 1)
end
@@ -492,7 +499,7 @@ RSpec.describe 'Project issue boards', :js do
context 'keyboard shortcuts' do
before do
- visit_project_board_path_without_query_limit(project, board)
+ visit_project_board(project, board)
wait_for_requests
end
@@ -505,6 +512,7 @@ RSpec.describe 'Project issue boards', :js do
context 'signed out user' do
before do
+ stub_feature_flags(apollo_boards: false)
visit project_board_path(project, board)
wait_for_requests
end
@@ -526,6 +534,7 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:user_guest) { create(:user) }
before do
+ stub_feature_flags(apollo_boards: false)
project.add_guest(user_guest)
sign_in(user_guest)
visit project_board_path(project, board)
@@ -587,11 +596,9 @@ RSpec.describe 'Project issue boards', :js do
end
end
- def visit_project_board_path_without_query_limit(project, board)
- inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
- visit project_board_path(project, board)
+ def visit_project_board(project, board)
+ visit project_board_path(project, board)
- wait_for_requests
- end
+ wait_for_requests
end
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 57f2bf26752..a3dda3b9d2f 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'Issue Boards', :js do
let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
before do
+ stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 511233b50c0..12d91e9c5a8 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
+ stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 0e914ae19d1..2b2a412194a 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'Project issue boards sidebar', :js do
let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) }
before do
+ stub_feature_flags(apollo_boards: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/boards/user_adds_lists_to_board_spec.rb b/spec/features/boards/user_adds_lists_to_board_spec.rb
index 26c310a6f56..480a88a6b84 100644
--- a/spec/features/boards/user_adds_lists_to_board_spec.rb
+++ b/spec/features/boards/user_adds_lists_to_board_spec.rb
@@ -31,13 +31,15 @@ RSpec.describe 'User adds lists', :js do
with_them do
before do
+ stub_feature_flags(apollo_boards: false)
sign_in(user)
set_cookie('sidebar_collapsed', 'true')
- if board_type == :project
+ case board_type
+ when :project
visit project_board_path(project, project_board)
- elsif board_type == :group
+ when :group
visit group_board_path(group, group_board)
end
diff --git a/spec/features/boards/user_visits_board_spec.rb b/spec/features/boards/user_visits_board_spec.rb
index 7fe32557d6a..c386477fa9d 100644
--- a/spec/features/boards/user_visits_board_spec.rb
+++ b/spec/features/boards/user_visits_board_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe 'User visits issue boards', :js do
let_it_be(:label1) { create(:group_label, group: group, name: label_name1) }
let_it_be(:label2) { create(:group_label, group: group, name: label_name2) }
- let_it_be(:assignee) { create_default(:group_member, :maintainer, user: create(:user, username: assignee_username), group: group ).user }
+ let_it_be(:assignee) { create_default(:group_member, :maintainer, user: create(:user, username: assignee_username), group: group).user }
let_it_be(:milestone) { create_default(:milestone, project: project, start_date: Date.today - 1, due_date: 7.days.from_now) }
before_all do
@@ -44,6 +44,7 @@ RSpec.describe 'User visits issue boards', :js do
with_them do
before do
+ stub_feature_flags(apollo_boards: false)
visit board_path
wait_for_requests
@@ -59,6 +60,7 @@ RSpec.describe 'User visits issue boards', :js do
end
context "project boards" do
+ stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, project: project) }
let_it_be(:backlog_list) { create_default(:backlog_list, board: board) }
@@ -68,6 +70,7 @@ RSpec.describe 'User visits issue boards', :js do
end
context "group boards" do
+ stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, group: group) }
let_it_be(:backlog_list) { create_default(:backlog_list, board: board) }
diff --git a/spec/features/broadcast_messages_spec.rb b/spec/features/broadcast_messages_spec.rb
index f339d45671d..1fec68a1d98 100644
--- a/spec/features/broadcast_messages_spec.rb
+++ b/spec/features/broadcast_messages_spec.rb
@@ -31,7 +31,8 @@ RSpec.describe 'Broadcast Messages' do
expect(page).not_to have_content 'SampleMessage'
end
- it 'broadcast message is still hidden after refresh', :js do
+ it 'broadcast message is still hidden after refresh', :js,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347118' do
visit root_path
find('.js-dismiss-current-broadcast-notification').click
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index db841ffc627..97f820c1518 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -212,13 +212,13 @@ RSpec.describe 'Commits' do
end
context 'author is just a name' do
- let(:author) { "#{author_commit.author_name}" }
+ let(:author) { author_commit.author_name.to_s }
it_behaves_like 'show commits by author'
end
context 'author is just an email' do
- let(:author) { "#{author_commit.author_email}" }
+ let(:author) { author_commit.author_email.to_s }
it_behaves_like 'show commits by author'
end
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 488a4f84297..8de4c66c62f 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
end
- context "when there's value stream analytics data" do
+ context "when there's value stream analytics data", :sidekiq_inline do
# NOTE: in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68595 travel back
# 5 days in time before we create data for these specs, to mitigate some flakiness
# So setting the date range to be the last 2 days should skip past the existing data
@@ -103,7 +103,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
end
- it 'shows data on each stage', :sidekiq_might_not_need_inline, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338332' do
+ it 'shows data on each stage', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338332' do
expect_issue_to_be_present
click_stage('Plan')
@@ -207,11 +207,11 @@ RSpec.describe 'Value Stream Analytics', :js do
wait_for_requests
end
- it 'does not show the commit stats' do
+ it 'does not show the commit stats', :sidekiq_inline do
expect(page.find(metrics_selector)).not_to have_selector("#commits")
end
- it 'does not show restricted stages', :aggregate_failures do
+ it 'does not show restricted stages', :aggregate_failures, :sidekiq_inline do
expect(find(stage_table_selector)).to have_content(issue.title)
expect(page).to have_selector('.gl-path-nav-list-item', text: 'Issue')
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
index 875ae41c55d..de8858fa8fa 100644
--- a/spec/features/dashboard/datetime_on_tooltips_spec.rb
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe 'Tooltips on .timeago dates', :js do
context 'on the activity tab' do
before do
- Event.create!( project: project, author_id: user.id, action: :joined,
- updated_at: created_date, created_at: created_date)
+ Event.create!(project: project, author_id: user.id, action: :joined,
+ updated_at: created_date, created_at: created_date)
sign_in user
visit user_activity_path(user)
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 0b468854322..c132caa88c8 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -139,7 +139,7 @@ RSpec.describe 'Dashboard Projects' do
end
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
- let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha, ref: project.default_branch) }
before do
# Since the cache isn't updated when a new pipeline is created
@@ -151,7 +151,7 @@ RSpec.describe 'Dashboard Projects' do
it 'shows that the last pipeline passed' do
visit dashboard_projects_path
- page.within('.controls') do
+ page.within('[data-testid="project_controls"]') do
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).to have_css('.ci-status-link')
expect(page).to have_css('.ci-status-icon-success')
@@ -163,7 +163,7 @@ RSpec.describe 'Dashboard Projects' do
it 'does not show the pipeline status' do
visit dashboard_projects_path
- page.within('.controls') do
+ page.within('[data-testid="project_controls"]') do
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).not_to have_css('.ci-status-link')
expect(page).not_to have_css('.ci-status-icon-success')
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index 666bf3594de..2e63ec2d4f2 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -2,14 +2,13 @@
require 'spec_helper'
-RSpec.describe 'Global search' do
+RSpec.describe 'Global search', :js do
include AfterNextHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
before do
- stub_feature_flags(search_page_vertical_nav: false)
project.add_maintainer(user)
sign_in(user)
end
@@ -42,7 +41,7 @@ RSpec.describe 'Global search' do
end
end
- it 'closes the dropdown on blur', :js do
+ it 'closes the dropdown on blur' do
find('#search').click
fill_in 'search', with: "a"
@@ -59,7 +58,7 @@ RSpec.describe 'Global search' do
expect(page).to have_no_selector('#js-header-search')
end
- it 'focuses search input when shortcut "s" is pressed', :js do
+ it 'focuses search input when shortcut "s" is pressed' do
expect(page).not_to have_selector('#search:focus')
find('body').native.send_key('s')
@@ -74,7 +73,7 @@ RSpec.describe 'Global search' do
stub_feature_flags(new_header_search: true)
visit dashboard_projects_path
- # intialize javascript loaded input search input field
+ # initialize javascript loaded input search input field
find('#search').click
find('body').click
end
@@ -84,7 +83,7 @@ RSpec.describe 'Global search' do
expect(page).to have_selector('#js-header-search')
end
- it 'focuses search input when shortcut "s" is pressed', :js do
+ it 'focuses search input when shortcut "s" is pressed' do
expect(page).not_to have_selector('#search:focus')
find('body').native.send_key('s')
diff --git a/spec/features/graphql_known_operations_spec.rb b/spec/features/graphql_known_operations_spec.rb
index ef406f12902..80214307be3 100644
--- a/spec/features/graphql_known_operations_spec.rb
+++ b/spec/features/graphql_known_operations_spec.rb
@@ -24,6 +24,6 @@ RSpec.describe 'Graphql known operations', :js do
expect(known_operations).to include("searchProjects")
expect(known_operations.length).to be > 20
- expect(known_operations).to all( match(%r{^[a-z]+}i) )
+ expect(known_operations).to all(match(%r{^[a-z]+}i))
end
end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index ab24162ad5a..e2c659d7dfe 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -11,23 +11,9 @@ RSpec.describe 'Group variables', :js do
before do
group.add_owner(user)
gitlab_sign_in(user)
+ visit page_path
wait_for_requests
end
- context 'with disabled ff `ci_variable_settings_graphql' do
- before do
- stub_feature_flags(ci_variable_settings_graphql: false)
- visit page_path
- end
-
- it_behaves_like 'variable list'
- end
-
- context 'with enabled ff `ci_variable_settings_graphql' do
- before do
- visit page_path
- end
-
- it_behaves_like 'variable list'
- end
+ it_behaves_like 'variable list'
end
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
index 6ca69e76d33..5bac80959b1 100644
--- a/spec/features/groups/activity_spec.rb
+++ b/spec/features/groups/activity_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Group activity page' do
- let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user }
+ let(:user) { create(:group_member, :developer, user: create(:user), group: group).user }
let(:group) { create(:group) }
let(:path) { activity_group_path(group) }
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
index 69a6788e438..10ef28f3fbc 100644
--- a/spec/features/groups/board_sidebar_spec.rb
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Group Issue Boards', :js do
include BoardHelpers
let(:group) { create(:group) }
- let(:user) { create(:group_member, user: create(:user), group: group ).user }
+ let(:user) { create(:group_member, user: create(:user), group: group).user }
let!(:project_1) { create(:project, :public, group: group) }
let!(:project_2) { create(:project, :public, group: group) }
let!(:project_1_label) { create(:label, project: project_1, name: 'Development 1') }
@@ -19,6 +19,7 @@ RSpec.describe 'Group Issue Boards', :js do
let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do
+ stub_feature_flags(apollo_boards: false)
sign_in(user)
visit group_board_path(group, board)
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index 84882fc674e..f1a8f97461a 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Group empty states' do
let(:group) { create(:group) }
- let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user }
+ let(:user) { create(:group_member, :developer, user: create(:user), group: group).user }
before do
sign_in(user)
diff --git a/spec/features/groups/group_runners_spec.rb b/spec/features/groups/group_runners_spec.rb
index e9807c487d5..c9d1c69e9e1 100644
--- a/spec/features/groups/group_runners_spec.rb
+++ b/spec/features/groups/group_runners_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe "Group Runners" do
include Spec::Support::Helpers::Features::RunnersHelpers
+ include Spec::Support::Helpers::ModalHelpers
let_it_be(:group_owner) { create(:user) }
let_it_be(:group) { create(:group) }
@@ -157,6 +158,19 @@ RSpec.describe "Group Runners" do
end
end
+ context "with multiple runners" do
+ before do
+ create(:ci_runner, :group, groups: [group], description: 'runner-foo')
+ create(:ci_runner, :group, groups: [group], description: 'runner-bar')
+
+ visit group_runners_path(group)
+ end
+
+ it_behaves_like 'deletes runners in bulk' do
+ let(:runner_count) { '2' }
+ end
+ end
+
describe 'filtered search' do
before do
visit group_runners_path(group)
@@ -201,18 +215,32 @@ RSpec.describe "Group Runners" do
end
describe "Group runner edit page", :js do
- let!(:group_runner) do
- create(:ci_runner, :group, groups: [group])
- end
+ context 'when updating a group runner' do
+ let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
- before do
- visit edit_group_runner_path(group, group_runner)
- wait_for_requests
+ before do
+ visit edit_group_runner_path(group, group_runner)
+ wait_for_requests
+ end
+
+ it_behaves_like 'submits edit runner form' do
+ let(:runner) { group_runner }
+ let(:runner_page_path) { group_runner_path(group, group_runner) }
+ end
end
- it_behaves_like 'submits edit runner form' do
- let(:runner) { group_runner }
- let(:runner_page_path) { group_runner_path(group, group_runner) }
+ context 'when updating a project runner' do
+ let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+
+ before do
+ visit edit_group_runner_path(group, project_runner)
+ wait_for_requests
+ end
+
+ it_behaves_like 'submits edit runner form' do
+ let(:runner) { project_runner }
+ let(:runner_page_path) { group_runner_path(group, project_runner) }
+ end
end
end
end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 2f599d24b01..81ff0088e1e 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -150,13 +150,15 @@ RSpec.describe 'Edit group settings' do
it 'can successfully transfer the group' do
visit edit_group_path(selected_group)
- page.within('.js-group-transfer-form') do
- namespace_select.find('button').click
- namespace_select.find('.dropdown-menu p', text: target_group_name, match: :first).click
-
- click_button 'Transfer group'
+ page.within('[data-testid="transfer-locations-dropdown"]') do
+ click_button _('Select parent group')
+ fill_in _('Search'), with: target_group_name
+ wait_for_requests
+ click_button target_group_name
end
+ click_button s_('GroupSettings|Transfer group')
+
page.within(confirm_modal) do
expect(page).to have_text "You are going to transfer #{selected_group.name} to another namespace. Are you ABSOLUTELY sure?"
@@ -169,16 +171,16 @@ RSpec.describe 'Edit group settings' do
end
end
- context 'from a subgroup' do
+ context 'when transfering from a subgroup' do
let(:selected_group) { create(:group, path: 'foo-subgroup', parent: group) }
- context 'to no parent group' do
+ context 'when transfering to no parent group' do
let(:target_group_name) { 'No parent group' }
it_behaves_like 'can transfer the group'
end
- context 'to a different parent group' do
+ context 'when transfering to a parent group' do
let(:target_group) { create(:group, path: 'foo-parentgroup') }
let(:target_group_name) { target_group.name }
@@ -190,14 +192,11 @@ RSpec.describe 'Edit group settings' do
end
end
- context 'from a root group' do
+ context 'when transfering from a root group to a parent group' do
let(:selected_group) { create(:group, path: 'foo-rootgroup') }
+ let(:target_group_name) { group.name }
- context 'to a parent group' do
- let(:target_group_name) { group.name }
-
- it_behaves_like 'can transfer the group'
- end
+ it_behaves_like 'can transfer the group'
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index eec07c84cde..d4e88505118 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe 'Group issues page' do
context 'issues list', :js do
let(:subgroup) { create(:group, parent: group) }
let(:subgroup_project) { create(:project, :public, group: subgroup) }
- let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group).user }
let!(:issue) { create(:issue, project: project, title: 'root group issue') }
let!(:subgroup_issue) { create(:issue, project: subgroup_project, title: 'subgroup issue') }
@@ -111,7 +111,7 @@ RSpec.describe 'Group issues page' do
context 'projects with issues disabled' do
describe 'issue dropdown' do
- let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group).user }
before do
[project, project_with_issues_disabled].each { |project| project.add_maintainer(user_in_group) }
@@ -129,7 +129,7 @@ RSpec.describe 'Group issues page' do
end
context 'manual ordering', :js do
- let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group).user }
let!(:issue1) { create(:issue, project: project, title: 'Issue #1', relative_position: 1) }
let!(:issue2) { create(:issue, project: project, title: 'Issue #2', relative_position: 2) }
@@ -161,6 +161,8 @@ RSpec.describe 'Group issues page' do
visit issues_group_path(group)
select_manual_sort
+ wait_for_requests
+
drag_to(selector: '.manual-ordering', from_index: 0, to_index: 2)
expect_issue_order
@@ -176,6 +178,8 @@ RSpec.describe 'Group issues page' do
visit issues_group_path(group)
select_manual_sort
+ wait_for_requests
+
drag_to(selector: '.manual-ordering', from_index: 0, to_index: 2)
expect(page).to have_text 'An error occurred while reordering issues.'
@@ -195,7 +199,7 @@ RSpec.describe 'Group issues page' do
end
context 'issues pagination', :js do
- let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group).user }
let!(:issues) do
(1..25).to_a.map { |index| create(:issue, project: project, title: "Issue #{index}") }
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 42eaa8358a1..92a40459737 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Group milestones' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project_empty_repo, group: group) }
- let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
around do |example|
freeze_time { example.run }
diff --git a/spec/features/groups/milestones_sorting_spec.rb b/spec/features/groups/milestones_sorting_spec.rb
index 125bf9ce3a7..6f3fc72775f 100644
--- a/spec/features/groups/milestones_sorting_spec.rb
+++ b/spec/features/groups/milestones_sorting_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Milestones sorting', :js do
let!(:project_milestone2) { create(:milestone, project: project, title: 'v2.0', due_date: 5.days.from_now) }
let!(:other_project_milestone2) { create(:milestone, project: other_project, title: 'v2.0', due_date: 5.days.from_now) }
let!(:group_milestone) { create(:milestone, group: group, title: 'v3.0', due_date: 7.days.from_now) }
- let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
before do
sign_in(user)
diff --git a/spec/features/groups/settings/repository_spec.rb b/spec/features/groups/settings/repository_spec.rb
index f6b8bbdd35f..cd7dcbdb28d 100644
--- a/spec/features/groups/settings/repository_spec.rb
+++ b/spec/features/groups/settings/repository_spec.rb
@@ -23,26 +23,9 @@ RSpec.describe 'Group Repository settings', :js do
stub_container_registry_config(enabled: true)
end
- context 'when ajax deploy tokens is enabled' do
- before do
- stub_feature_flags(ajax_new_deploy_token: true)
- end
-
- it_behaves_like 'a deploy token in settings' do
- let(:entity_type) { 'group' }
- let(:page_path) { group_settings_repository_path(group) }
- end
- end
-
- context 'when ajax deploy tokens is disabled' do
- before do
- stub_feature_flags(ajax_new_deploy_token: false)
- end
-
- it_behaves_like 'a deploy token in settings' do
- let(:entity_type) { 'group' }
- let(:page_path) { group_settings_repository_path(group) }
- end
+ it_behaves_like 'a deploy token in settings' do
+ let(:entity_type) { 'group' }
+ let(:page_path) { group_settings_repository_path(group) }
end
end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 546257b9f10..eef48d09f32 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'Help Pages' do
end
it 'renders the version check badge' do
- expect(page).to have_selector('.js-gitlab-version-check')
+ expect(page).to have_selector('.js-gitlab-version-check-badge')
end
end
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index 8a95d7c5544..4ffa5212970 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -21,6 +21,6 @@ RSpec.describe 'IDE merge request', :js do
wait_for_requests
- expect(page).to have_selector('.monaco-diff-editor')
+ expect(page).not_to have_selector('.monaco-diff-editor')
end
end
diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb
index c7c740c2293..1f6d34efc0f 100644
--- a/spec/features/ide_spec.rb
+++ b/spec/features/ide_spec.rb
@@ -3,47 +3,90 @@
require 'spec_helper'
RSpec.describe 'IDE', :js do
- describe 'sub-groups' do
- let(:ide_iframe_selector) { '#ide iframe' }
- let(:user) { create(:user) }
- let(:group) { create(:group) }
- let(:subgroup) { create(:group, parent: group) }
- let(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
+ include WebIdeSpecHelpers
+
+ let_it_be(:ide_iframe_selector) { '#ide iframe' }
+ let_it_be(:normal_project) { create(:project, :repository) }
+
+ let(:project) { normal_project }
+ let(:vscode_ff) { false }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(vscode_web_ide: vscode_ff)
+
+ sign_in(user)
+ end
+
+ shared_examples "legacy Web IDE" do
+ it 'loads legacy Web IDE', :aggregate_failures do
+ expect(page).to have_selector('.context-header', text: project.name)
+
+ # Assert new Web IDE is not loaded
+ expect(page).not_to have_selector(ide_iframe_selector)
+ end
+ end
+
+ shared_examples "new Web IDE" do
+ it 'loads new Web IDE', :aggregate_failures do
+ expect(page).not_to have_selector('.context-header')
+
+ iframe = find(ide_iframe_selector)
+
+ page.within_frame(iframe) do
+ expect(page).to have_selector('.title', text: project.name.upcase)
+ end
+ end
+ end
+ context 'with vscode feature flag off' do
before do
- stub_feature_flags(vscode_web_ide: vscode_ff)
- subgroup_project.add_maintainer(user)
- sign_in(user)
+ ide_visit(project)
+ end
- visit project_path(subgroup_project)
+ it_behaves_like 'legacy Web IDE'
- click_link('Web IDE')
+ it 'does not show switch button' do
+ expect(page).not_to have_button('Switch to new Web IDE')
+ end
+ end
+
+ context 'with vscode feature flag on and use_legacy_web_ide=true' do
+ let(:vscode_ff) { true }
+ let(:user) { create(:user, use_legacy_web_ide: true) }
- wait_for_requests
+ before do
+ ide_visit(project)
end
- context 'with vscode feature flag on' do
- let(:vscode_ff) { true }
+ it_behaves_like 'legacy Web IDE'
- it 'loads project in Web IDE' do
- iframe = find(ide_iframe_selector)
+ describe 'when user switches to new Web IDE' do
+ before do
+ click_button('Switch to new Web IDE')
- page.within_frame(iframe) do
- expect(page).to have_selector('.title', text: subgroup_project.name.upcase)
+ # Confirm modal
+ page.within('#confirmationModal') do
+ click_button('Switch editors')
end
end
+
+ it_behaves_like 'new Web IDE'
end
+ end
- context 'with vscode feature flag off' do
- let(:vscode_ff) { false }
+ describe 'sub-groups' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:subgroup_project) { create(:project, :repository, namespace: subgroup) }
- it 'loads project in legacy Web IDE' do
- expect(page).to have_selector('.context-header', text: subgroup_project.name)
- end
+ let(:project) { subgroup_project }
- it 'does not load new Web IDE' do
- expect(page).not_to have_selector(ide_iframe_selector)
- end
+ before do
+ ide_visit(project)
end
+
+ it_behaves_like 'legacy Web IDE'
end
end
diff --git a/spec/features/incidents/user_views_incident_spec.rb b/spec/features/incidents/user_views_incident_spec.rb
index a669966502e..054a084ea9c 100644
--- a/spec/features/incidents/user_views_incident_spec.rb
+++ b/spec/features/incidents/user_views_incident_spec.rb
@@ -4,12 +4,16 @@ require "spec_helper"
RSpec.describe "User views incident" do
let_it_be(:project) { create(:project_empty_repo, :public) }
- let_it_be(:user) { create(:user) }
- let_it_be(:incident) { create(:incident, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) }
- let_it_be(:note) { create(:note, noteable: incident, project: project, author: user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:user) { developer }
+ let(:author) { developer }
+ let(:description) { "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)" }
+ let(:incident) { create(:incident, project: project, description: description, author: author) }
before_all do
- project.add_developer(user)
+ project.add_developer(developer)
+ project.add_guest(guest)
end
before do
@@ -18,57 +22,61 @@ RSpec.describe "User views incident" do
visit(project_issues_incident_path(project, incident))
end
- it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") }
+ specify do
+ expect(page).to have_header_with_correct_id_and_link(1, 'Description header', 'description-header')
+ end
it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
describe 'user actions' do
it 'shows the merge request and incident actions', :js, :aggregate_failures do
+ expected_href = new_project_issue_path(project,
+ issuable_template: 'incident',
+ issue: { issue_type: 'incident' },
+ add_related_issue: incident.iid)
+
click_button 'Incident actions'
- expect(page).to have_link('New related incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' }, add_related_issue: incident.iid }))
+ expect(page).to have_link('New related incident', href: expected_href)
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident')
end
- context 'when user is a guest' do
- before do
- project.add_guest(user)
+ context 'when user is guest' do
+ let(:user) { guest }
- login_as(user)
+ context 'and author' do
+ let(:author) { guest }
- visit(project_issues_incident_path(project, incident))
+ it 'does not show the incident actions', :js do
+ expect(page).not_to have_button('Incident actions')
+ end
end
- it 'does not show the incident actions', :js, :aggregate_failures do
- expect(page).not_to have_button('Incident actions')
+ context 'and not author' do
+ it 'shows incident actions', :js do
+ click_button 'Incident actions'
+
+ expect(page).to have_link 'Report abuse'
+ end
end
end
end
context 'when the project is archived' do
- before do
+ before_all do
project.update!(archived: true)
- visit(project_issues_incident_path(project, incident))
end
- it 'hides the merge request and incident actions', :aggregate_failures do
- expect(page).not_to have_link('New incident')
- expect(page).not_to have_button('Create merge request')
- expect(page).not_to have_link('Close incident')
+ it 'does not show the incident actions', :js do
+ expect(page).not_to have_button('Incident actions')
end
end
describe 'user status' do
- subject { visit(project_issues_incident_path(project, incident)) }
-
context 'when showing status of the author of the incident' do
- it_behaves_like 'showing user status' do
- let(:user_with_status) { user }
- end
- end
+ subject { visit(project_issues_incident_path(project, incident)) }
- context 'when showing status of a user who commented on an incident', :js do
it_behaves_like 'showing user status' do
let(:user_with_status) { user }
end
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index ab7c0ce2891..c5a8e9f367c 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -21,15 +21,17 @@ RSpec.describe "Internal references", :js do
sign_in(private_project_user)
visit(project_issue_path(private_project, private_project_issue))
+ wait_for_requests
add_note("##{public_project_issue.to_reference(private_project)}")
end
- context "when user doesn't have access to private project", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/257832' do
+ context "when user doesn't have access to private project" do
before do
sign_in(public_project_user)
visit(project_issue_path(public_project, public_project_issue))
+ wait_for_requests
end
it { expect(page).not_to have_css(".note") }
@@ -41,6 +43,7 @@ RSpec.describe "Internal references", :js do
sign_in(private_project_user)
visit(project_merge_request_path(private_project, private_project_merge_request))
+ wait_for_requests
add_note("##{public_project_issue.to_reference(private_project)}")
end
@@ -50,9 +53,10 @@ RSpec.describe "Internal references", :js do
sign_in(public_project_user)
visit(project_issue_path(public_project, public_project_issue))
+ wait_for_requests
end
- it "doesn't show any references", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/257832' do
+ it "doesn't show any references" do
expect(page).not_to have_text 'Related merge requests'
end
end
@@ -60,6 +64,7 @@ RSpec.describe "Internal references", :js do
context "when user has access to private project" do
before do
visit(project_issue_path(public_project, public_project_issue))
+ wait_for_requests
end
it "shows references", :sidekiq_might_not_need_inline do
@@ -85,15 +90,17 @@ RSpec.describe "Internal references", :js do
sign_in(private_project_user)
visit(project_issue_path(private_project, private_project_issue))
+ wait_for_requests
add_note("##{public_project_merge_request.to_reference(private_project)}")
end
- context "when user doesn't have access to private project", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/257832' do
+ context "when user doesn't have access to private project" do
before do
sign_in(public_project_user)
visit(project_merge_request_path(public_project, public_project_merge_request))
+ wait_for_requests
end
it { expect(page).not_to have_css(".note") }
@@ -105,6 +112,7 @@ RSpec.describe "Internal references", :js do
sign_in(private_project_user)
visit(project_merge_request_path(private_project, private_project_merge_request))
+ wait_for_requests
add_note("##{public_project_merge_request.to_reference(private_project)}")
end
@@ -114,9 +122,10 @@ RSpec.describe "Internal references", :js do
sign_in(public_project_user)
visit(project_merge_request_path(public_project, public_project_merge_request))
+ wait_for_requests
end
- it "doesn't show any references", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/257832' do
+ it "doesn't show any references" do
expect(page).not_to have_text 'Related merge requests'
end
end
@@ -124,6 +133,7 @@ RSpec.describe "Internal references", :js do
context "when user has access to private project" do
before do
visit(project_merge_request_path(public_project, public_project_merge_request))
+ wait_for_requests
end
it "shows references", :sidekiq_might_not_need_inline do
diff --git a/spec/features/issues/confidential_notes_spec.rb b/spec/features/issues/confidential_notes_spec.rb
new file mode 100644
index 00000000000..858c054c803
--- /dev/null
+++ b/spec/features/issues/confidential_notes_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "Confidential notes on issues", :js do
+ it_behaves_like 'confidential notes on issuables' do
+ let_it_be(:issuable_parent) { create(:project) }
+ let_it_be(:issuable) { create(:issue, project: issuable_parent) }
+ let_it_be(:user) { create(:user) }
+
+ let(:issuable_path) { project_issue_path(issuable_parent, issuable) }
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 05eb656461e..40b0bfd9aa4 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe 'Dropdown assignee', :js do
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
+ before do
+ stub_feature_flags(or_issuable_queries: false)
+ end
+
describe 'behavior' do
before do
project.add_maintainer(user)
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 36a8f1f3902..a67d114c6d1 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe 'Dropdown author', :js do
let_it_be(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index dcbab308efa..cbe917931aa 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe 'Dropdown hint', :js do
let_it_be(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 8d96bbc38cb..e48df1b1c53 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe 'Filter issues', :js do
end
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_maintainer(user)
create(:issue, project: project, author: user2, title: "Bug report 1")
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index c44181a60e4..854b88c3f81 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'Visual tokens', :js do
let_it_be(:issue) { create(:issue, project: project) }
before do
+ stub_feature_flags(or_issuable_queries: false)
project.add_member(user, :maintainer)
project.add_member(user_rock, :maintainer)
sign_in(user)
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index e749c555dcf..fe591d7fe3a 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -140,14 +140,10 @@ RSpec.describe 'New/edit issue', :js do
end
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
- click_button 'Milestone'
- page.within '.issue-milestone' do
- click_link milestone.title
- end
+ click_button 'Select milestone'
+ click_button milestone.title
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
- page.within '.js-milestone-select' do
- expect(page).to have_content milestone.title
- end
+ expect(page).to have_button milestone.title
click_button 'Labels'
page.within '.dropdown-menu-labels' do
@@ -307,14 +303,11 @@ RSpec.describe 'New/edit issue', :js do
end
it 'escapes milestone' do
- click_button 'Milestone'
+ click_button 'Select milestone'
+ click_button milestone.title
page.within '.issue-milestone' do
- click_link milestone.title
- end
-
- page.within '.js-milestone-select' do
- expect(page).to have_content milestone.title
+ expect(page).to have_button milestone.title
expect(page).not_to have_selector 'img'
end
end
@@ -444,9 +437,7 @@ RSpec.describe 'New/edit issue', :js do
expect(page).to have_content user.name
end
- page.within '.js-milestone-select' do
- expect(page).to have_content milestone.title
- end
+ expect(page).to have_button milestone.title
click_button 'Labels'
page.within '.dropdown-menu-labels' do
diff --git a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
index 4837d13574c..2a201e0bc23 100644
--- a/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_labels_spec.rb
@@ -417,7 +417,7 @@ RSpec.describe 'Issues > Labels bulk assignment' do
click_button 'Select milestone'
wait_for_requests
items.map do |item|
- click_link item
+ click_button item
end
end
diff --git a/spec/features/issues/user_bulk_edits_issues_spec.rb b/spec/features/issues/user_bulk_edits_issues_spec.rb
index 1ef2918adec..d7fad355cb4 100644
--- a/spec/features/issues/user_bulk_edits_issues_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do
click_button 'Edit issues'
check 'Select all'
click_button 'Select milestone'
- click_link milestone.title
+ click_button milestone.title
click_update_issues_button
expect(page.find('.issue')).to have_content milestone.title
@@ -97,7 +97,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do
click_button 'Edit issues'
check 'Select all'
click_button 'Select milestone'
- click_link 'No milestone'
+ click_button 'No milestone'
click_update_issues_button
expect(find('.issue:first-of-type')).not_to have_text milestone.title
diff --git a/spec/features/issues/user_comments_on_issue_spec.rb b/spec/features/issues/user_comments_on_issue_spec.rb
index a1e7c007b90..ef00e66af7e 100644
--- a/spec/features/issues/user_comments_on_issue_spec.rb
+++ b/spec/features/issues/user_comments_on_issue_spec.rb
@@ -5,9 +5,9 @@ require "spec_helper"
RSpec.describe "User comments on issue", :js do
include Spec::Support::Helpers::Features::NotesHelpers
- let(:project) { create(:project_empty_repo, :public) }
- let(:issue) { create(:issue, project: project) }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:user) { create(:user) }
before do
project.add_guest(user)
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index b96490bd7e7..1d023a15159 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -188,7 +188,7 @@ RSpec.describe "User creates issue" do
end
it 'does not hide the milestone select' do
- expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
+ expect(page).to have_button 'Select milestone'
end
end
@@ -204,7 +204,7 @@ RSpec.describe "User creates issue" do
end
it 'shows the milestone select' do
- expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
+ expect(page).to have_button 'Select milestone'
end
it 'hides the incident help text' do
@@ -265,7 +265,7 @@ RSpec.describe "User creates issue" do
end
it 'shows the milestone select' do
- expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
+ expect(page).to have_button 'Select milestone'
end
it 'hides the weight input' do
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 4eecb63c47e..75df85f362f 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -101,6 +101,35 @@ RSpec.describe "Issues > User edits issue", :js do
visit project_issue_path(project, issue)
end
+ describe 'edit description' do
+ def click_edit_issue_description
+ click_on 'Edit title and description'
+ end
+
+ it 'places focus on the web editor' do
+ toggle_editing_mode_selector = '[data-testid="toggle-editing-mode-button"] label'
+ content_editor_focused_selector = '[data-testid="content-editor"].is-focused'
+ markdown_field_focused_selector = 'textarea:focus'
+ click_edit_issue_description
+
+ expect(page).to have_selector(markdown_field_focused_selector)
+
+ find(toggle_editing_mode_selector, text: 'Rich text').click
+
+ expect(page).not_to have_selector(content_editor_focused_selector)
+
+ refresh
+
+ click_edit_issue_description
+
+ expect(page).to have_selector(content_editor_focused_selector)
+
+ find(toggle_editing_mode_selector, text: 'Source').click
+
+ expect(page).not_to have_selector(markdown_field_focused_selector)
+ end
+ end
+
describe 'update labels' do
it 'will not send ajax request when no data is changed' do
page.within '.labels' do
@@ -186,7 +215,7 @@ RSpec.describe "Issues > User edits issue", :js do
visit project_issue_path(project, issue)
page.within('.assignee') do
- expect(page).to have_content "#{user.name}"
+ expect(page).to have_content user.name.to_s
click_link 'Edit'
click_link 'Unassigned'
@@ -261,7 +290,7 @@ RSpec.describe "Issues > User edits issue", :js do
visit project_issue_path(project, issue)
page.within('.assignee') do
- expect(page).to have_content "#{user.name}"
+ expect(page).to have_content user.name.to_s
click_button('Edit')
wait_for_requests
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index 47b28b88108..a2dea7f048b 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -209,22 +209,25 @@ RSpec.describe 'User interacts with awards' do
it 'adds award to issue' do
first('[data-testid="award-button"]').click
-
+ wait_for_requests
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)
+ wait_for_requests
expect(first('[data-testid="award-button"]')).to have_content '1'
end
it 'removes award from issue', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375241' do
first('[data-testid="award-button"]').click
+ wait_for_requests
find('[data-testid="award-button"].selected').click
-
+ wait_for_requests
expect(first('[data-testid="award-button"]')).to have_content '0'
visit project_issue_path(project, issue)
+ wait_for_requests
expect(first('[data-testid="award-button"]')).to have_content '0'
end
diff --git a/spec/features/issues/user_sees_empty_state_spec.rb b/spec/features/issues/user_sees_empty_state_spec.rb
index 0e2a7cb4358..b4c5a57de4f 100644
--- a/spec/features/issues/user_sees_empty_state_spec.rb
+++ b/spec/features/issues/user_sees_empty_state_spec.rb
@@ -22,9 +22,9 @@ RSpec.describe 'Issues > User sees empty state', :js do
it 'user sees empty state' do
visit project_issues_path(project)
+ expect(page).to have_content('Use issues to collaborate on ideas, solve problems, and plan work')
+ expect(page).to have_content('Learn more about issues.')
expect(page).to have_content('Register / Sign In')
- expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project.')
- expect(page).to have_content('You can register or sign in to create issues for this project.')
end
it_behaves_like 'empty state with filters'
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 7add6c782f7..2716d742be3 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe "User sorts issues" do
sign_in(user)
end
- it 'keeps the sort option', :js do
+ it 'keeps the sort option', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/378184' do
visit(project_issues_path(project))
click_button 'Created date'
diff --git a/spec/features/jira_connect/subscriptions_spec.rb b/spec/features/jira_connect/subscriptions_spec.rb
index 94c293c88b9..0468cfd70fc 100644
--- a/spec/features/jira_connect/subscriptions_spec.rb
+++ b/spec/features/jira_connect/subscriptions_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Subscriptions Content Security Policy' do
p.style_src :self, 'https://some-cdn.test'
end
- setup_existing_csp_for_controller(JiraConnect::SubscriptionsController, csp)
+ setup_csp_for_controller(JiraConnect::SubscriptionsController, csp)
end
it 'appends to CSP directives' do
diff --git a/spec/features/markdown/sandboxed_mermaid_spec.rb b/spec/features/markdown/sandboxed_mermaid_spec.rb
index 05fe83b3107..2bf88d7882d 100644
--- a/spec/features/markdown/sandboxed_mermaid_spec.rb
+++ b/spec/features/markdown/sandboxed_mermaid_spec.rb
@@ -3,30 +3,54 @@
require 'spec_helper'
RSpec.describe 'Sandboxed Mermaid rendering', :js do
- let_it_be(:project) { create(:project, :public) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:description) do
+ <<~MERMAID
+ ```mermaid
+ graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+ ```
+ MERMAID
+ end
- before do
- stub_feature_flags(sandboxed_mermaid: true)
+ let_it_be(:expected) do
+ %(<iframe src="/-/sandbox/mermaid" sandbox="allow-scripts allow-popups" frameborder="0" scrolling="no")
end
- it 'includes mermaid frame correctly' do
- description = <<~MERMAID
- ```mermaid
- graph TD;
- A-->B;
- A-->C;
- B-->D;
- C-->D;
- ```
- MERMAID
+ context 'in an issue' do
+ let(:issue) { create(:issue, project: project, description: description) }
+
+ it 'includes mermaid frame correctly' do
+ visit project_issue_path(project, issue)
+
+ wait_for_requests
+
+ expect(page.html).to include(expected)
+ end
+ end
+
+ context 'in a merge request' do
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, description: description) }
+
+ it 'renders diffs and includes mermaid frame correctly' do
+ visit(diffs_project_merge_request_path(project, merge_request))
+
+ wait_for_requests
- issue = create(:issue, project: project, description: description)
+ page.within('.tab-content') do
+ expect(page).to have_selector('.diffs')
+ end
- visit project_issue_path(project, issue)
+ visit(project_merge_request_path(project, merge_request))
- wait_for_requests
+ wait_for_requests
- expected = %(<iframe src="/-/sandbox/mermaid" sandbox="allow-scripts allow-popups" frameborder="0" scrolling="no")
- expect(page.html).to include(expected)
+ page.within('.merge-request') do
+ expect(page.html).to include(expected)
+ end
+ end
end
end
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index 159306b28d8..b50e6779e07 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -18,6 +18,8 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
click_button('Merge')
+ puts merge_request.short_merged_commit_sha
+
expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
end
diff --git a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
index 0dd87ac3e24..59b5923b2a1 100644
--- a/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
+++ b/spec/features/merge_request/user_edits_assignees_sidebar_spec.rb
@@ -88,7 +88,7 @@ RSpec.describe 'Merge request > User edits assignees sidebar', :js do
end
context 'when GraphQL assignees widget feature flag is enabled' do
- let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-item", text: assignee.username ) }
+ let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-item", text: assignee.username) }
let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item['title'] }
context 'when user is an owner' do
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index 21f96299958..abf916c72b3 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -151,7 +151,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-section-container') do
+ page.within('.mr-state-widget') do
expect(page).to have_content('Something went wrong. Try again.')
end
end
@@ -170,7 +170,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-section-container') do
+ page.within('.mr-state-widget') do
expect(page).to have_content('Something went wrong. Try again.')
end
end
diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
index 60ea168940a..cf4875a7a25 100644
--- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
+++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Merge request > User scrolls to note on load', :js do
wait_for_all_requests
- expect(page).to have_selector("#{fragment_id}")
+ expect(page).to have_selector(fragment_id.to_s)
page_scroll_y = page.evaluate_script("window.scrollY")
fragment_position_top = page.evaluate_script("Math.round(document.querySelector('#{fragment_id}').getBoundingClientRect().top + window.pageYOffset)")
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 63ac7862b06..6f8ecf5f5c2 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -18,6 +18,11 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
let(:build) { create(:ci_build, :with_deployment, environment: environment.name, pipeline: pipeline) }
let!(:deployment) { build.deployment }
+ def assert_env_widget(text, env_name)
+ expect(find('.js-deploy-env-name')[:title]).to have_text(env_name)
+ expect(page).to have_content(text)
+ end
+
before do
merge_request.update!(merge_commit_sha: sha)
project.add_member(user, role)
@@ -33,7 +38,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_content("Deployed to #{environment.name}")
+ assert_env_widget("Deployed to", environment.name)
expect(find('.js-deploy-time')['title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
end
@@ -47,8 +52,8 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
wait_for_requests
expect(page).to have_selector('.js-deployment-info', count: 1)
- expect(page).to have_content("#{environment.name}")
- expect(page).not_to have_content("#{environment2.name}")
+ expect(find('.js-deploy-env-name')[:title]).to have_text(environment.name)
+ expect(find('.js-deploy-env-name')[:title]).not_to have_text(environment2.name)
end
end
end
@@ -62,7 +67,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_content("Failed to deploy to #{environment.name}")
+ assert_env_widget("Failed to deploy to", environment.name)
expect(page).not_to have_css('.js-deploy-time')
end
end
@@ -76,7 +81,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_content("Deploying to #{environment.name}")
+ assert_env_widget("Deploying to", environment.name)
expect(page).not_to have_css('.js-deploy-time')
end
end
@@ -89,7 +94,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_content("Will deploy to #{environment.name}")
+ assert_env_widget("Will deploy to", environment.name)
expect(page).not_to have_css('.js-deploy-time')
end
end
@@ -103,7 +108,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_content("Canceled deployment to #{environment.name}")
+ assert_env_widget("Canceled deployment to", environment.name)
expect(page).not_to have_css('.js-deploy-time')
end
end
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index 2e65183d26f..0bae019793c 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -38,6 +38,20 @@ RSpec.describe 'Merge request > User sees diff', :js do
end
end
+ context 'when linking to a line' do
+ let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request }
+ let(:line) { note.diff_file.highlighted_diff_lines.last }
+ let(:line_code) { line.line_code }
+
+ before do
+ visit "#{diffs_project_merge_request_path(project, merge_request)}##{line_code}"
+ end
+
+ it 'shows the linked line' do
+ expect(page).to have_selector("[id='#{line_code}']", visible: true, obscured: false)
+ end
+ end
+
context 'when merge request has overflow' do
it 'displays warning' do
allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
new file mode 100644
index 00000000000..9fbe7662fc0
--- /dev/null
+++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > User sees discussions navigation', :js do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { project.creator }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ # FIXME: before removing this please fix discussions navigation with this flag enabled
+ stub_feature_flags(moved_mr_sidebar: false)
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'Code discussions' do
+ let!(:position) do
+ build(:text_diff_position, :added,
+ file: "files/images/wm.svg",
+ new_line: 1,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ let!(:first_discussion) do
+ create(:diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: position
+ ).to_discussion
+ end
+
+ let!(:second_discussion) do
+ create(:diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: position
+ ).to_discussion
+ end
+
+ let(:first_discussion_selector) { ".discussion[data-discussion-id='#{first_discussion.id}']" }
+ let(:second_discussion_selector) { ".discussion[data-discussion-id='#{second_discussion.id}']" }
+
+ shared_examples 'a page with a thread navigation' do
+ context 'with active threads' do
+ it 'navigates to the first thread' do
+ goto_next_thread
+ expect(page).to have_selector(first_discussion_selector, obscured: false)
+ end
+
+ it 'navigates to the last thread' do
+ goto_previous_thread
+ expect(page).to have_selector(second_discussion_selector, obscured: false)
+ end
+
+ it 'navigates through active threads' do
+ goto_next_thread
+ goto_next_thread
+ expect(page).to have_selector(second_discussion_selector, obscured: false)
+ end
+
+ it 'cycles back to the first thread' do
+ goto_next_thread
+ goto_next_thread
+ goto_next_thread
+ expect(page).to have_selector(first_discussion_selector, obscured: false)
+ end
+
+ it 'cycles back to the last thread' do
+ goto_previous_thread
+ goto_previous_thread
+ goto_previous_thread
+ expect(page).to have_selector(second_discussion_selector, obscured: false)
+ end
+ end
+
+ context 'with resolved threads' do
+ let!(:resolved_discussion) do
+ create(:diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: position
+ ).to_discussion
+ end
+
+ let(:resolved_discussion_selector) { ".discussion[data-discussion-id='#{resolved_discussion.id}']" }
+
+ before do
+ # :resolved attr doesn't actually resolve the thread but just collapses it
+ page.within(resolved_discussion_selector) do
+ click_button text: 'Resolve thread'
+ end
+ page.execute_script("window.scrollTo(0,0)")
+ end
+
+ it 'excludes resolved threads during navigation' do
+ goto_next_thread
+ goto_next_thread
+ goto_next_thread
+ expect(page).to have_selector(first_discussion_selector, obscured: false)
+ end
+ end
+ end
+
+ describe "Overview page discussions navigation" do
+ before do
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it_behaves_like 'a page with a thread navigation'
+
+ context 'with collapsed threads' do
+ before do
+ page.within(first_discussion_selector) do
+ click_button 'Hide thread'
+ end
+ end
+
+ it 'expands threads during navigation' do
+ goto_next_thread
+ expect(page).to have_selector "#note_#{first_discussion.first_note.id}"
+ end
+ end
+ end
+
+ describe "Changes page discussions navigation" do
+ before do
+ visit diffs_project_merge_request_path(project, merge_request)
+ end
+
+ it_behaves_like 'a page with a thread navigation'
+ end
+ end
+
+ describe 'Merge request discussions' do
+ shared_examples 'a page with no code discussions' do
+ let!(:first_discussion) do
+ create(:discussion_note_on_merge_request,
+ noteable: merge_request,
+ project: project
+ ).to_discussion
+ end
+
+ let!(:second_discussion) do
+ create(:discussion_note_on_merge_request,
+ noteable: merge_request,
+ project: project
+ ).to_discussion
+ end
+
+ let(:first_discussion_selector) { ".discussion[data-discussion-id='#{first_discussion.id}']" }
+ let(:second_discussion_selector) { ".discussion[data-discussion-id='#{second_discussion.id}']" }
+
+ describe "Changes page discussions navigation" do
+ it 'navigates to the first discussion on the Overview page' do
+ goto_next_thread
+ expect(page).to have_selector(first_discussion_selector, obscured: false)
+ end
+
+ it 'navigates to the last discussion on the Overview page' do
+ goto_previous_thread
+ expect(page).to have_selector(second_discussion_selector, obscured: false)
+ end
+ end
+ end
+
+ context 'on changes page' do
+ before do
+ visit diffs_project_merge_request_path(project, merge_request)
+ end
+
+ it_behaves_like 'a page with no code discussions'
+ end
+
+ context 'on commits page' do
+ before do
+ # we can't go directly to the commits page since it doesn't load discussions
+ visit project_merge_request_path(project, merge_request)
+ click_link 'Commits'
+ end
+
+ it_behaves_like 'a page with no code discussions'
+ end
+
+ context 'on pipelines page' do
+ before do
+ visit project_merge_request_path(project, merge_request)
+ click_link 'Pipelines'
+ end
+
+ it_behaves_like 'a page with no code discussions'
+ end
+ end
+
+ def goto_next_thread
+ begin
+ # this is required when moved_mr_sidebar is enabled
+ page.within('.issue-sticky-header') do
+ click_button 'Go to next unresolved thread'
+ end
+ rescue StandardError
+ click_button 'Go to next unresolved thread'
+ end
+ wait_for_scroll_end
+ end
+
+ def goto_previous_thread
+ begin
+ page.within('.issue-sticky-header') do
+ click_button 'Go to previous unresolved thread'
+ end
+ rescue StandardError
+ click_button 'Go to previous unresolved thread'
+ end
+ wait_for_scroll_end
+ end
+
+ def wait_for_scroll_end
+ sleep(1)
+ end
+end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 77ac6fac22f..c4a29c1fb07 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -14,15 +14,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
let(:merge_request_in_only_mwps_project) { create(:merge_request, source_project: project_only_mwps) }
def click_expand_button
- find('[data-testid="report-section-expand-button"]').click
+ find('[data-testid="toggle-button"]').click
end
before do
project.add_maintainer(user)
project_only_mwps.add_maintainer(user)
sign_in(user)
-
- stub_feature_flags(refactor_mr_widget_test_summary: false)
end
context 'new merge request', :sidekiq_might_not_need_inline do
@@ -64,7 +62,8 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
wait_for_requests
page.within('.js-pre-deployment') do
- expect(page).to have_content("Deployed to #{environment.name}")
+ expect(find('.js-deploy-env-name')[:title]).to have_text(environment.name)
+ expect(page).to have_content("Deployed to")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end
end
@@ -343,7 +342,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-section-container') do
+ page.within('.mr-state-widget') do
expect(page).to have_content('Something went wrong.')
end
end
@@ -364,7 +363,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-section-container') do
+ page.within('.mr-state-widget') do
expect(page).to have_content('Something went wrong.')
end
end
@@ -399,9 +398,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
it 'updates the MR widget', :sidekiq_might_not_need_inline do
click_button 'Merge'
- page.within('.mr-widget-body') do
- expect(page).to have_content('An error occurred while merging')
- end
+ expect(page).to have_content('An error occurred while merging')
end
end
@@ -531,7 +528,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows parsing status' do
- expect(page).to have_content('Test summary results are being parsed')
+ expect(page).to have_content('Test summary results are loading')
end
end
@@ -546,7 +543,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows parsed results' do
- expect(page).to have_content('Test summary contained')
+ expect(page).to have_content('Test summary:')
end
end
@@ -560,7 +557,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows the error state' do
- expect(page).to have_content('Test summary failed loading results')
+ expect(page).to have_content('Test summary failed to load results')
end
end
@@ -607,13 +604,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows test reports summary which includes the new failure' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- expect(page).to have_content('Test summary contained 1 failed out of 2 total tests')
- within(".js-report-section-container") do
- expect(page).to have_content('rspec found no changed test results out of 1 total test')
- expect(page).to have_content('junit found 1 failed out of 1 total test')
+ expect(page).to have_content('Test summary: 1 failed, 2 total tests')
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ expect(page).to have_content('rspec: no changed test results, 1 total test')
+ expect(page).to have_content('junit: 1 failed, 1 total test')
expect(page).to have_content('New')
expect(page).to have_content('addTest')
end
@@ -622,15 +619,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the new failure' do
it 'shows the test report detail' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- within(".js-report-section-container") do
- click_button 'addTest'
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ click_link 'addTest'
end
end
- within("#modal-mrwidget-reports") do
+ within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest')
expect(page).to have_content('6.66')
expect(page).to have_content(sample_java_failed_message.gsub(/\s+/, ' ').strip)
@@ -655,13 +652,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows test reports summary which includes the existing failure' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- expect(page).to have_content('Test summary contained 1 failed out of 2 total tests')
- within(".js-report-section-container") do
- expect(page).to have_content('rspec found 1 failed out of 1 total test')
- expect(page).to have_content('junit found no changed test results out of 1 total test')
+ expect(page).to have_content('Test summary: 1 failed, 2 total tests')
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ expect(page).to have_content('rspec: 1 failed, 1 total test')
+ expect(page).to have_content('junit: no changed test results, 1 total test')
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary')
end
end
@@ -669,15 +666,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the existing failure' do
it 'shows test report detail of it' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- within(".js-report-section-container") do
- click_button 'Test#sum when a is 1 and b is 3 returns summary'
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ click_link 'Test#sum when a is 1 and b is 3 returns summary'
end
end
- within("#modal-mrwidget-reports") do
+ within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary')
expect(page).to have_content('2.22')
expect(page).to have_content(sample_rspec_failed_message.gsub(/\s+/, ' ').strip)
@@ -702,13 +699,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows test reports summary which includes the resolved failure' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- expect(page).to have_content('Test summary contained 1 fixed test result out of 2 total tests')
- within(".js-report-section-container") do
- expect(page).to have_content('rspec found no changed test results out of 1 total test')
- expect(page).to have_content('junit found 1 fixed test result out of 1 total test')
+ expect(page).to have_content('Test summary: 1 fixed test result, 2 total tests')
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ expect(page).to have_content('rspec: no changed test results, 1 total test')
+ expect(page).to have_content('junit: 1 fixed test result, 1 total test')
+ expect(page).to have_content('Fixed')
expect(page).to have_content('addTest')
end
end
@@ -716,15 +714,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the resolved failure' do
it 'shows test report detail of it' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- within(".js-report-section-container") do
- click_button 'addTest'
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ click_link 'addTest'
end
end
- within("#modal-mrwidget-reports") do
+ within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest')
expect(page).to have_content('5.55')
end
@@ -748,13 +746,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows test reports summary which includes the new error' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- expect(page).to have_content('Test summary contained 1 error out of 2 total tests')
- within(".js-report-section-container") do
- expect(page).to have_content('rspec found no changed test results out of 1 total test')
- expect(page).to have_content('junit found 1 error out of 1 total test')
+ expect(page).to have_content('Test summary: 1 error, 2 total tests')
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ expect(page).to have_content('rspec: no changed test results, 1 total test')
+ expect(page).to have_content('junit: 1 error, 1 total test')
expect(page).to have_content('New')
expect(page).to have_content('addTest')
end
@@ -763,15 +761,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the new error' do
it 'shows the test report detail' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- within(".js-report-section-container") do
- click_button 'addTest'
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ click_link 'addTest'
end
end
- within("#modal-mrwidget-reports") do
+ within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest')
expect(page).to have_content('8.88')
end
@@ -795,13 +793,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows test reports summary which includes the existing error' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- expect(page).to have_content('Test summary contained 1 error out of 2 total tests')
- within(".js-report-section-container") do
- expect(page).to have_content('rspec found 1 error out of 1 total test')
- expect(page).to have_content('junit found no changed test results out of 1 total test')
+ expect(page).to have_content('Test summary: 1 error, 2 total tests')
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ expect(page).to have_content('rspec: 1 error, 1 total test')
+ expect(page).to have_content('junit: no changed test results, 1 total test')
expect(page).to have_content('Test#sum when a is 4 and b is 4 returns summary')
end
end
@@ -809,15 +807,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the existing error' do
it 'shows test report detail of it' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- within(".js-report-section-container") do
- click_button 'Test#sum when a is 4 and b is 4 returns summary'
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ click_link 'Test#sum when a is 4 and b is 4 returns summary'
end
end
- within("#modal-mrwidget-reports") do
+ within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('Test#sum when a is 4 and b is 4 returns summary')
expect(page).to have_content('4.44')
end
@@ -841,13 +839,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows test reports summary which includes the resolved error' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- expect(page).to have_content('Test summary contained 1 fixed test result out of 2 total tests')
- within(".js-report-section-container") do
- expect(page).to have_content('rspec found no changed test results out of 1 total test')
- expect(page).to have_content('junit found 1 fixed test result out of 1 total test')
+ expect(page).to have_content('Test summary: 1 fixed test result, 2 total tests')
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ expect(page).to have_content('rspec: no changed test results, 1 total test')
+ expect(page).to have_content('junit: 1 fixed test result, 1 total test')
+ expect(page).to have_content('Fixed')
expect(page).to have_content('addTest')
end
end
@@ -855,15 +854,15 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
context 'when user clicks the resolved error' do
it 'shows test report detail of it' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- within(".js-report-section-container") do
- click_button 'addTest'
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ click_link 'addTest'
end
end
- within("#modal-mrwidget-reports") do
+ within('[data-testid="test-case-details-modal"]') do
expect(page).to have_content('addTest')
expect(page).to have_content('5.55')
end
@@ -895,13 +894,13 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
it 'shows test reports summary which includes the resolved failure' do
- within(".js-reports-container") do
+ within('[data-testid="widget-extension"]') do
click_expand_button
- expect(page).to have_content('Test summary contained 20 failed out of 20 total tests')
- within(".js-report-section-container") do
- expect(page).to have_content('rspec found 10 failed out of 10 total tests')
- expect(page).to have_content('junit found 10 failed out of 10 total tests')
+ expect(page).to have_content('Test summary: 20 failed, 20 total tests')
+ within('[data-testid="widget-extension-collapsed-section"]') do
+ expect(page).to have_content('rspec: 10 failed, 10 total tests')
+ expect(page).to have_content('junit: 10 failed, 10 total tests')
expect(page).to have_content('Test#sum when a is 1 and b is 3 returns summary', count: 2)
end
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index cf9760bcd7f..5c3cb098e28 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -130,7 +130,7 @@ RSpec.describe 'Merge requests > User mass updates', :js do
click_button 'Edit merge requests'
check 'Select all'
click_button 'Select milestone'
- click_link text
+ click_button text
click_update_merge_requests_button
end
diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb
index f612956600f..4f529179522 100644
--- a/spec/features/monitor_sidebar_link_spec.rb
+++ b/spec/features/monitor_sidebar_link_spec.rb
@@ -64,7 +64,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).not_to have_link('Metrics', href: project_metrics_dashboard_path(project))
expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Product Analytics', href: project_product_analytics_path(project))
expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
end
@@ -119,7 +118,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).not_to have_link('Metrics', href: project_metrics_dashboard_path(project))
expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).not_to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).not_to have_link('Product Analytics', href: project_product_analytics_path(project))
expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
end
@@ -135,7 +133,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).to have_link('Incidents', href: project_incidents_path(project))
expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).to have_link('Product Analytics', href: project_product_analytics_path(project))
expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project))
expect(page).not_to have_link('Kubernetes', href: project_clusters_path(project))
@@ -154,7 +151,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).to have_link('Incidents', href: project_incidents_path(project))
expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).to have_link('Product Analytics', href: project_product_analytics_path(project))
expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
end
@@ -171,7 +167,6 @@ RSpec.describe 'Monitor dropdown sidebar', :aggregate_failures do
expect(page).to have_link('Incidents', href: project_incidents_path(project))
expect(page).to have_link('Environments', href: project_environments_path(project))
expect(page).to have_link('Error Tracking', href: project_error_tracking_index_path(project))
- expect(page).to have_link('Product Analytics', href: project_product_analytics_path(project))
expect(page).to have_link('Kubernetes', href: project_clusters_path(project))
end
diff --git a/spec/features/nav/top_nav_tooltip_spec.rb b/spec/features/nav/top_nav_tooltip_spec.rb
index 73e4571e7a2..a110c6cfecf 100644
--- a/spec/features/nav/top_nav_tooltip_spec.rb
+++ b/spec/features/nav/top_nav_tooltip_spec.rb
@@ -10,7 +10,8 @@ RSpec.describe 'top nav tooltips', :js do
visit explore_projects_path
end
- it 'clicking new dropdown hides tooltip', :aggregate_failures do
+ it 'clicking new dropdown hides tooltip', :aggregate_failures,
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/382786' do
btn = '#js-onboarding-new-project-link'
page.find(btn).hover
diff --git a/spec/features/one_trust_spec.rb b/spec/features/one_trust_spec.rb
index 0ed08e8b99b..a7dfbfd6bdf 100644
--- a/spec/features/one_trust_spec.rb
+++ b/spec/features/one_trust_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'OneTrust' do
end
it 'has the OneTrust CSP settings', :aggregate_failures do
- expect(response_headers['Content-Security-Policy']).to include("#{onetrust_url}")
+ expect(response_headers['Content-Security-Policy']).to include(onetrust_url.to_s)
expect(page.html).to include("https://cdn.cookielaw.org/consent/#{one_trust_id}/OtAutoBlock.js")
end
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 913c375f909..ca156642bc8 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -27,41 +27,20 @@ RSpec.describe 'Profile account page', :js do
expect(User.exists?(user.id)).to be_truthy
end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'deletes user', :js, :sidekiq_inline do
- click_button 'Delete account'
-
- fill_in 'password', with: user.password
+ it 'deletes user', :js, :sidekiq_inline do
+ click_button 'Delete account'
- page.within '.modal' do
- click_button 'Delete account'
- end
+ fill_in 'password', with: user.password
- expect(page).to have_content('Account scheduled for removal')
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: user)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'deletes user', :js, :sidekiq_inline do
+ page.within '.modal' do
click_button 'Delete account'
-
- fill_in 'password', with: user.password
-
- page.within '.modal' do
- click_button 'Delete account'
- end
-
- expect(page).to have_content('Account scheduled for removal')
- expect(User.exists?(user.id)).to be_falsy
end
+
+ expect(page).to have_content('Account scheduled for removal')
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: user)
+ ).to be_exists
end
it 'shows invalid password flash message', :js do
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 6c860740354..8887ff1746d 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -152,6 +152,33 @@ RSpec.describe 'Profile > Password' do
it_behaves_like 'user enters an incorrect current password'
end
+ context 'when the password is too weak' do
+ let(:new_password) { 'password' }
+
+ subject do
+ page.within '.update-password' do
+ fill_in "user_password", with: user.password
+ fill_passwords(new_password, new_password)
+ end
+ end
+
+ it 'tracks the error and does not change the password', :aggregate_failures do
+ expect { subject }.not_to change { user.reload.valid_password?(new_password) }
+ expect(user.failed_attempts).to eq(0)
+
+ page.within '.gl-alert-danger' do
+ expect(page).to have_content('must not contain commonly used combinations of words and letters')
+ end
+
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'Profiles::PasswordsController',
+ method: 'update'
+ )
+ end
+ end
+
context 'when the password reset is successful' do
subject do
page.within '.update-password' do
@@ -195,6 +222,23 @@ RSpec.describe 'Profile > Password' do
expect(page).to have_current_path new_user_session_path, ignore_query: true
end
+ it 'tracks weak password error' do
+ visit edit_profile_password_path
+
+ expect(page).to have_current_path new_profile_password_path, ignore_query: true
+
+ fill_in :user_password, with: user.password
+ fill_in :user_new_password, with: "password"
+ fill_in :user_password_confirmation, with: "password"
+ click_button 'Set new password'
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'Profiles::PasswordsController',
+ method: 'create'
+ )
+ end
+
context 'when global require_two_factor_authentication is enabled' do
it 'needs change user password' do
stub_application_setting(require_two_factor_authentication: true)
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 088c8a7a15a..3ae88da06f6 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -4,22 +4,11 @@ require 'spec_helper'
RSpec.describe 'Profile > Personal Access Tokens', :js do
include Spec::Support::Helpers::ModalHelpers
+ include Spec::Support::Helpers::AccessTokenHelpers
let(:user) { create(:user) }
let(:pat_create_service) { double('PersonalAccessTokens::CreateService', execute: ServiceResponse.error(message: 'error', payload: { personal_access_token: PersonalAccessToken.new })) }
- def active_personal_access_tokens
- find("[data-testid='active-tokens']")
- end
-
- def created_personal_access_token
- find_field('new-access-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
-
before do
sign_in(user)
end
@@ -43,11 +32,11 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
click_on "Create personal access token"
wait_for_all_requests
- expect(active_personal_access_tokens).to have_text(name)
- expect(active_personal_access_tokens).to have_text('in')
- expect(active_personal_access_tokens).to have_text('read_api')
- expect(active_personal_access_tokens).to have_text('read_user')
- expect(created_personal_access_token).not_to be_empty
+ expect(active_access_tokens).to have_text(name)
+ expect(active_access_tokens).to have_text('in')
+ expect(active_access_tokens).to have_text('read_api')
+ expect(active_access_tokens).to have_text('read_user')
+ expect(created_access_token).to match(/[\w-]{20}/)
end
context "when creation fails" do
@@ -73,8 +62,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
it 'only shows personal access tokens' do
visit profile_personal_access_tokens_path
- expect(active_personal_access_tokens).to have_text(personal_access_token.name)
- expect(active_personal_access_tokens).not_to have_text(impersonation_token.name)
+ expect(active_access_tokens).to have_text(personal_access_token.name)
+ expect(active_access_tokens).not_to have_text(impersonation_token.name)
end
context 'when User#time_display_relative is false' do
@@ -85,7 +74,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
it 'shows absolute times for expires_at' do
visit profile_personal_access_tokens_path
- expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
+ expect(active_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
end
end
end
@@ -97,14 +86,14 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
visit profile_personal_access_tokens_path
accept_gl_confirm(button_text: 'Revoke') { click_on "Revoke" }
- expect(active_personal_access_tokens).to have_text("This user has no active personal access tokens.")
+ expect(active_access_tokens).to have_text("This user has no active personal access tokens.")
end
it "removes expired tokens from 'active' section" do
personal_access_token.update!(expires_at: 5.days.ago)
visit profile_personal_access_tokens_path
- expect(active_personal_access_tokens).to have_text("This user has no active personal access tokens.")
+ expect(active_access_tokens).to have_text("This user has no active personal access tokens.")
end
context "when revocation fails" do
@@ -115,12 +104,16 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
visit profile_personal_access_tokens_path
accept_gl_confirm(button_text: "Revoke") { click_on "Revoke" }
- expect(active_personal_access_tokens).to have_text(personal_access_token.name)
+ expect(active_access_tokens).to have_text(personal_access_token.name)
end
end
end
describe "feed token" do
+ 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
+
context "when enabled" do
it "displays feed token" do
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
diff --git a/spec/features/profiles/two_factor_auths_spec.rb b/spec/features/profiles/two_factor_auths_spec.rb
index b4355f2d669..decc2904b6e 100644
--- a/spec/features/profiles/two_factor_auths_spec.rb
+++ b/spec/features/profiles/two_factor_auths_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe 'Two factor auths' do
end
context 'when user has two-factor authentication disabled' do
- let_it_be(:user) { create(:user ) }
+ let_it_be(:user) { create(:user) }
it 'requires the current password to set up two factor authentication', :js do
visit profile_two_factor_auth_path
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index d3bedbf3a75..33b4af3b5aa 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -12,62 +12,29 @@ RSpec.describe 'Project variables', :js do
sign_in(user)
project.add_maintainer(user)
project.variables << variable
+ visit page_path
+ wait_for_requests
end
- context 'with disabled ff `ci_variable_settings_graphql' do
- before do
- stub_feature_flags(ci_variable_settings_graphql: false)
- visit page_path
- end
-
- it_behaves_like 'variable list'
-
- it 'adds a new variable with an environment scope' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- fill_in 'Key', with: 'akey'
- find('#ci-variable-value').set('akey_value')
- find('[data-testid="environment-scope"]').click
- find('[data-testid="ci-environment-search"]').set('review/*')
- find('[data-testid="create-wildcard-button"]').click
-
- click_button('Add variable')
- end
-
- wait_for_requests
+ it_behaves_like 'variable list'
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
- end
- end
- end
-
- context 'with enabled ff `ci_variable_settings_graphql' do
- before do
- visit page_path
- end
+ it 'adds a new variable with an environment scope' do
+ click_button('Add variable')
- it_behaves_like 'variable list'
+ page.within('#add-ci-variable') do
+ fill_in 'Key', with: 'akey'
+ find('#ci-variable-value').set('akey_value')
+ find('[data-testid="environment-scope"]').click
+ find('[data-testid="ci-environment-search"]').set('review/*')
+ find('[data-testid="create-wildcard-button"]').click
- it 'adds a new variable with an environment scope' do
click_button('Add variable')
+ end
- page.within('#add-ci-variable') do
- fill_in 'Key', with: 'akey'
- find('#ci-variable-value').set('akey_value')
- find('[data-testid="environment-scope"]').click
- find('[data-testid="ci-environment-search"]').set('review/*')
- find('[data-testid="create-wildcard-button"]').click
-
- click_button('Add variable')
- end
-
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
- end
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
end
end
end
diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb
index b6b6dcb5cf1..3f0614532f1 100644
--- a/spec/features/projects/branches/user_views_branches_spec.rb
+++ b/spec/features/projects/branches/user_views_branches_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe "User views branches", :js do
it "shows branches" do
expect(page).to have_content("Branches").and have_content("master")
- expect(page.all(".graph-side")).to all( have_content(/\d+/) )
+ 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
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 361a07ebd0b..ecf6349e431 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -7,7 +7,20 @@ RSpec.describe 'Branches' do
let_it_be(:project) { create(:project, :public, :repository) }
let(:repository) { project.repository }
- context 'logged in as developer' do
+ context 'when logged in as reporter' do
+ before do
+ sign_in(user)
+ project.add_reporter(user)
+ end
+
+ it 'does not show delete button' do
+ visit project_branches_path(project)
+
+ expect(page).not_to have_css '.js-delete-branch-button'
+ end
+ end
+
+ context 'when logged in as developer' do
before do
sign_in(user)
project.add_developer(user)
@@ -21,11 +34,15 @@ RSpec.describe 'Branches' do
before do
# Add 4 stale branches
(1..4).reverse_each do |i|
- travel_to((threshold + i.hours).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") }
+ travel_to((threshold + i.hours).ago) do
+ create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}")
+ end
end
# Add 6 active branches
(1..6).each do |i|
- travel_to((threshold - i.hours).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") }
+ travel_to((threshold - i.hours).ago) do
+ create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}")
+ end
end
end
@@ -38,7 +55,10 @@ RSpec.describe 'Branches' do
expect(page).to have_button('Copy branch name')
- expect(page).to have_link('Show more active branches', href: project_branches_filtered_path(project, state: 'active'))
+ expect(page).to have_link(
+ 'Show more active branches',
+ href: project_branches_filtered_path(project, state: 'active')
+ )
expect(page).not_to have_content('Show more stale branches')
end
end
@@ -75,13 +95,15 @@ RSpec.describe 'Branches' do
it 'shows only default_per_page active branches sorted by last updated' do
visit project_branches_filtered_path(project, state: 'active')
- expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page, sort_by: :updated_desc, state: 'active'))
+ expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page,
+ sort_by: :updated_desc, state: 'active'))
end
it 'shows only default_per_page branches sorted by last updated on All branches' do
visit project_branches_filtered_path(project, state: 'all')
- expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page, sort_by: :updated_desc))
+ expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page,
+ sort_by: :updated_desc))
end
end
end
@@ -141,7 +163,7 @@ RSpec.describe 'Branches' do
it 'avoids a N+1 query in branches index' do
control_count = ActiveRecord::QueryRecorder.new { visit project_branches_path(project) }.count
- %w(one two three four five).each { |ref| repository.add_branch(user, ref, 'master') }
+ %w[one two three four five].each { |ref| repository.add_branch(user, ref, 'master') }
expect { visit project_branches_filtered_path(project, state: 'all') }.not_to exceed_query_limit(control_count)
end
@@ -193,7 +215,7 @@ RSpec.describe 'Branches' do
end
end
- context 'logged in as maintainer' do
+ context 'when logged in as maintainer' do
before do
sign_in(user)
project.add_maintainer(user)
@@ -220,7 +242,7 @@ RSpec.describe 'Branches' do
end
end
- context 'logged out' do
+ context 'when logged out' do
before do
visit project_branches_path(project)
end
@@ -314,7 +336,7 @@ RSpec.describe 'Branches' do
Regexp.new(sorted_branches.join('.*'))
end
- def create_file(message: 'message', branch_name:)
+ def create_file(branch_name:, message: 'message')
repository.create_file(user, generate(:branch), 'content', message: message, branch_name: branch_name)
end
diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb
index 54685441300..e99af734c43 100644
--- a/spec/features/projects/container_registry_spec.rb
+++ b/spec/features/projects/container_registry_spec.rb
@@ -56,10 +56,11 @@ RSpec.describe 'Container Registry', :js do
expect(page).to have_content 'my/image'
end
- it 'user removes entire container repository', :sidekiq_might_not_need_inline do
+ it 'user removes entire container repository' do
visit_container_registry
- expect_any_instance_of(ContainerRepository).to receive(:delete_tags!).and_return(true)
+ expect_any_instance_of(ContainerRepository).to receive(:delete_scheduled!).and_call_original
+ expect(DeleteContainerRepositoryWorker).not_to receive(:perform_async)
find('[title="Remove repository"]').click
expect(find('.modal .modal-title')).to have_content _('Remove repository')
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index dc6e496d081..706c880d097 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Environment' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:role) { :developer }
@@ -17,7 +17,7 @@ RSpec.describe 'Environment' do
end
describe 'environment details page' do
- let!(:environment) { create(:environment, project: project) }
+ let_it_be(:environment) { create(:environment, project: project) }
let!(:permissions) {}
let!(:deployment) {}
let!(:action) {}
@@ -160,10 +160,20 @@ RSpec.describe 'Environment' do
end
context 'with related deployable present' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) }
+ let_it_be(:previous_pipeline) { create(:ci_pipeline, project: project) }
- let(:deployment) do
+ let_it_be(:previous_build) do
+ create(:ci_build, :success, pipeline: previous_pipeline, environment: environment.name)
+ end
+
+ let_it_be(:previous_deployment) do
+ create(:deployment, :success, environment: environment, deployable: previous_build)
+ end
+
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) }
+
+ let_it_be(:deployment) do
create(:deployment, :success, environment: environment, deployable: build)
end
@@ -171,12 +181,10 @@ RSpec.describe 'Environment' do
visit_environment(environment)
end
- it 'does show build name' do
- expect(page).to have_link("#{build.name} (##{build.id})")
- end
-
- it 'shows the re-deploy button' do
+ it 'shows deployment information and buttons', :js do
expect(page).to have_button('Re-deploy to environment')
+ expect(page).to have_button('Rollback environment')
+ expect(page).to have_link("#{build.name} (##{build.id})")
end
context 'with manual action' do
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 9ec41cd8f8d..b445b0da901 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -336,6 +336,11 @@ RSpec.describe 'Environments page', :js do
accept_gl_confirm do
find(action_link_selector).click
end
+
+ # Wait for UI to transition to ensure we an GraphQL request has been made
+ within(actions_button_selector) { find('.gl-spinner') }
+ within(actions_button_selector) { find('[data-testid="play-icon"]') }
+
wait_for_requests
end
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 24943e7dd0f..9ceadb63178 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -134,7 +134,7 @@ RSpec.describe 'Project fork' do
context 'fork form', :js do
let(:group) { create(:group) }
let(:group2) { create(:group) }
- let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let(:user) { create(:group_member, :maintainer, user: create(:user), group: group).user }
def submit_form(group_obj = group)
find('[data-testid="select_namespace_dropdown"]').click
@@ -180,7 +180,7 @@ RSpec.describe 'Project fork' do
context 'with cache_home_panel feature flag' do
before do
- create(:group_member, :maintainer, user: user, group: group2 )
+ create(:group_member, :maintainer, user: user, group: group2)
end
context 'when caching is enabled' do
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index c7fbaa85483..6f015f9cd22 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'Import/Export - project import integration test', :js do
- include GitHelpers
-
let(:user) { create(:user) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
diff --git a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
index e7d4ed58549..d2c48cb2af0 100644
--- a/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/integrations/user_activates_issue_tracker_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User activates issue tracker', :js do
+RSpec.describe 'User activates issue tracker', :js, feature_category: :integrations do
include_context 'project integration activation'
let(:url) { 'http://tracker.example.com' }
diff --git a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
index b423543dc33..3d40bae8544 100644
--- a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
+++ b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe 'viewing an issue with cross project references' do
let(:issue) do
create(:issue,
project: project,
- description: description_referencing_other_issue )
+ description: description_referencing_other_issue)
end
let(:confidential_issue) do
@@ -102,8 +102,8 @@ RSpec.describe 'viewing an issue with cross project references' do
it 'shows only the link to the cross project references' do
visit project_issue_path(project, issue)
- expect(page).to have_link("#{other_issue.to_reference(project)}")
- expect(page).to have_link("#{other_merge_request.to_reference(project)}")
+ expect(page).to have_link(other_issue.to_reference(project).to_s)
+ expect(page).to have_link(other_merge_request.to_reference(project).to_s)
expect(page).not_to have_content("#{other_issue.to_reference(project)} (#{other_issue.state})")
expect(page).not_to have_xpath("//a[@title='#{other_issue.title}']")
expect(page).not_to have_content("#{other_merge_request.to_reference(project)} (#{other_merge_request.state})")
@@ -113,7 +113,7 @@ RSpec.describe 'viewing an issue with cross project references' do
it 'does not link a cross project confidential issue if the user does not have access' do
visit project_issue_path(project, issue)
- expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)}")
+ expect(page).not_to have_link(other_confidential_issue.to_reference(project).to_s)
expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
end
@@ -122,7 +122,7 @@ RSpec.describe 'viewing an issue with cross project references' do
visit project_issue_path(project, issue)
- expect(page).to have_link("#{other_confidential_issue.to_reference(project)}")
+ expect(page).to have_link(other_confidential_issue.to_reference(project).to_s)
expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index b6019944071..740d009d6b8 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -211,4 +211,48 @@ RSpec.describe 'Project Jobs Permissions' do
end
end
end
+
+ context 'with CI_DEBUG_SERVICES' do
+ let_it_be(:ci_instance_variable) { create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES') }
+
+ describe 'trace endpoint and raw page' do
+ let_it_be(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
+
+ where(:public_builds, :user_project_role, :ci_debug_services, :expected_status_code, :expected_msg) do
+ true | 'developer' | true | 200 | nil
+ true | 'guest' | true | 403 | 'You must have developer or higher permissions'
+ true | nil | true | 404 | 'Page Not Found Make sure the address is correct'
+ true | 'developer' | false | 200 | nil
+ true | 'guest' | false | 200 | nil
+ true | nil | false | 404 | 'Page Not Found Make sure the address is correct'
+ false | 'developer' | true | 200 | nil
+ false | 'guest' | true | 403 | 'You must have developer or higher permissions'
+ false | nil | true | 404 | 'Page Not Found Make sure the address is correct'
+ false | 'developer' | false | 200 | nil
+ false | 'guest' | false | 403 | 'The current user is not authorized to access the job log'
+ false | nil | false | 404 | 'Page Not Found Make sure the address is correct'
+ end
+
+ with_them do
+ before do
+ ci_instance_variable.update!(value: ci_debug_services)
+ project.update!(public_builds: public_builds)
+ user_project_role && project.add_role(user, user_project_role)
+ end
+
+ it 'renders trace to authorized users' do
+ visit trace_project_job_path(project, job)
+
+ expect(status_code).to eq(expected_status_code)
+ end
+
+ it 'renders raw trace to authorized users' do
+ visit raw_project_job_path(project, job)
+
+ expect(status_code).to eq(expected_status_code)
+ expect(page).to have_content(expected_msg)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index 995f4a1e3d2..cb3c1594868 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -203,15 +203,15 @@ RSpec.describe 'User browses jobs' do
end
it 'contains a link to the job sha' do
- expect(page.find('[data-testid="job-sha"]')).to have_content "#{job.sha[0..7]}"
+ expect(page.find('[data-testid="job-sha"]')).to have_content job.sha[0..7].to_s
end
it 'contains a link to the job id' do
- expect(page.find('[data-testid="job-id-link"]')).to have_content "#{job.id}"
+ expect(page.find('[data-testid="job-id-link"]')).to have_content job.id.to_s
end
it 'contains a link to the job ref' do
- expect(page.find('[data-testid="job-ref"]')).to have_content "#{job.ref}"
+ expect(page.find('[data-testid="job-ref"]')).to have_content job.ref.to_s
end
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 84c75752bc1..96a8168e708 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -463,7 +463,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'when variables are stored in trigger_request' do
before do
- trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
visit project_job_path(project, job)
end
@@ -508,7 +508,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'when variables are stored in trigger_request' do
before do
- trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' })
visit project_job_path(project, job)
end
diff --git a/spec/features/projects/members/manage_members_spec.rb b/spec/features/projects/members/manage_members_spec.rb
index 56eb02607a5..1f317c55256 100644
--- a/spec/features/projects/members/manage_members_spec.rb
+++ b/spec/features/projects/members/manage_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects > Members > Manage members', :js do
+RSpec.describe 'Projects > Members > Manage members', :js, product_group: :onboarding do
include Spec::Support::Helpers::Features::MembersHelpers
include Spec::Support::Helpers::Features::InviteMembersModalHelper
include Spec::Support::Helpers::ModalHelpers
diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb
index 1ee0ea51e53..97b743b4d73 100644
--- a/spec/features/projects/network_graph_spec.rb
+++ b/spec/features/projects/network_graph_spec.rb
@@ -13,98 +13,110 @@ RSpec.describe 'Project Network Graph', :js do
allow(Network::Graph).to receive(:max_count).and_return(10)
end
- context 'when branch is master' do
- def switch_ref_to(ref_name)
- first('.js-project-refs-dropdown').click
-
- page.within '.project-refs-form' do
- click_link ref_name
+ shared_examples 'network graph' do
+ context 'when branch is master' do
+ def switch_ref_to(ref_name)
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link ref_name
+ end
end
- end
- def click_show_only_selected_branch_checkbox
- find('#filter_ref').click
- end
+ def click_show_only_selected_branch_checkbox
+ find('#filter_ref').click
+ end
- before do
- visit project_network_path(project, 'master')
- end
+ before do
+ visit project_network_path(project, 'master')
+ end
- it 'renders project network' do
- expect(page).to have_selector ".network-graph"
- expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
- page.within '.network-graph' do
- expect(page).to have_content 'master'
+ it 'renders project network' do
+ expect(page).to have_selector ".network-graph"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
+ page.within '.network-graph' do
+ expect(page).to have_content 'master'
+ end
end
- end
- it 'switches ref to branch' do
- switch_ref_to('feature')
+ it 'switches ref to branch' do
+ switch_ref_to('feature')
- expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature'
- page.within '.network-graph' do
- expect(page).to have_content 'feature'
+ expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature'
+ page.within '.network-graph' do
+ expect(page).to have_content 'feature'
+ end
end
- end
- it 'switches ref to tag' do
- switch_ref_to('v1.0.0')
+ it 'switches ref to tag' do
+ switch_ref_to('v1.0.0')
- expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0'
- page.within '.network-graph' do
- expect(page).to have_content 'v1.0.0'
+ expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0'
+ page.within '.network-graph' do
+ expect(page).to have_content 'v1.0.0'
+ end
end
- end
- it 'renders by commit sha of "v1.0.0"' do
- page.within ".network-form" do
- fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
- find('button').click
+ it 'renders by commit sha of "v1.0.0"' do
+ page.within ".network-form" do
+ fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
+ find('button').click
+ end
+
+ expect(page).to have_selector ".network-graph"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
+ page.within '.network-graph' do
+ expect(page).to have_content 'v1.0.0'
+ end
end
- expect(page).to have_selector ".network-graph"
- expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
- page.within '.network-graph' do
- expect(page).to have_content 'v1.0.0'
- end
- end
+ it 'filters select tag' do
+ switch_ref_to('v1.0.0')
- it 'filters select tag' do
- switch_ref_to('v1.0.0')
+ expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false
+ page.within '.network-graph' do
+ expect(page).to have_content 'Change some files'
+ end
- expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false
- page.within '.network-graph' do
- expect(page).to have_content 'Change some files'
- end
+ click_show_only_selected_branch_checkbox
- click_show_only_selected_branch_checkbox
+ page.within '.network-graph' do
+ expect(page).not_to have_content 'Change some files'
+ end
- page.within '.network-graph' do
- expect(page).not_to have_content 'Change some files'
+ click_show_only_selected_branch_checkbox
+
+ page.within '.network-graph' do
+ expect(page).to have_content 'Change some files'
+ end
end
- click_show_only_selected_branch_checkbox
+ it 'renders error message when sha commit not exists' do
+ page.within ".network-form" do
+ fill_in 'extended_sha1', with: ';'
+ find('button').click
+ end
- page.within '.network-graph' do
- expect(page).to have_content 'Change some files'
+ expect(page).to have_selector '[data-testid="alert-danger"]', text: "Git revision ';' does not exist."
end
end
- it 'renders error message when sha commit not exists' do
- page.within ".network-form" do
- fill_in 'extended_sha1', with: ';'
- find('button').click
- end
+ it 'renders project network with test branch' do
+ visit project_network_path(project, "'test'")
- expect(page).to have_selector '[data-testid="alert-danger"]', text: "Git revision ';' does not exist."
+ page.within '.network-graph' do
+ expect(page).to have_content "'test'"
+ end
end
end
- it 'renders project network with test branch' do
- visit project_network_path(project, "'test'")
+ it_behaves_like 'network graph'
- page.within '.network-graph' do
- expect(page).to have_content "'test'"
+ context 'when disable_network_graph_notes_count is disabled' do
+ before do
+ stub_feature_flags(disable_network_graph_notes_count: false)
end
+
+ it_behaves_like 'network graph'
end
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 4ed0a11da38..e569fef76f8 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Pipeline Schedules', :js do
include Spec::Support::Helpers::ModalHelpers
let!(:project) { create(:project, :repository) }
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
let(:scope) { nil }
let!(:user) { create(:user) }
@@ -45,7 +45,7 @@ RSpec.describe 'Pipeline Schedules', :js do
description = find_field('schedule_description').value
expect(description).to eq('pipeline schedule')
expect(page).to have_button('master')
- expect(page).to have_button('UTC')
+ expect(page).to have_button('Select timezone')
end
it 'edits the scheduled pipeline' do
@@ -164,7 +164,7 @@ RSpec.describe 'Pipeline Schedules', :js do
it 'sets defaults for timezone and target branch' do
expect(page).to have_button('master')
- expect(page).to have_button('UTC')
+ expect(page).to have_button('Select timezone')
end
it 'creates a new scheduled pipeline' do
@@ -314,8 +314,8 @@ RSpec.describe 'Pipeline Schedules', :js do
end
def select_timezone
- find('.js-timezone-dropdown').click
- click_link 'American Samoa'
+ find('[data-testid="schedule-timezone"] .dropdown-toggle').click
+ find("button", text: "Arizona").click
end
def select_target_branch
diff --git a/spec/features/projects/pipelines/legacy_pipeline_spec.rb b/spec/features/projects/pipelines/legacy_pipeline_spec.rb
index d93c951791d..c4fc194f0cd 100644
--- a/spec/features/projects/pipelines/legacy_pipeline_spec.rb
+++ b/spec/features/projects/pipelines/legacy_pipeline_spec.rb
@@ -726,12 +726,7 @@ RSpec.describe 'Pipeline', :js do
before do
schedule.owner.block!
-
- begin
- PipelineScheduleWorker.new.perform
- rescue Ci::CreatePipelineService::CreateError
- # Do nothing, assert view code after the Pipeline failed to create.
- end
+ PipelineScheduleWorker.new.perform
end
it 'displays the PipelineSchedule in an inactive state' do
diff --git a/spec/features/projects/pipelines/legacy_pipelines_spec.rb b/spec/features/projects/pipelines/legacy_pipelines_spec.rb
index 2e0ea695ab3..9d3ac71a875 100644
--- a/spec/features/projects/pipelines/legacy_pipelines_spec.rb
+++ b/spec/features/projects/pipelines/legacy_pipelines_spec.rb
@@ -316,7 +316,6 @@ RSpec.describe 'Pipelines', :js do
end
before do
- stub_feature_flags(bootstrap_confirmation_modals: false)
visit_project_pipelines
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 0b43e13996f..2d729af513a 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -64,7 +64,9 @@ RSpec.describe 'Pipeline', :js do
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
+ let(:pipeline) do
+ create(:ci_pipeline, name: 'Build pipeline', project: project, ref: 'master', sha: project.commit.id, user: user)
+ end
subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) }
@@ -96,6 +98,45 @@ RSpec.describe 'Pipeline', :js do
end
end
+ context 'with pipeline_name feature flag enabled' do
+ before do
+ stub_feature_flags(pipeline_name: true)
+ end
+
+ it 'displays pipeline name instead of commit title' do
+ visit_pipeline
+
+ within 'h3' do
+ expect(page).to have_content(pipeline.name)
+ end
+
+ within '.well-segment[data-testid="commit-row"]' do
+ expect(page).to have_content(project.commit.title)
+ expect(page).to have_content(project.commit.short_id)
+ end
+ end
+ end
+
+ context 'with pipeline_name feature flag disabled' do
+ before do
+ stub_feature_flags(pipeline_name: false)
+ end
+
+ it 'displays commit title' do
+ visit_pipeline
+
+ within 'h3' do
+ expect(page).not_to have_content(pipeline.name)
+ expect(page).to have_content(project.commit.title)
+ end
+
+ within '.well-segment[data-testid="commit-row"]' do
+ expect(page).not_to have_content(project.commit.title)
+ expect(page).to have_content(project.commit.short_id)
+ end
+ end
+ end
+
describe 'related merge requests' do
context 'when there are no related merge requests' do
it 'shows a "no related merge requests" message' do
@@ -363,7 +404,7 @@ RSpec.describe 'Pipeline', :js do
project: downstream_project,
ref: 'master',
sha: downstream_project.commit.id,
- child_of: pipeline )
+ child_of: pipeline)
end
let!(:build) { create(:ci_build, status, pipeline: downstream_pipeline, user: user) }
@@ -851,12 +892,7 @@ RSpec.describe 'Pipeline', :js do
before do
schedule.owner.block!
-
- begin
- PipelineScheduleWorker.new.perform
- rescue Ci::CreatePipelineService::CreateError
- # Do nothing, assert view code after the Pipeline failed to create.
- end
+ PipelineScheduleWorker.new.perform
end
it 'displays the PipelineSchedule in an inactive state' do
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index eabbcd5e38e..b7b715cb6db 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -680,7 +680,7 @@ RSpec.describe 'Pipelines', :js do
end
context 'when variables are specified' do
- it 'creates a new pipeline with variables', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a new pipeline with variables' do
page.within(find("[data-testid='ci-variable-row']")) do
find("[data-testid='pipeline-form-ci-variable-key']").set('key_name')
find("[data-testid='pipeline-form-ci-variable-value']").set('value')
@@ -708,7 +708,7 @@ RSpec.describe 'Pipelines', :js do
it { expect(page).to have_content('Missing CI config file') }
- it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
stub_ci_pipeline_to_return_yaml_file
expect do
@@ -722,6 +722,7 @@ RSpec.describe 'Pipelines', :js do
# Run Pipeline form with REST endpoints
# TODO: Clean up tests when run_pipeline_graphql is enabled
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/372310
context 'with feature flag disabled' do
before do
stub_feature_flags(run_pipeline_graphql: false)
diff --git a/spec/features/projects/product_analytics/events_spec.rb b/spec/features/projects/product_analytics/events_spec.rb
deleted file mode 100644
index 05d12e12acb..00000000000
--- a/spec/features/projects/product_analytics/events_spec.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Product Analytics > Events' do
- let_it_be(:project) { create(:project_empty_repo) }
- let_it_be(:user) { create(:user) }
-
- let(:event) { create(:product_analytics_event, project: project) }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'shows no events message' do
- visit(project_product_analytics_path(project))
-
- expect(page).to have_content('There are currently no events')
- end
-
- it 'shows events' do
- event
-
- visit(project_product_analytics_path(project))
-
- expect(page).to have_content('dvce_created_tstamp')
- expect(page).to have_content(event.event_id)
- end
-end
diff --git a/spec/features/projects/product_analytics/graphs_spec.rb b/spec/features/projects/product_analytics/graphs_spec.rb
deleted file mode 100644
index e2293893589..00000000000
--- a/spec/features/projects/product_analytics/graphs_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Product Analytics > Graphs' do
- let_it_be(:project) { create(:project) }
- let_it_be(:user) { create(:user) }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'shows graphs', :js do
- create(:product_analytics_event, project: project)
-
- visit(graphs_project_product_analytics_path(project))
-
- expect(page).to have_content('Showing graphs based on events')
- expect(page).to have_content('platform')
- expect(page).to have_content('os_timezone')
- expect(page).to have_content('br_lang')
- expect(page).to have_content('doc_charset')
- end
-end
diff --git a/spec/features/projects/product_analytics/setup_spec.rb b/spec/features/projects/product_analytics/setup_spec.rb
deleted file mode 100644
index 45c2b67502c..00000000000
--- a/spec/features/projects/product_analytics/setup_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Product Analytics > Setup' do
- let_it_be(:project) { create(:project_empty_repo) }
- let_it_be(:user) { create(:user) }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'shows the setup instructions' do
- visit(setup_project_product_analytics_path(project))
-
- expect(page).to have_content('Copy the code below to implement tracking in your application')
- end
-end
diff --git a/spec/features/projects/product_analytics/test_spec.rb b/spec/features/projects/product_analytics/test_spec.rb
deleted file mode 100644
index 8984fb409d1..00000000000
--- a/spec/features/projects/product_analytics/test_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Product Analytics > Test' do
- let_it_be(:project) { create(:project_empty_repo) }
- let_it_be(:user) { create(:user) }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'says it sends a payload' do
- visit(test_project_product_analytics_path(project))
-
- expect(page).to have_content('This page sends a payload.')
- end
-
- it 'shows the last event if there is one' do
- event = create(:product_analytics_event, project: project)
-
- visit(test_project_product_analytics_path(project))
-
- expect(page).to have_content(event.event_id)
- end
-end
diff --git a/spec/features/projects/releases/user_views_edit_release_spec.rb b/spec/features/projects/releases/user_views_edit_release_spec.rb
index 6551b254643..78b9798941a 100644
--- a/spec/features/projects/releases/user_views_edit_release_spec.rb
+++ b/spec/features/projects/releases/user_views_edit_release_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'User edits Release', :js do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
- let(:release) { create(:release, :with_milestones, milestones_count: 1, project: project, name: 'The first release', tag: "v1.1.0" ) }
+ let(:release) { create(:release, :with_milestones, milestones_count: 1, project: project, name: 'The first release', tag: "v1.1.0") }
let(:release_link) { create(:release_link, release: release) }
before do
diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb
index a7348b62fc0..10418e8072d 100644
--- a/spec/features/projects/releases/user_views_releases_spec.rb
+++ b/spec/features/projects/releases/user_views_releases_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'User views releases', :js do
let_it_be(:guest) { create(:user) }
let_it_be(:internal_link) { create(:release_link, release: release_v1, name: 'An internal link', url: "#{project.web_url}/-/jobs/1/artifacts/download", filepath: nil) }
- let_it_be(:internal_link_with_redirect) { create(:release_link, release: release_v1, name: 'An internal link with a redirect', url: "#{project.web_url}/-/jobs/2/artifacts/download", filepath: '/binaries/linux-amd64' ) }
+ let_it_be(:internal_link_with_redirect) { create(:release_link, release: release_v1, name: 'An internal link with a redirect', url: "#{project.web_url}/-/jobs/2/artifacts/download", filepath: '/binaries/linux-amd64') }
let_it_be(:external_link) { create(:release_link, release: release_v1, name: 'An external link', url: "https://example.com/an/external/link", filepath: nil) }
before do
diff --git a/spec/features/projects/settings/branch_names_settings_spec.rb b/spec/features/projects/settings/branch_names_settings_spec.rb
new file mode 100644
index 00000000000..fdd883bc2b6
--- /dev/null
+++ b/spec/features/projects/settings/branch_names_settings_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project settings > repositories > Branch names', :js do
+ let_it_be(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+
+ sign_in(user)
+ end
+
+ context 'when Issues are initially disabled' do
+ let(:project_feature) { project.project_feature }
+
+ before do
+ project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
+ visit project_settings_repository_path(project)
+ end
+
+ it 'do not render the Branch names settings' do
+ expect(page).not_to have_content('Branch name template')
+ end
+ end
+
+ context 'when Issues are initially enabled' do
+ before do
+ visit project_settings_repository_path(project)
+ end
+
+ it 'shows the Branch names settings' do
+ expect(page).to have_content('Branch name template')
+
+ value = "feature-%{id}"
+
+ within('section#branch-defaults-settings') do
+ fill_in 'project[issue_branch_template]', with: value
+
+ click_on('Save changes')
+ end
+
+ expect(project.reload.issue_branch_template).to eq(value)
+ expect(page).to have_content('Branch name template')
+ end
+ end
+end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index d9bdbf7aa1a..b25ae80b3c3 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -30,7 +30,6 @@ RSpec.describe 'Projects > Settings > Repository settings' do
before do
stub_container_registry_config(enabled: true)
- stub_feature_flags(ajax_new_deploy_token: project)
end
it_behaves_like 'a deploy token in settings' do
diff --git a/spec/features/projects/settings/user_changes_default_branch_spec.rb b/spec/features/projects/settings/user_changes_default_branch_spec.rb
index 508bbcc5327..bf064839bd7 100644
--- a/spec/features/projects/settings/user_changes_default_branch_spec.rb
+++ b/spec/features/projects/settings/user_changes_default_branch_spec.rb
@@ -25,11 +25,11 @@ RSpec.describe 'Projects > Settings > User changes default branch' do
fill_in 'Search branch', with: 'fix'
click_button 'fix'
- page.within '#default-branch-settings' do
+ page.within '#branch-defaults-settings' do
click_button 'Save changes'
end
- expect(find("#{dropdown_selector} input", visible: false).value).to eq 'fix'
+ expect(find(dropdown_selector)).to have_text 'fix'
end
end
diff --git a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
index eed3494ef5b..47383be1ba1 100644
--- a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
+++ b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
@@ -11,7 +11,6 @@ RSpec.describe 'Repository Settings > User sees revoke deploy token modal', :js
before do
project.add_role(user, role)
sign_in(user)
- stub_feature_flags(ajax_new_deploy_token: project)
visit(project_settings_repository_path(project))
click_button('Revoke')
end
diff --git a/spec/features/projects/settings/webhooks_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb
index 25752bcaf45..adbf2f6ee5c 100644
--- a/spec/features/projects/settings/webhooks_settings_spec.rb
+++ b/spec/features/projects/settings/webhooks_settings_spec.rb
@@ -48,22 +48,47 @@ RSpec.describe 'Projects > Settings > Webhook Settings' do
expect(page).to have_content('Releases events')
end
- it 'create webhook', :js do
- visit webhooks_path
+ context 'when feature flag "enhanced_webhook_support_regex" is disabled' do
+ before do
+ stub_feature_flags(enhanced_webhook_support_regex: false)
+ end
- fill_in 'URL', with: url
- check 'Tag push events'
- fill_in 'hook_push_events_branch_filter', with: 'master'
- check 'Enable SSL verification'
- check 'Job events'
+ it 'create webhook', :js do
+ visit webhooks_path
- click_button 'Add webhook'
+ fill_in 'URL', with: url
+ check 'Tag push events'
+ fill_in 'hook_push_events_branch_filter', with: 'master'
+ check 'Enable SSL verification'
+ check 'Job events'
- expect(page).to have_content(url)
- expect(page).to have_content('SSL Verification: enabled')
- expect(page).to have_content('Push events')
- expect(page).to have_content('Tag push events')
- expect(page).to have_content('Job events')
+ click_button 'Add webhook'
+
+ expect(page).to have_content(url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Tag push events')
+ expect(page).to have_content('Job events')
+ expect(page).to have_content('Push events')
+ end
+ end
+
+ context 'when feature flag "enhanced_webhook_support_regex" is enabled' do
+ it 'create webhook', :js do
+ visit webhooks_path
+
+ fill_in 'URL', with: url
+ check 'Tag push events'
+ check 'Enable SSL verification'
+ check 'Job events'
+
+ click_button 'Add webhook'
+
+ expect(page).to have_content(url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Tag push events')
+ expect(page).to have_content('Job events')
+ expect(page).to have_content('Push events')
+ end
end
it 'edit existing webhook', :js do
diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb
index d2a7596aec0..df13bb55c6d 100644
--- a/spec/features/projects/user_changes_project_visibility_spec.rb
+++ b/spec/features/projects/user_changes_project_visibility_spec.rb
@@ -103,6 +103,9 @@ RSpec.describe 'User changes public project visibility', :js do
sign_in(project.first_owner)
visit edit_project_path(project)
+
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/381259
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(110)
end
it_behaves_like 'does not require confirmation'
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 50e6eb66466..ee74ac84a73 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -2,228 +2,237 @@
require 'spec_helper'
-RSpec.describe 'User searches for code' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, namespace: user.namespace) }
-
- context 'when signed in' do
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'finds a file' do
- visit(project_path(project))
+RSpec.describe 'User searches for code', :js, :disable_rate_limiter do
+ using RSpec::Parameterized::TableSyntax
- submit_search('application.js')
- select_search_scope('Code')
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
- expect(page).to have_selector('.results', text: 'application.js')
- expect(page).to have_selector('.file-content .code')
- expect(page).to have_selector("span.line[lang='javascript']")
- expect(page).to have_link('application.js', href: %r{master/files/js/application.js})
- expect(page).to have_button('Copy file path')
- end
-
- context 'when on a project page', :js do
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ context 'when signed in' do
before do
- visit(search_path)
- find('[data-testid="project-filter"]').click
-
- wait_for_requests
-
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_maintainer(user)
+ sign_in(user)
end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'blobs'
+ it 'finds a file' do
+ visit(project_path(project))
- it 'finds code and links to blob' do
- fill_in('dashboard_search', with: 'rspec')
- find('.gl-search-box-by-click-search-button').click
+ submit_search('application.js')
+ select_search_scope('Code')
- expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
-
- find("#blob-L3").click
- expect(current_url).to match(%r{blob/master/.gitignore#L3})
+ expect(page).to have_selector('.results', text: 'application.js')
+ expect(page).to have_selector('.file-content .code')
+ expect(page).to have_selector("span.line[lang='javascript']")
+ expect(page).to have_link('application.js', href: %r{master/files/js/application.js})
+ expect(page).to have_button('Copy file path')
end
- it 'finds code and links to blame' do
- fill_in('dashboard_search', with: 'rspec')
- find('.gl-search-box-by-click-search-button').click
+ context 'when on a project page' do
+ before do
+ visit(search_path)
+ find('[data-testid="project-filter"]').click
- expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
+ wait_for_requests
- find("#blame-L3").click
- expect(current_url).to match(%r{blame/master/.gitignore#L3})
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
+ end
- it 'search mutiple words with refs switching' do
- expected_result = 'Use `snake_case` for naming files'
- search = 'for naming files'
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'blobs' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- fill_in('dashboard_search', with: search)
- find('.gl-search-box-by-click-search-button').click
+ it 'finds code and links to blob' do
+ expected_result = 'Update capybara, rspec-rails, poltergeist to recent versions'
- expect(page).to have_selector('.results', text: expected_result)
+ fill_in('dashboard_search', with: 'rspec')
+ find('.gl-search-box-by-click-search-button').click
- find('.ref-selector').click
- wait_for_requests
+ expect(page).to have_selector('.results', text: expected_result)
- page.within('.ref-selector') do
- find('li', text: 'v1.0.0').click
+ find("#blob-L3").click
+ expect(current_url).to match(%r{blob/master/.gitignore#L3})
end
- expect(page).to have_selector('.results', text: expected_result)
+ it 'finds code and links to blame' do
+ expected_result = 'Update capybara, rspec-rails, poltergeist to recent versions'
- expect(find_field('dashboard_search').value).to eq(search)
- expect(find("#blob-L1502")[:href]).to match(%r{blob/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
- expect(find("#blame-L1502")[:href]).to match(%r{blame/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
- end
- end
+ fill_in('dashboard_search', with: 'rspec')
+ find('.gl-search-box-by-click-search-button').click
- context 'when :new_header_search is true' do
- context 'search code within refs', :js do
- let(:ref_name) { 'v1.0.0' }
+ expect(page).to have_selector('.results', text: expected_result)
- before do
- # This feature is diabled by default in spec_helper.rb.
- # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
- # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
- stub_feature_flags(new_header_search: true)
- visit(project_tree_path(project, ref_name))
-
- submit_search('gitlab-grack')
- select_search_scope('Code')
+ find("#blame-L3").click
+ expect(current_url).to match(%r{blame/master/.gitignore#L3})
end
- it 'shows ref switcher in code result summary' do
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ it 'search multiple words with refs switching' do
+ expected_result = 'Use `snake_case` for naming files'
+ search = 'for naming files'
- it 'persists branch name across search' do
+ fill_in('dashboard_search', with: search)
find('.gl-search-box-by-click-search-button').click
- expect(find('.ref-selector')).to have_text(ref_name)
- end
- # this example is use to test the desgine that the refs is not
- # only repersent the branch as well as the tags.
- it 'ref swither list all the branchs and tags' do
+ expect(page).to have_selector('.results', text: expected_result)
+
find('.ref-selector').click
wait_for_requests
page.within('.ref-selector') do
- expect(page).to have_selector('li', text: 'add-ipython-files')
- expect(page).to have_selector('li', text: 'v1.0.0')
+ find('li', text: 'v1.0.0').click
end
+
+ expect(page).to have_selector('.results', text: expected_result)
+
+ expect(find_field('dashboard_search').value).to eq(search)
+ expect(find("#blob-L1502")[:href]).to match(%r{blob/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
+ expect(find("#blame-L1502")[:href]).to match(%r{blame/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
end
+ end
- it 'search result changes when refs switched' do
- ref = 'master'
- expect(find('.results')).not_to have_content('path = gitlab-grack')
+ context 'when :new_header_search is true' do
+ context 'search code within refs' do
+ let(:ref_name) { 'v1.0.0' }
- find('.ref-selector').click
- wait_for_requests
+ before do
+ # This feature is disabled by default in spec_helper.rb.
+ # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
+ # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
+ stub_feature_flags(new_header_search: true)
+ visit(project_tree_path(project, ref_name))
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
+ submit_search('gitlab-grack')
+ select_search_scope('Code')
+ end
+
+ it 'shows ref switcher in code result summary' do
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
+
+ it 'persists branch name across search' do
+ find('.gl-search-box-by-click-search-button').click
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
+
+ # this example is use to test the design that the refs is not
+ # only represent the branch as well as the tags.
+ it 'ref switcher list all the branches and tags' do
+ find('.ref-selector').click
wait_for_requests
- find('li', text: ref).click
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
+ end
end
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
- end
- end
- end
+ it 'search result changes when refs switched' do
+ ref = 'master'
+ expect(find('.results')).not_to have_content('path = gitlab-grack')
- context 'when :new_header_search is false' do
- context 'search code within refs', :js do
- let(:ref_name) { 'v1.0.0' }
+ find('.ref-selector').click
+ wait_for_requests
- before do
- # This feature is diabled by default in spec_helper.rb.
- # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
- # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
- stub_feature_flags(new_header_search: false)
- visit(project_tree_path(project, ref_name))
-
- submit_search('gitlab-grack')
- select_search_scope('Code')
- end
+ page.within('.ref-selector') do
+ fill_in _('Search by Git revision'), with: ref
+ wait_for_requests
- it 'shows ref switcher in code result summary' do
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ find('li', text: ref).click
+ end
- it 'persists branch name across search' do
- find('.gl-search-box-by-click-search-button').click
- expect(find('.ref-selector')).to have_text(ref_name)
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
+ end
end
+ end
- # this example is use to test the desgine that the refs is not
- # only repersent the branch as well as the tags.
- it 'ref swither list all the branchs and tags' do
- find('.ref-selector').click
- wait_for_requests
+ context 'when :new_header_search is false' do
+ context 'search code within refs' do
+ let(:ref_name) { 'v1.0.0' }
- page.within('.ref-selector') do
- expect(page).to have_selector('li', text: 'add-ipython-files')
- expect(page).to have_selector('li', text: 'v1.0.0')
+ before do
+ # This feature is disabled by default in spec_helper.rb.
+ # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
+ # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
+ stub_feature_flags(new_header_search: false)
+ visit(project_tree_path(project, ref_name))
+
+ submit_search('gitlab-grack')
+ select_search_scope('Code')
end
- end
- it 'search result changes when refs switched' do
- ref = 'master'
- expect(find('.results')).not_to have_content('path = gitlab-grack')
+ it 'shows ref switcher in code result summary' do
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- find('.ref-selector').click
- wait_for_requests
+ it 'persists branch name across search' do
+ find('.gl-search-box-by-click-search-button').click
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
+ # this example is use to test the design that the refs is not
+ # only represent the branch as well as the tags.
+ it 'ref switcher list all the branches and tags' do
+ find('.ref-selector').click
wait_for_requests
- find('li', text: ref).click
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
+ end
end
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
+ it 'search result changes when refs switched' do
+ ref = 'master'
+ expect(find('.results')).not_to have_content('path = gitlab-grack')
+
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ fill_in _('Search by Git revision'), with: ref
+ wait_for_requests
+
+ find('li', text: ref).click
+ end
+
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
+ end
end
end
- end
- it 'no ref switcher shown in issue result summary', :js do
- issue = create(:issue, title: 'test', project: project)
- visit(project_tree_path(project))
+ it 'no ref switcher shown in issue result summary' do
+ issue = create(:issue, title: 'test', project: project)
+ visit(project_tree_path(project))
- submit_search('test')
- select_search_scope('Code')
+ submit_search('test')
+ select_search_scope('Code')
- expect(page).to have_selector('.ref-selector')
+ expect(page).to have_selector('.ref-selector')
- select_search_scope('Issues')
+ select_search_scope('Issues')
- expect(find(:css, '.results')).to have_link(issue.title)
- expect(page).not_to have_selector('.ref-selector')
+ expect(find(:css, '.results')).to have_link(issue.title)
+ expect(page).not_to have_selector('.ref-selector')
+ end
end
- end
- context 'when signed out' do
- let(:project) { create(:project, :public, :repository) }
-
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- visit(project_path(project))
- end
+ context 'when signed out' do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ end
- it 'finds code' do
- submit_search('rspec')
- select_search_scope('Code')
+ context 'when block_anonymous_global_searches is enabled' do
+ it 'is redirected to login page' do
+ visit(search_path)
- expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
+ expect(page).to have_content('You must be logged in to search across all of GitLab')
+ end
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
index a6793bc3aa7..3c39e9f41d4 100644
--- a/spec/features/search/user_searches_for_comments_spec.rb
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -2,45 +2,52 @@
require 'spec_helper'
-RSpec.describe 'User searches for comments' do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+RSpec.describe 'User searches for comments', :js, :disable_rate_limiter do
+ using RSpec::Parameterized::TableSyntax
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_reporter(user)
- sign_in(user)
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
- visit(project_path(project))
- end
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_reporter(user)
+ sign_in(user)
- include_examples 'search timeouts', 'notes'
+ visit(project_path(project))
+ end
- context 'when a comment is in commits' do
- context 'when comment belongs to an invalid commit' do
- let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
+ include_examples 'search timeouts', 'notes' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- it 'finds a commit' do
- submit_search(comment.note)
- select_search_scope('Comments')
+ context 'when a comment is in commits' do
+ context 'when comment belongs to an invalid commit' do
+ let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
- page.within('.results') do
- expect(page).to have_content('Commit deleted')
- expect(page).to have_content('12345678')
+ it 'finds a commit' do
+ submit_search(comment.note)
+ select_search_scope('Comments')
+
+ page.within('.results') do
+ expect(page).to have_content('Commit deleted')
+ expect(page).to have_content('12345678')
+ end
end
end
end
- end
- context 'when a comment is in a snippet' do
- let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
- let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
+ context 'when a comment is in a snippet' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
+ let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
- it 'finds a snippet' do
- submit_search(comment.note)
- select_search_scope('Comments')
+ it 'finds a snippet' do
+ submit_search(comment.note)
+ select_search_scope('Comments')
- expect(page).to have_selector('.results', text: snippet.title)
+ expect(page).to have_selector('.results', text: snippet.title)
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
index 4ec2a9e6cff..e5d86c27942 100644
--- a/spec/features/search/user_searches_for_commits_spec.rb
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -2,54 +2,62 @@
require 'spec_helper'
-RSpec.describe 'User searches for commits', :js do
+RSpec.describe 'User searches for commits', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+
let(:project) { create(:project, :repository) }
let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
- let(:user) { create(:user) }
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_reporter(user)
- sign_in(user)
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_reporter(user)
+ sign_in(user)
- visit(search_path(project_id: project.id))
- end
+ visit(search_path(project_id: project.id))
+ end
- include_examples 'search timeouts', 'commits'
+ include_examples 'search timeouts', 'commits' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- context 'when searching by SHA' do
- it 'finds a commit and redirects to its page' do
- submit_search(sha)
+ context 'when searching by SHA' do
+ it 'finds a commit and redirects to its page' do
+ submit_search(sha)
- expect(page).to have_current_path(project_commit_path(project, sha))
- end
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
- it 'finds a commit in uppercase and redirects to its page' do
- submit_search(sha.upcase)
+ it 'finds a commit in uppercase and redirects to its page' do
+ submit_search(sha.upcase)
- expect(page).to have_current_path(project_commit_path(project, sha))
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
end
- end
- context 'when searching by message' do
- it 'finds a commit and holds on /search page' do
- project.repository.commit_files(
- user,
- message: 'Message referencing another sha: "deadbeef"',
- branch_name: 'master',
- actions: [{ action: :create, file_path: 'a/new.file', contents: 'new file' }]
- )
+ context 'when searching by message' do
+ it 'finds a commit and holds on /search page' do
+ project.repository.commit_files(
+ user,
+ message: 'Message referencing another sha: "deadbeef"',
+ branch_name: 'master',
+ actions: [{ action: :create, file_path: 'a/new.file', contents: 'new file' }]
+ )
- submit_search('deadbeef')
+ submit_search('deadbeef')
- expect(page).to have_current_path('/search', ignore_query: true)
- end
+ expect(page).to have_current_path('/search', ignore_query: true)
+ end
- it 'finds multiple commits' do
- submit_search('See merge request')
- select_search_scope('Commits')
+ it 'finds multiple commits' do
+ submit_search('See merge request')
+ select_search_scope('Commits')
- expect(page).to have_selector('.commit-row-description', visible: false, count: 9)
+ expect(page).to have_selector('.commit-row-description', visible: false, count: 9)
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index 51d2f355848..22d48bd38f2 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -2,9 +2,12 @@
require 'spec_helper'
-RSpec.describe 'User searches for issues', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
+
let!(:issue1) { create(:issue, title: 'issue Foo', project: project, created_at: 1.hour.ago) }
let!(:issue2) { create(:issue, :closed, :confidential, title: 'issue Bar', project: project) }
@@ -14,127 +17,133 @@ RSpec.describe 'User searches for issues', :js do
select_search_scope('Issues')
end
- context 'when signed in' do
- before do
- project.add_maintainer(user)
- sign_in(user)
- stub_feature_flags(search_page_vertical_nav: false)
-
- visit(search_path)
- end
+ where(search_page_vertical_nav_enabled: [true, false])
- include_examples 'top right search form'
- include_examples 'search timeouts', 'issues'
+ with_them do
+ context 'when signed in' do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- it 'finds an issue' do
- search_for_issue(issue1.title)
+ project.add_maintainer(user)
+ sign_in(user)
- page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
+ visit(search_path)
end
- end
-
- it 'hides confidential icon for non-confidential issues' do
- search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).not_to have_css('[data-testid="eye-slash-icon"]')
- end
- end
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'issues'
- it 'shows confidential icon for confidential issues' do
- search_for_issue(issue2.title)
+ it 'finds an issue' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_css('[data-testid="eye-slash-icon"]')
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
+ end
end
- end
- it 'shows correct badge for open issues' do
- search_for_issue(issue1.title)
+ it 'hides confidential icon for non-confidential issues' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_css('.badge-success')
- expect(page).not_to have_css('.badge-info')
+ page.within('.results') do
+ expect(page).not_to have_css('[data-testid="eye-slash-icon"]')
+ end
end
- end
- it 'shows correct badge for closed issues' do
- search_for_issue(issue2.title)
+ it 'shows confidential icon for confidential issues' do
+ search_for_issue(issue2.title)
- page.within('.results') do
- expect(page).not_to have_css('.badge-success')
- expect(page).to have_css('.badge-info')
+ page.within('.results') do
+ expect(page).to have_css('[data-testid="eye-slash-icon"]')
+ end
end
- end
- it 'sorts by created date' do
- search_for_issue('issue')
+ it 'shows correct badge for open issues' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(issue2.title)
- expect(page.all('.search-result-row').last).to have_link(issue1.title)
+ page.within('.results') do
+ expect(page).to have_css('.badge-success')
+ expect(page).not_to have_css('.badge-info')
+ end
end
- find('[data-testid="sort-highest-icon"]').click
+ it 'shows correct badge for closed issues' do
+ search_for_issue(issue2.title)
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(issue1.title)
- expect(page.all('.search-result-row').last).to have_link(issue2.title)
+ page.within('.results') do
+ expect(page).not_to have_css('.badge-success')
+ expect(page).to have_css('.badge-info')
+ end
end
- end
-
- context 'when on a project page' do
- it 'finds an issue' do
- find('[data-testid="project-filter"]').click
- wait_for_requests
+ it 'sorts by created date' do
+ search_for_issue('issue')
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(issue2.title)
+ expect(page.all('.search-result-row').last).to have_link(issue1.title)
end
- search_for_issue(issue1.title)
+ find('[data-testid="sort-highest-icon"]').click
page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
+ expect(page.all('.search-result-row').first).to have_link(issue1.title)
+ expect(page.all('.search-result-row').last).to have_link(issue2.title)
end
end
- end
- end
- context 'when signed out' do
- context 'when block_anonymous_global_searches is disabled' do
- let(:project) { create(:project, :public) }
+ context 'when on a project page' do
+ it 'finds an issue' do
+ find('[data-testid="project-filter"]').click
- before do
- stub_feature_flags(block_anonymous_global_searches: false)
- stub_feature_flags(search_page_vertical_nav: false)
- visit(search_path)
- end
+ wait_for_requests
- include_examples 'top right search form'
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- it 'finds an issue' do
- search_for_issue(issue1.title)
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
+ end
end
end
end
- context 'when block_anonymous_global_searches is enabled' do
+ context 'when signed out' do
before do
- stub_feature_flags(search_page_vertical_nav: false)
- visit(search_path)
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
end
- it 'is redirected to login page' do
- expect(page).to have_content('You must be logged in to search across all of GitLab')
+ context 'when block_anonymous_global_searches is disabled' do
+ let_it_be(:project) { create(:project, :public) }
+
+ before do
+ stub_feature_flags(block_anonymous_global_searches: false)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ search_for_issue(issue1.title)
+
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
+ end
+ end
+ end
+
+ context 'when block_anonymous_global_searches is enabled' do
+ it 'is redirected to login page' do
+ visit(search_path)
+
+ expect(page).to have_content('You must be logged in to search across all of GitLab')
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index a4fbe3a6e59..9bbf2cf16d8 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe 'User searches for merge requests', :js do
+RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) }
@@ -14,62 +16,64 @@ RSpec.describe 'User searches for merge requests', :js do
select_search_scope('Merge requests')
end
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_maintainer(user)
- sign_in(user)
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ sign_in(user)
- visit(search_path)
- end
+ visit(search_path)
+ end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'merge_requests'
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'merge_requests'
- it 'finds a merge request' do
- search_for_mr(merge_request1.title)
+ it 'finds a merge request' do
+ search_for_mr(merge_request1.title)
- page.within('.results') do
- expect(page).to have_link(merge_request1.title)
- expect(page).not_to have_link(merge_request2.title)
+ page.within('.results') do
+ expect(page).to have_link(merge_request1.title)
+ expect(page).not_to have_link(merge_request2.title)
- # Each result should have MR refs like `gitlab-org/gitlab!1`
- page.all('.search-result-row').each do |e|
- expect(e.text).to match(/!\d+/)
+ # 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
- end
- it 'sorts by created date' do
- search_for_mr('Merge Request')
+ it 'sorts by created date' do
+ search_for_mr('Merge Request')
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(merge_request2.title)
- expect(page.all('.search-result-row').last).to have_link(merge_request1.title)
- end
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(merge_request2.title)
+ expect(page.all('.search-result-row').last).to have_link(merge_request1.title)
+ end
- find('[data-testid="sort-highest-icon"]').click
+ find('[data-testid="sort-highest-icon"]').click
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(merge_request1.title)
- expect(page.all('.search-result-row').last).to have_link(merge_request2.title)
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(merge_request1.title)
+ expect(page.all('.search-result-row').last).to have_link(merge_request2.title)
+ end
end
- end
- context 'when on a project page' do
- it 'finds a merge request' do
- find('[data-testid="project-filter"]').click
+ context 'when on a project page' do
+ it 'finds a merge request' do
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- search_for_mr(merge_request1.title)
+ search_for_mr(merge_request1.title)
- page.within('.results') do
- expect(page).to have_link(merge_request1.title)
- expect(page).not_to have_link(merge_request2.title)
+ page.within('.results') do
+ expect(page).to have_link(merge_request1.title)
+ expect(page).not_to have_link(merge_request2.title)
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 6773059830c..702d4e60022 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -2,44 +2,30 @@
require 'spec_helper'
-RSpec.describe 'User searches for milestones', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
- let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
+RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
- before do
- project.add_maintainer(user)
- sign_in(user)
- stub_feature_flags(search_page_vertical_nav: false)
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
- visit(search_path)
- end
+ let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
+ let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
- include_examples 'top right search form'
- include_examples 'search timeouts', 'milestones'
+ where(search_page_vertical_nav_enabled: [true, false])
- it 'finds a milestone' do
- fill_in('dashboard_search', with: milestone1.title)
- find('.gl-search-box-by-click-search-button').click
- select_search_scope('Milestones')
+ with_them do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- page.within('.results') do
- expect(page).to have_link(milestone1.title)
- expect(page).not_to have_link(milestone2.title)
+ visit(search_path)
end
- end
-
- context 'when on a project page' do
- it 'finds a milestone' do
- find('[data-testid="project-filter"]').click
-
- wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'milestones'
+ it 'finds a milestone' do
fill_in('dashboard_search', with: milestone1.title)
find('.gl-search-box-by-click-search-button').click
select_search_scope('Milestones')
@@ -49,5 +35,26 @@ RSpec.describe 'User searches for milestones', :js do
expect(page).not_to have_link(milestone2.title)
end
end
+
+ context 'when on a project page' do
+ it 'finds a milestone' do
+ find('[data-testid="project-filter"]').click
+
+ wait_for_requests
+
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
+
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.gl-search-box-by-click-search-button').click
+ select_search_scope('Milestones')
+
+ page.within('.results') do
+ expect(page).to have_link(milestone1.title)
+ expect(page).not_to have_link(milestone2.title)
+ end
+ end
+ end
end
end
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
index 5902859d1f5..15c6224b61b 100644
--- a/spec/features/search/user_searches_for_projects_spec.rb
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -2,15 +2,12 @@
require 'spec_helper'
-RSpec.describe 'User searches for projects', :js do
+RSpec.describe 'User searches for projects', :js, :disable_rate_limiter do
let!(:project) { create(:project, :public, name: 'Shop') }
context 'when signed out' do
context 'when block_anonymous_global_searches is disabled' do
before do
- stub_feature_flags(search_page_vertical_nav: false)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
stub_feature_flags(block_anonymous_global_searches: false)
end
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
index e21a66fed92..1d649b42c8c 100644
--- a/spec/features/search/user_searches_for_users_spec.rb
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -2,84 +2,90 @@
require 'spec_helper'
-RSpec.describe 'User searches for users' do
- let(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') }
- let(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') }
- let(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') }
-
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- sign_in(user1)
- end
-
- include_examples 'search timeouts', 'users'
+RSpec.describe 'User searches for users', :js, :clean_gitlab_redis_rate_limiting do
+ let_it_be(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') }
+ let_it_be(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') }
+ let_it_be(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') }
- context 'when on the dashboard' do
- it 'finds the user', :js do
- visit dashboard_projects_path
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- submit_search('gob')
- select_search_scope('Users')
+ sign_in(user1)
+ end
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ include_examples 'search timeouts', 'users' do
+ before do
+ visit(search_path)
end
end
- end
- context 'when on the project page' do
- let(:project) { create(:project) }
+ context 'when on the dashboard' do
+ it 'finds the user' do
+ visit dashboard_projects_path
- before do
- create(:project_member, :developer, user: user1, project: project)
- create(:project_member, :developer, user: user2, project: project)
- user3
+ submit_search('gob')
+ select_search_scope('Users')
+
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
+ end
+ end
end
- it 'finds the user belonging to the project' do
- visit project_path(project)
+ context 'when on the project page' do
+ let_it_be_with_reload(:project) { create(:project) }
- submit_search('gob')
- select_search_scope('Users')
+ before do
+ project.add_developer(user1)
+ project.add_developer(user2)
+ end
+
+ it 'finds the user belonging to the project' do
+ visit project_path(project)
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ submit_search('gob')
+ select_search_scope('Users')
- expect(page).not_to have_content('Michael Bluth')
- expect(page).not_to have_content('@michael_bluth')
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
- expect(page).not_to have_content('George Oscar Bluth')
- expect(page).not_to have_content('@gob_2018')
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
+
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
+ end
end
end
- end
- context 'when on the group page' do
- let(:group) { create(:group) }
+ context 'when on the group page' do
+ let(:group) { create(:group) }
- before do
- create(:group_member, :developer, user: user1, group: group)
- create(:group_member, :developer, user: user2, group: group)
- user3
- end
+ before do
+ group.add_developer(user1)
+ group.add_developer(user2)
+ end
- it 'finds the user belonging to the group' do
- visit group_path(group)
+ it 'finds the user belonging to the group' do
+ visit group_path(group)
- submit_search('gob')
- select_search_scope('Users')
+ submit_search('gob')
+ select_search_scope('Users')
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
- expect(page).not_to have_content('Michael Bluth')
- expect(page).not_to have_content('@michael_bluth')
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
- expect(page).not_to have_content('George Oscar Bluth')
- expect(page).not_to have_content('@gob_2018')
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 2e390309022..0f20ad0aa07 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -2,55 +2,59 @@
require 'spec_helper'
-RSpec.describe 'User searches for wiki pages', :js do
- let(:user) { create(:user) }
+RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+
let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'directory/title', content: 'Some Wiki content') }
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_maintainer(user)
- sign_in(user)
-
- visit(search_path)
- end
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_maintainer(user)
+ sign_in(user)
- include_examples 'top right search form'
- include_examples 'search timeouts', 'wiki_blobs'
+ visit(search_path)
+ end
- shared_examples 'search wiki blobs' do
- before do
- stub_feature_flags(search_page_vertical_nav: false)
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'wiki_blobs' do
+ let(:additional_params) { { project_id: project.id } }
end
- it 'finds a page' do
- find('[data-testid="project-filter"]').click
+ shared_examples 'search wiki blobs' do
+ it 'finds a page' do
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- fill_in('dashboard_search', with: search_term)
- find('.gl-search-box-by-click-search-button').click
- select_search_scope('Wiki')
+ fill_in('dashboard_search', with: search_term)
+ find('.gl-search-box-by-click-search-button').click
+ select_search_scope('Wiki')
- page.within('.results') do
- expect(page).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ page.within('.results') do
+ expect(page).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ end
end
end
- end
- context 'when searching by content' do
- it_behaves_like 'search wiki blobs' do
- let(:search_term) { 'content' }
+ context 'when searching by content' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'content' }
+ end
end
- end
- context 'when searching by title' do
- it_behaves_like 'search wiki blobs' do
- let(:search_term) { 'title' }
+ context 'when searching by title' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'title' }
+ end
end
end
end
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 827e3984896..04f22cd2a31 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uses header search field', :js do
+RSpec.describe 'User uses header search field', :js, :disable_rate_limiter do
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -17,10 +17,6 @@ RSpec.describe 'User uses header search field', :js do
end
before do
- stub_feature_flags(search_page_vertical_nav: false)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
sign_in(user)
end
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index 69b9a0aa64d..d18729d080a 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -2,11 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Search Snippets' do
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- end
-
+RSpec.describe 'Search Snippets', :js do
it 'user searches for snippets by title' do
public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
diff --git a/spec/features/user_sees_marketing_header_spec.rb b/spec/features/user_sees_marketing_header_spec.rb
index 31f088ce010..eae964cec02 100644
--- a/spec/features/user_sees_marketing_header_spec.rb
+++ b/spec/features/user_sees_marketing_header_spec.rb
@@ -6,50 +6,14 @@ RSpec.describe 'User sees experimental lmarketing header' do
let_it_be(:project) { create(:project, :public) }
context 'when not logged in' do
- context 'when experiment candidate' do
- it 'shows marketing header links', :aggregate_failures do
- stub_experiments(logged_out_marketing_header: :candidate)
-
- visit project_path(project)
-
- expect(page).to have_text "About GitLab"
- expect(page).to have_text "Pricing"
- expect(page).to have_text "Talk to an expert"
- expect(page).to have_text "Sign up now"
- expect(page).to have_text "Login"
- end
- end
-
- context 'when experiment candidate (trial focused variant)' do
- it 'shows marketing header links', :aggregate_failures do
- stub_experiments(logged_out_marketing_header: :trial_focused)
-
- visit project_path(project)
-
- expect(page).to have_text "About GitLab"
- expect(page).to have_text "Pricing"
- expect(page).to have_text "Talk to an expert"
- expect(page).to have_text "Get a free trial"
- expect(page).to have_text "Sign up"
- expect(page).to have_text "Login"
- end
- end
-
- context 'when experiment control' do
- it 'does not show marketing header links', :aggregate_failures do
- stub_experiments(logged_out_marketing_header: :control)
-
- visit project_path(project)
+ it 'shows marketing header links', :aggregate_failures do
+ visit project_path(project)
- expect(page).not_to have_text "About GitLab"
- expect(page).not_to have_text "Pricing"
- expect(page).not_to have_text "Talk to an expert"
- expect(page).not_to have_text "Sign up now"
- expect(page).not_to have_text "Login"
- expect(page).not_to have_text "Get a free trial"
- expect(page).not_to have_text "Sign up"
- expect(page).to have_text "Sign in / Register"
- end
+ expect(page).to have_text "About GitLab"
+ expect(page).to have_text "Pricing"
+ expect(page).to have_text "Talk to an expert"
+ expect(page).to have_text "Sign up now"
+ expect(page).to have_text "Login"
end
end
@@ -57,8 +21,6 @@ RSpec.describe 'User sees experimental lmarketing header' do
it 'does not show marketing header links', :aggregate_failures do
sign_in(create(:user))
- stub_experiments(logged_out_marketing_header: :candidate)
-
visit project_path(project)
expect(page).not_to have_text "About GitLab"
diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
index c722a4ec05c..e2ee78a7cc5 100644
--- a/spec/features/users/active_sessions_spec.rb
+++ b/spec/features/users/active_sessions_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions do
user = create(:user)
Gitlab::Redis::Sessions.with do |redis|
- redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ redis.sadd?("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
gitlab_sign_in(user)
@@ -45,7 +45,7 @@ RSpec.describe 'Active user sessions', :clean_gitlab_redis_sessions do
personal_access_token = create(:personal_access_token, user: user)
Gitlab::Redis::Sessions.with do |redis|
- redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ redis.sadd?("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
visit user_path(user, :atom, private_token: personal_access_token.token)
diff --git a/spec/features/work_items/work_item_children_spec.rb b/spec/features/work_items/work_item_children_spec.rb
index 95774680a2b..10a1bf7541e 100644
--- a/spec/features/work_items/work_item_children_spec.rb
+++ b/spec/features/work_items/work_item_children_spec.rb
@@ -15,7 +15,6 @@ RSpec.describe 'Work item children', :js do
sign_in(user)
stub_feature_flags(work_items: true)
- stub_feature_flags(work_items_hierarchy: true)
visit project_issue_path(project, issue)
@@ -49,6 +48,7 @@ RSpec.describe 'Work item children', :js do
expect(page).not_to have_selector('[data-testid="add-links-form"]')
click_button 'Add'
+ click_button 'New task'
expect(page).to have_selector('[data-testid="add-links-form"]')
@@ -58,9 +58,10 @@ RSpec.describe 'Work item children', :js do
end
end
- it 'addss a child task', :aggregate_failures do
+ it 'adds a new child task', :aggregate_failures do
page.within('[data-testid="work-item-links"]') do
click_button 'Add'
+ click_button 'New task'
expect(page).to have_button('Create task', disabled: true)
fill_in 'Add a title', with: 'Task 1'
@@ -78,6 +79,7 @@ RSpec.describe 'Work item children', :js do
it 'removes a child task and undoing', :aggregate_failures do
page.within('[data-testid="work-item-links"]') do
click_button 'Add'
+ click_button 'New task'
fill_in 'Add a title', with: 'Task 1'
click_button 'Create task'
wait_for_all_requests
@@ -106,5 +108,29 @@ RSpec.describe 'Work item children', :js do
expect(find('[data-testid="children-count"]')).to have_content('1')
end
end
+
+ context 'with existing task' do
+ let_it_be(:task) { create(:work_item, :task, project: project) }
+
+ it 'adds an existing child task', :aggregate_failures do
+ page.within('[data-testid="work-item-links"]') do
+ click_button 'Add'
+ click_button 'Existing task'
+
+ expect(page).to have_button('Add task', disabled: true)
+ find('[data-testid="work-item-token-select-input"]').set(task.title)
+ wait_for_all_requests
+ click_button task.title
+
+ expect(page).to have_button('Add task', disabled: false)
+
+ click_button 'Add task'
+
+ wait_for_all_requests
+
+ expect(find('[data-testid="links-child"]')).to have_content(task.title)
+ end
+ end
+ end
end
end
diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index de031041e18..57f804e471f 100644
--- a/spec/finders/autocomplete/users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -8,7 +8,8 @@ RSpec.describe Autocomplete::UsersFinder do
describe '#execute' do
let_it_be(:user1) { create(:user, name: 'zzzzzname', username: 'johndoe') }
- let_it_be(:user2) { create(:user, :blocked, username: 'notsorandom') }
+ let_it_be(:blocked_user) { create(:user, :blocked, username: 'blocked_user') }
+ let_it_be(:banned_user) { create(:user, :banned, username: 'banned_user') }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
@@ -39,7 +40,13 @@ RSpec.describe Autocomplete::UsersFinder do
end
context 'and author is blocked' do
- let(:params) { { author_id: user2.id } }
+ let(:params) { { author_id: blocked_user.id } }
+
+ it { is_expected.to match_array([project.first_owner]) }
+ end
+
+ context 'and author is banned' do
+ let(:params) { { author_id: banned_user.id } }
it { is_expected.to match_array([project.first_owner]) }
end
@@ -108,7 +115,7 @@ RSpec.describe Autocomplete::UsersFinder do
end
context 'when filtered by skip_users' do
- let(:params) { { skip_users: [omniauth_user.id, current_user.id] } }
+ let(:params) { { skip_users: [omniauth_user.id, current_user.id, blocked_user] } }
it { is_expected.to match_array([user1, external_user]) }
end
@@ -139,10 +146,10 @@ RSpec.describe Autocomplete::UsersFinder do
end
context 'when filtered by current_user' do
- let(:current_user) { user2 }
+ let(:current_user) { blocked_user }
let(:params) { { current_user: true } }
- it { is_expected.to match_array([user2, user1, external_user, omniauth_user]) }
+ it { is_expected.to match_array([blocked_user, user1, external_user, omniauth_user]) }
end
context 'when filtered by author_id' do
@@ -155,5 +162,37 @@ RSpec.describe Autocomplete::UsersFinder do
associations = subject.map { |user| user.association(:status) }
expect(associations).to all(be_loaded)
end
+
+ context 'when filtered by state' do
+ context "searching without states" do
+ let(:params) { { states: nil } }
+
+ it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
+ end
+
+ context "searching with states=active" do
+ let(:params) { { states: %w[active] } }
+
+ it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
+ end
+
+ context "searching with states=blocked" do
+ let(:params) { { states: %w[blocked] } }
+
+ it { is_expected.to match_array([blocked_user]) }
+ end
+
+ context "searching with states=banned" do
+ let(:params) { { states: %w[banned] } }
+
+ it { is_expected.to match_array([banned_user]) }
+ end
+
+ context "searching with states=blocked,banned" do
+ let(:params) { { states: %w[blocked banned] } }
+
+ it { is_expected.to match_array([blocked_user, banned_user]) }
+ end
+ end
end
end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 9314f616c44..f14c60c4b8f 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -211,7 +211,7 @@ RSpec.describe BranchesFinder do
it 'raises an error' do
expect do
subject
- end.to raise_error(Gitlab::Git::CommandError, '13:could not find page token.')
+ end.to raise_error(Gitlab::Git::CommandError, /could not find page token/)
end
end
diff --git a/spec/finders/clusters/agent_authorizations_finder_spec.rb b/spec/finders/clusters/agent_authorizations_finder_spec.rb
index 2d90f32adc5..f680792d6c4 100644
--- a/spec/finders/clusters/agent_authorizations_finder_spec.rb
+++ b/spec/finders/clusters/agent_authorizations_finder_spec.rb
@@ -64,14 +64,6 @@ RSpec.describe Clusters::AgentAuthorizationsFinder do
let!(:project_authorization) { create(:agent_project_authorization, agent: non_ancestor_agent, project: requesting_project) }
it { is_expected.to match_array([project_authorization]) }
-
- context 'agent_authorization_include_descendants feature flag is disabled' do
- before do
- stub_feature_flags(agent_authorization_include_descendants: false)
- end
-
- it { is_expected.to be_empty }
- end
end
context 'with project authorizations present' do
@@ -138,14 +130,6 @@ RSpec.describe Clusters::AgentAuthorizationsFinder do
let!(:group_authorization) { create(:agent_group_authorization, agent: non_ancestor_agent, group: bottom_level_group) }
it { is_expected.to match_array([group_authorization]) }
-
- context 'agent_authorization_include_descendants feature flag is disabled' do
- before do
- stub_feature_flags(agent_authorization_include_descendants: false)
- end
-
- it { is_expected.to be_empty }
- end
end
it_behaves_like 'access_as' do
diff --git a/spec/finders/clusters/agent_tokens_finder_spec.rb b/spec/finders/clusters/agent_tokens_finder_spec.rb
new file mode 100644
index 00000000000..619aca891c1
--- /dev/null
+++ b/spec/finders/clusters/agent_tokens_finder_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentTokensFinder do
+ describe '#execute' do
+ let_it_be(:project) { create(:project) }
+ let(:user) { create(:user, maintainer_projects: [project]) }
+ let(:agent) { create(:cluster_agent, project: project) }
+ let(:agent_id) { agent.id }
+
+ let!(:matching_agent_tokens) do
+ [
+ create(:cluster_agent_token, agent: agent),
+ create(:cluster_agent_token, :revoked, agent: agent)
+ ]
+ end
+
+ subject(:execute) { described_class.new(project, user, agent_id).execute }
+
+ it 'returns the tokens of the specified agent' do
+ # creating a token in a different agent to make sure it will not be included in the result
+ create(:cluster_agent_token, agent: create(:cluster_agent))
+
+ expect(execute).to match_array(matching_agent_tokens)
+ end
+
+ context 'when user does not have permission' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'raises an error' do
+ expect { execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when agent does not exist' do
+ let(:agent_id) { non_existing_record_id }
+
+ it 'raises an error' do
+ expect { execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/finders/incident_management/timeline_event_tags_finder_spec.rb b/spec/finders/incident_management/timeline_event_tags_finder_spec.rb
new file mode 100644
index 00000000000..5bdb356ff62
--- /dev/null
+++ b/spec/finders/incident_management/timeline_event_tags_finder_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::TimelineEventTagsFinder do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:incident) { create(:incident, project: project) }
+ let_it_be(:timeline_event) do
+ create(:incident_management_timeline_event, project: project, incident: incident, occurred_at: Time.current)
+ end
+
+ let_it_be(:timeline_event_tag) do
+ create(:incident_management_timeline_event_tag, project: project)
+ end
+
+ let_it_be(:timeline_event_tag_link) do
+ create(:incident_management_timeline_event_tag_link,
+ timeline_event: timeline_event,
+ timeline_event_tag: timeline_event_tag)
+ end
+
+ let(:params) { {} }
+
+ describe '#execute' do
+ subject(:execute) { described_class.new(user, timeline_event, params).execute }
+
+ context 'when user has permissions' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns tags on the event' do
+ is_expected.to match_array([timeline_event_tag])
+ end
+
+ context 'when event does not have tags' do
+ let(:timeline_event) do
+ create(:incident_management_timeline_event, project: project, incident: incident, occurred_at: Time.current)
+ end
+
+ it 'returns empty result' do
+ is_expected.to match_array([])
+ end
+ end
+
+ context 'when timeline event is nil' do
+ let(:timeline_event) { nil }
+
+ it { is_expected.to eq(IncidentManagement::TimelineEventTag.none) }
+ end
+ end
+
+ context 'when user does not have permissions' do
+ it { is_expected.to eq(IncidentManagement::TimelineEventTag.none) }
+ end
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 1fa2a975ec3..02153715eac 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -350,43 +350,6 @@ RSpec.describe ProjectsFinder do
end
end
- describe 'filter by without_deleted' do
- let_it_be(:pending_delete_project) { create(:project, :public, pending_delete: true) }
-
- let(:params) { { without_deleted: without_deleted } }
-
- shared_examples 'returns all projects' do
- it { expect(subject).to include(public_project, internal_project, pending_delete_project) }
- end
-
- context 'when without_deleted is true' do
- let(:without_deleted) { true }
-
- it 'returns projects that are not pending_delete' do
- expect(subject).not_to include(pending_delete_project)
- expect(subject).to include(public_project, internal_project)
- end
- end
-
- context 'when without_deleted is false' do
- let(:without_deleted) { false }
-
- it_behaves_like 'returns all projects'
- end
-
- context 'when without_deleted is nil' do
- let(:without_deleted) { nil }
-
- it_behaves_like 'returns all projects'
- end
-
- context 'when without_deleted is not present' do
- let(:params) { {} }
-
- it_behaves_like 'returns all projects'
- end
- end
-
describe 'filter by last_activity_after' do
let(:params) { { last_activity_after: 60.minutes.ago } }
@@ -398,6 +361,15 @@ RSpec.describe ProjectsFinder do
it { is_expected.to match_array([internal_project]) }
end
+ describe 'always filters by without_deleted' do
+ let_it_be(:pending_delete_project) { create(:project, :public, pending_delete: true) }
+
+ it 'returns projects that are not pending_delete' do
+ expect(subject).not_to include(pending_delete_project)
+ expect(subject).to include(public_project, internal_project)
+ end
+ end
+
describe 'filter by last_activity_before' do
let(:params) { { last_activity_before: 60.minutes.ago } }
diff --git a/spec/finders/users_star_projects_finder_spec.rb b/spec/finders/users_star_projects_finder_spec.rb
index 038506cc93f..e824940430c 100644
--- a/spec/finders/users_star_projects_finder_spec.rb
+++ b/spec/finders/users_star_projects_finder_spec.rb
@@ -8,10 +8,12 @@ RSpec.describe UsersStarProjectsFinder do
let(:user) { create(:user) }
let(:private_user) { create(:user, private_profile: true) }
let(:other_user) { create(:user) }
+ let(:blocked_user) { create(:user, state: 'blocked') }
before do
user.toggle_star(project)
private_user.toggle_star(project)
+ blocked_user.toggle_star(project)
end
describe '#execute' do
@@ -38,5 +40,13 @@ RSpec.describe UsersStarProjectsFinder do
it { is_expected.to match_array(public_stars) }
end
+
+ describe 'with active users only' do
+ let(:current_user) { private_user }
+
+ it 'ignores stars of non-active users' do
+ is_expected.not_to include(*blocked_user.users_star_projects)
+ end
+ end
end
end
diff --git a/spec/fixtures/api/schemas/entities/codequality_degradation.json b/spec/fixtures/api/schemas/entities/codequality_degradation.json
index 6cf20ee8b9e..863b9f0c77e 100644
--- a/spec/fixtures/api/schemas/entities/codequality_degradation.json
+++ b/spec/fixtures/api/schemas/entities/codequality_degradation.json
@@ -18,7 +18,10 @@
},
"line": {
"type": "integer"
+ },
+ "web_url": {
+ "type": "string"
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json b/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json
index afe82f5e632..05ae036d986 100644
--- a/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json
+++ b/spec/fixtures/api/schemas/entities/codequality_reports_comparer.json
@@ -1,6 +1,12 @@
{
"type": "object",
- "required": ["status", "summary", "new_errors", "resolved_errors", "existing_errors"],
+ "required": [
+ "status",
+ "summary",
+ "new_errors",
+ "resolved_errors",
+ "existing_errors"
+ ],
"properties": {
"status": {
"type": "string"
@@ -18,7 +24,11 @@
"type": "integer"
}
},
- "required": ["total", "resolved", "errored"]
+ "required": [
+ "total",
+ "resolved",
+ "errored"
+ ]
},
"new_errors": {
"type": "array",
@@ -40,4 +50,4 @@
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/entities/protected_ref_access.json b/spec/fixtures/api/schemas/entities/protected_ref_access.json
new file mode 100644
index 00000000000..144852e1558
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/protected_ref_access.json
@@ -0,0 +1,25 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "access_level",
+ "access_level_description"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "access_level": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "access_level_description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/graphql/packages/package_details.json b/spec/fixtures/api/schemas/graphql/packages/package_details.json
index 33eb67a0280..1f3de0e0ff5 100644
--- a/spec/fixtures/api/schemas/graphql/packages/package_details.json
+++ b/spec/fixtures/api/schemas/graphql/packages/package_details.json
@@ -14,7 +14,8 @@
"versions",
"status",
"canDestroy",
- "lastDownloadedAt"
+ "lastDownloadedAt",
+ "_links"
],
"properties": {
"id": {
@@ -177,6 +178,15 @@
},
"lastDownloadedAt": {
"type": ["string", "null"]
+ },
+ "_links": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "webPath": {
+ "type": ["string", "null"]
+ }
+ }
}
}
}
diff --git a/spec/fixtures/api/schemas/ml/run.json b/spec/fixtures/api/schemas/ml/run.json
index 2418f44b21f..48d0ed25ce4 100644
--- a/spec/fixtures/api/schemas/ml/run.json
+++ b/spec/fixtures/api/schemas/ml/run.json
@@ -27,15 +27,43 @@
"end_time"
],
"properties": {
- "run_id": { "type": "string" },
- "run_uuid": { "type": "string" },
- "experiment_id": { "type": "string" },
- "artifact_location": { "type": "string" },
- "start_time": { "type": "integer" },
- "end_time": { "type": "integer" },
+ "run_id": {
+ "type": "string"
+ },
+ "run_uuid": {
+ "type": "string"
+ },
+ "experiment_id": {
+ "type": "string"
+ },
+ "artifact_uri": {
+ "type": "string"
+ },
+ "start_time": {
+ "type": "integer"
+ },
+ "end_time": {
+ "type": "integer"
+ },
"user_id": "",
- "status": { "type": { "enum" : ["RUNNING", "SCHEDULED", "FINISHED", "FAILED", "KILLED"] } },
- "lifecycle_stage": { "type": { "enum" : ["active"] } }
+ "status": {
+ "type": {
+ "enum": [
+ "RUNNING",
+ "SCHEDULED",
+ "FINISHED",
+ "FAILED",
+ "KILLED"
+ ]
+ }
+ },
+ "lifecycle_stage": {
+ "type": {
+ "enum": [
+ "active"
+ ]
+ }
+ }
}
},
"data": {
@@ -44,4 +72,4 @@
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/pipeline_schedule_variable.json b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
index 022d36cb88c..83eed11f458 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule_variable.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
@@ -6,9 +6,18 @@
"variable_type"
],
"properties": {
- "key": { "type": "string" },
- "value": { "type": "string" },
- "variable_type": { "type": "string" }
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ },
+ "variable_type": {
+ "type": "string"
+ },
+ "raw": {
+ "type": "boolean"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/project_mirror.json b/spec/fixtures/api/schemas/project_mirror.json
new file mode 100644
index 00000000000..0f626c04f24
--- /dev/null
+++ b/spec/fixtures/api/schemas/project_mirror.json
@@ -0,0 +1,48 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "url",
+ "update_status",
+ "last_update_at",
+ "last_update_started_at",
+ "last_successful_update_at",
+ "last_error"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "url": {
+ "type": "string"
+ },
+ "update_status": {
+ "type": "string"
+ },
+ "last_update_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "last_update_started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "last_successful_update_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "last_error": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "additionalProperties": false
+} \ No newline at end of file
diff --git a/spec/fixtures/api/schemas/protected_branch.json b/spec/fixtures/api/schemas/protected_branch.json
new file mode 100644
index 00000000000..4ad5dbe2313
--- /dev/null
+++ b/spec/fixtures/api/schemas/protected_branch.json
@@ -0,0 +1,33 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "push_access_levels",
+ "merge_access_levels",
+ "allow_force_push"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "push_access_levels": {
+ "type": "array",
+ "items": {
+ "$ref": "entities/protected_ref_access.json"
+ }
+ },
+ "merge_access_levels": {
+ "type": "array",
+ "items": {
+ "$ref": "entities/protected_ref_access.json"
+ }
+ },
+ "allow_force_push": {
+ "type": "boolean"
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/protected_branches.json b/spec/fixtures/api/schemas/protected_branches.json
new file mode 100644
index 00000000000..c87b3b153a9
--- /dev/null
+++ b/spec/fixtures/api/schemas/protected_branches.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "protected_branch.json"
+ }
+}
diff --git a/spec/fixtures/api/schemas/protected_tag.json b/spec/fixtures/api/schemas/protected_tag.json
new file mode 100644
index 00000000000..c5aaf0f0cba
--- /dev/null
+++ b/spec/fixtures/api/schemas/protected_tag.json
@@ -0,0 +1,19 @@
+{
+ "type": "object",
+ "required": [
+ "name",
+ "create_access_levels"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "create_access_levels": {
+ "type": "array",
+ "items": {
+ "$ref": "entities/protected_ref_access.json"
+ }
+ },
+ "additionalProperties": false
+ }
+}
diff --git a/spec/fixtures/api/schemas/protected_tags.json b/spec/fixtures/api/schemas/protected_tags.json
new file mode 100644
index 00000000000..731d0368a09
--- /dev/null
+++ b/spec/fixtures/api/schemas/protected_tags.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "protected_tag.json"
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/metadata.json b/spec/fixtures/api/schemas/public_api/v4/metadata.json
index fd219b95df8..f5a6aa86890 100644
--- a/spec/fixtures/api/schemas/public_api/v4/metadata.json
+++ b/spec/fixtures/api/schemas/public_api/v4/metadata.json
@@ -3,11 +3,16 @@
"required": [
"version",
"revision",
- "kas"
+ "kas",
+ "enterprise"
],
"properties": {
- "version": { "type": "string" },
- "revision": { "type": "string" },
+ "version": {
+ "type": "string"
+ },
+ "revision": {
+ "type": "string"
+ },
"kas": {
"type": "object",
"required": [
@@ -16,11 +21,26 @@
"version"
],
"properties": {
- "enabled": { "type": "boolean" },
- "externalUrl": { "type": ["string", "null"] },
- "version": { "type": ["string", "null"] }
+ "enabled": {
+ "type": "boolean"
+ },
+ "externalUrl": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "version": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
}
+ },
+ "enterprise": {
+ "type": "boolean"
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/spec/fixtures/gitlab/import_export/project.tar.gz b/spec/fixtures/gitlab/import_export/project.tar.gz
new file mode 100644
index 00000000000..eec5d6f66ce
--- /dev/null
+++ b/spec/fixtures/gitlab/import_export/project.tar.gz
Binary files differ
diff --git a/spec/fixtures/gitlab/import_export/uploads.tar.gz b/spec/fixtures/gitlab/import_export/uploads.tar.gz
new file mode 100644
index 00000000000..3f1aa18c113
--- /dev/null
+++ b/spec/fixtures/gitlab/import_export/uploads.tar.gz
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 a5bdd378f53..520328f1041 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
@@ -14,7 +14,7 @@ time_frame: 7d
data_source:
data_category: operational
instrumentation_class: Count
-performance_indicator_type:
+performance_indicator_type: []
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 4931285f6cf..1942f33e043 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
@@ -14,7 +14,7 @@ time_frame: 7d
data_source:
data_category: optional
instrumentation_class: Count
-performance_indicator_type:
+performance_indicator_type: []
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 39472af686d..a72ba5109cc 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
@@ -15,7 +15,7 @@ time_frame: 7d
data_source:
data_category: optional
instrumentation_class: Count
-performance_indicator_type:
+performance_indicator_type: []
distribution:
- ce
- ee
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index a03177ba85e..8e7cb487444 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -6961,7 +6961,7 @@
"id": 2,
"pipeline_id": 36,
"project_id": 5,
- "title": "Build pipeline"
+ "name": "Build pipeline"
},
"notes": [
{
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
index 0c19f23cc24..cadaa5abfcd 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
@@ -1,7 +1,7 @@
{"id":19,"project_id":5,"ref":"master","sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":24,"project_id":5,"pipeline_id":40,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":79,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.695Z","trace":"Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.","created_at":"2016-03-22T15:20:35.950Z","updated_at":"2016-03-29T06:28:12.696Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":40,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":80,"project_id":5,"status":"success","finished_at":null,"trace":"Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.","created_at":"2016-03-22T15:20:35.966Z","updated_at":"2016-03-22T15:20:35.966Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":40,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
{"id":20,"project_id":5,"ref":"master","sha":"ce84140e8b878ce6e7c4d298c7202ff38170e3ac","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":false,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[],"source":"external_pull_request_event","external_pull_request":{"id":3,"pull_request_iid":4,"source_branch":"feature","target_branch":"master","source_repository":"the-repository","target_repository":"the-repository","source_sha":"ce84140e8b878ce6e7c4d298c7202ff38170e3ac","target_sha":"a09386439ca39abe575675ffd4b89ae824fec22f","status":"open","created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z"}}
{"id":26,"project_id":5,"ref":"master","sha":"048721d90c449b244b7b4c53a9186b04330174ec","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.757Z","updated_at":"2016-03-22T15:20:35.757Z","tag":false,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"source":"merge_request_event","merge_request_id":27,"stages":[{"id":21,"project_id":5,"pipeline_id":37,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":74,"project_id":5,"status":"success","finished_at":null,"trace":"Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.","created_at":"2016-03-22T15:20:35.846Z","updated_at":"2016-03-22T15:20:35.846Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":73,"project_id":5,"status":"canceled","finished_at":null,"trace":null,"created_at":"2016-03-22T15:20:35.842Z","updated_at":"2016-03-22T15:20:35.842Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}],"merge_request":{"id":27,"target_branch":"feature","source_branch":"feature_conflict","source_project_id":2147483547,"author_id":1,"assignee_id":null,"title":"MR1","created_at":"2016-06-14T15:02:36.568Z","updated_at":"2016-06-14T15:02:56.815Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":9,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"diff_head_sha":"HEAD","source_branch_sha":"ABCD","target_branch_sha":"DCBA","merge_params":{"force_remove_source_branch":null}}}
-{"id":36,"project_id":5,"ref":null,"sha":"sha-notes","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.755Z","updated_at":"2016-03-22T15:20:35.755Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"user_id":2147483547,"duration":null,"source":"push","merge_request_id":null,"pipeline_metadata": {"id": 2, "project_id": 5, "pipeline_id": 36, "title": "Build pipeline"},"notes":[{"id":2147483547,"note":"Natus rerum qui dolorem dolorum voluptas.","noteable_type":"Commit","author_id":1,"created_at":"2016-03-22T15:19:59.469Z","updated_at":"2016-03-22T15:19:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":"be93687618e4b132087f430a4d8fc3a609c9b77c","noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"}}],"stages":[{"id":11,"project_id":5,"pipeline_id":36,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":71,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.630Z","trace":null,"created_at":"2016-03-22T15:20:35.772Z","updated_at":"2016-03-29T06:28:12.634Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":{"image":"busybox:latest"},"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"stage_id":11,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null,"type":"Ci::Build","token":"abcd","artifacts_file_store":1,"artifacts_metadata_store":1,"artifacts_size":10},{"id":72,"project_id":5,"status":"success","finished_at":null,"trace":"Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.","created_at":"2016-03-22T15:20:35.777Z","updated_at":"2016-03-22T15:20:35.777Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ deploy command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"deploy","trigger_request_id":null,"stage_idx":1,"stage_id":12,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]},{"id":12,"project_id":5,"pipeline_id":36,"name":"deploy","status":2,"created_at":"2016-03-22T15:45:45.772Z","updated_at":"2016-03-29T06:45:45.634Z"}]}
+{"id":36,"project_id":5,"ref":null,"sha":"sha-notes","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.755Z","updated_at":"2016-03-22T15:20:35.755Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"user_id":2147483547,"duration":null,"source":"push","merge_request_id":null,"pipeline_metadata": {"id": 2, "project_id": 5, "pipeline_id": 36, "name": "Build pipeline"},"notes":[{"id":2147483547,"note":"Natus rerum qui dolorem dolorum voluptas.","noteable_type":"Commit","author_id":1,"created_at":"2016-03-22T15:19:59.469Z","updated_at":"2016-03-22T15:19:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":"be93687618e4b132087f430a4d8fc3a609c9b77c","noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"}}],"stages":[{"id":11,"project_id":5,"pipeline_id":36,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":71,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.630Z","trace":null,"created_at":"2016-03-22T15:20:35.772Z","updated_at":"2016-03-29T06:28:12.634Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":{"image":"busybox:latest"},"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"stage_id":11,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null,"type":"Ci::Build","token":"abcd","artifacts_file_store":1,"artifacts_metadata_store":1,"artifacts_size":10},{"id":72,"project_id":5,"status":"success","finished_at":null,"trace":"Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.","created_at":"2016-03-22T15:20:35.777Z","updated_at":"2016-03-22T15:20:35.777Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ deploy command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"deploy","trigger_request_id":null,"stage_idx":1,"stage_id":12,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]},{"id":12,"project_id":5,"pipeline_id":36,"name":"deploy","status":2,"created_at":"2016-03-22T15:45:45.772Z","updated_at":"2016-03-29T06:45:45.634Z"}]}
{"id":38,"iid":1,"project_id":5,"ref":"master","sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.759Z","updated_at":"2016-03-22T15:20:35.759Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":22,"project_id":5,"pipeline_id":38,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":76,"project_id":5,"status":"success","finished_at":null,"trace":"Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.","created_at":"2016-03-22T15:20:35.882Z","updated_at":"2016-03-22T15:20:35.882Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":75,"project_id":5,"status":"failed","finished_at":null,"trace":"Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.","created_at":"2016-03-22T15:20:35.864Z","updated_at":"2016-03-22T15:20:35.864Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
{"id":39,"project_id":5,"ref":"master","sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.761Z","updated_at":"2016-03-22T15:20:35.761Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":23,"project_id":5,"pipeline_id":39,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":78,"project_id":5,"status":"success","finished_at":null,"trace":"Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.","created_at":"2016-03-22T15:20:35.927Z","updated_at":"2016-03-22T15:20:35.927Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":77,"project_id":5,"status":"failed","finished_at":null,"trace":"Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.","created_at":"2016-03-22T15:20:35.905Z","updated_at":"2016-03-22T15:20:35.905Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
{"id":41,"project_id":5,"ref":"master","sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[]}
diff --git a/spec/fixtures/lib/sbom/package-url-test-cases.json b/spec/fixtures/lib/sbom/package-url-test-cases.json
new file mode 100644
index 00000000000..448387397f6
--- /dev/null
+++ b/spec/fixtures/lib/sbom/package-url-test-cases.json
@@ -0,0 +1,502 @@
+[
+ {
+ "description": "valid maven purl",
+ "purl": "pkg:maven/org.apache.commons/io@1.3.4",
+ "canonical_purl": "pkg:maven/org.apache.commons/io@1.3.4",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": "1.3.4",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "basic valid maven purl without version",
+ "purl": "pkg:maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid go purl without version and with subpath",
+ "purl": "pkg:GOLANG/google.golang.org/genproto#/googleapis/api/annotations/",
+ "canonical_purl": "pkg:golang/google.golang.org/genproto#googleapis/api/annotations",
+ "type": "golang",
+ "namespace": "google.golang.org",
+ "name": "genproto",
+ "version": null,
+ "qualifiers": null,
+ "subpath": "googleapis/api/annotations",
+ "is_invalid": false
+ },
+ {
+ "description": "valid go purl with version and subpath",
+ "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#/googleapis/api/annotations/",
+ "canonical_purl": "pkg:golang/google.golang.org/genproto@abcdedf#googleapis/api/annotations",
+ "type": "golang",
+ "namespace": "google.golang.org",
+ "name": "genproto",
+ "version": "abcdedf",
+ "qualifiers": null,
+ "subpath": "googleapis/api/annotations",
+ "is_invalid": false
+ },
+ {
+ "description": "bitbucket namespace and name should be lowercased",
+ "purl": "pkg:bitbucket/birKenfeld/pyGments-main@244fd47e07d1014f0aed9c",
+ "canonical_purl": "pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c",
+ "type": "bitbucket",
+ "namespace": "birkenfeld",
+ "name": "pygments-main",
+ "version": "244fd47e07d1014f0aed9c",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "github namespace and name should be lowercased",
+ "purl": "pkg:github/Package-url/purl-Spec@244fd47e07d1004f0aed9c",
+ "canonical_purl": "pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
+ "type": "github",
+ "namespace": "package-url",
+ "name": "purl-spec",
+ "version": "244fd47e07d1004f0aed9c",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "debian can use qualifiers",
+ "purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie",
+ "canonical_purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie",
+ "type": "deb",
+ "namespace": "debian",
+ "name": "curl",
+ "version": "7.50.3-1",
+ "qualifiers": {
+ "arch": "i386",
+ "distro": "jessie"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "docker uses qualifiers and hash image id as versions",
+ "purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io",
+ "canonical_purl": "pkg:docker/customer/dockerimage@sha256%3A244fd47e07d1004f0aed9c?repository_url=gcr.io",
+ "type": "docker",
+ "namespace": "customer",
+ "name": "dockerimage",
+ "version": "sha256:244fd47e07d1004f0aed9c",
+ "qualifiers": {
+ "repository_url": "gcr.io"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "Java gem can use a qualifier",
+ "purl": "pkg:gem/jruby-launcher@1.1.2?Platform=java",
+ "canonical_purl": "pkg:gem/jruby-launcher@1.1.2?platform=java",
+ "type": "gem",
+ "namespace": null,
+ "name": "jruby-launcher",
+ "version": "1.1.2",
+ "qualifiers": {
+ "platform": "java"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "maven often uses qualifiers",
+ "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repositorY_url=repo.spring.io/release",
+ "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io%2Frelease",
+ "type": "maven",
+ "namespace": "org.apache.xmlgraphics",
+ "name": "batik-anim",
+ "version": "1.9.1",
+ "qualifiers": {
+ "classifier": "sources",
+ "repository_url": "repo.spring.io/release"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "maven pom reference",
+ "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repositorY_url=repo.spring.io/release",
+ "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repository_url=repo.spring.io%2Frelease",
+ "type": "maven",
+ "namespace": "org.apache.xmlgraphics",
+ "name": "batik-anim",
+ "version": "1.9.1",
+ "qualifiers": {
+ "extension": "pom",
+ "repository_url": "repo.spring.io/release"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "maven can come with a type qualifier",
+ "purl": "pkg:Maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll",
+ "canonical_purl": "pkg:maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll",
+ "type": "maven",
+ "namespace": "net.sf.jacob-project",
+ "name": "jacob",
+ "version": "1.14.3",
+ "qualifiers": {
+ "classifier": "x86",
+ "type": "dll"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "npm can be scoped",
+ "purl": "pkg:npm/%40angular/animation@12.3.1",
+ "canonical_purl": "pkg:npm/%40angular/animation@12.3.1",
+ "type": "npm",
+ "namespace": "@angular",
+ "name": "animation",
+ "version": "12.3.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "nuget names are case sensitive",
+ "purl": "pkg:Nuget/EnterpriseLibrary.Common@6.0.1304",
+ "canonical_purl": "pkg:nuget/EnterpriseLibrary.Common@6.0.1304",
+ "type": "nuget",
+ "namespace": null,
+ "name": "EnterpriseLibrary.Common",
+ "version": "6.0.1304",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "pypi names have special rules and not case sensitive",
+ "purl": "pkg:PYPI/Django_package@1.11.1.dev1",
+ "canonical_purl": "pkg:pypi/django-package@1.11.1.dev1",
+ "type": "pypi",
+ "namespace": null,
+ "name": "django-package",
+ "version": "1.11.1.dev1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "rpm often use qualifiers",
+ "purl": "pkg:Rpm/fedora/curl@7.50.3-1.fc25?Arch=i386&Distro=fedora-25",
+ "canonical_purl": "pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25",
+ "type": "rpm",
+ "namespace": "fedora",
+ "name": "curl",
+ "version": "7.50.3-1.fc25",
+ "qualifiers": {
+ "arch": "i386",
+ "distro": "fedora-25"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "a scheme is always required",
+ "purl": "EnterpriseLibrary.Common@6.0.1304",
+ "canonical_purl": "EnterpriseLibrary.Common@6.0.1304",
+ "type": null,
+ "namespace": null,
+ "name": "EnterpriseLibrary.Common",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "a type is always required",
+ "purl": "pkg:EnterpriseLibrary.Common@6.0.1304",
+ "canonical_purl": "pkg:EnterpriseLibrary.Common@6.0.1304",
+ "type": null,
+ "namespace": null,
+ "name": "EnterpriseLibrary.Common",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "a name is required",
+ "purl": "pkg:maven/@1.3.4",
+ "canonical_purl": "pkg:maven/@1.3.4",
+ "type": "maven",
+ "namespace": null,
+ "name": null,
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "slash / after scheme is not significant",
+ "purl": "pkg:/maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "double slash // after scheme is not significant",
+ "purl": "pkg://maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "slash /// after type is not significant",
+ "purl": "pkg:///maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid maven purl with case sensitive namespace and name",
+ "purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3",
+ "canonical_purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3",
+ "type": "maven",
+ "namespace": "HTTPClient",
+ "name": "HTTPClient",
+ "version": "0.3-3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid maven purl containing a space in the version and qualifier",
+ "purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value",
+ "canonical_purl": "pkg:maven/mygroup/myartifact@1.0.0+Final?mykey=my+value",
+ "type": "maven",
+ "namespace": "mygroup",
+ "name": "myartifact",
+ "version": "1.0.0 Final",
+ "qualifiers": {
+ "mykey": "my value"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "checks for invalid qualifier keys",
+ "purl": "pkg:npm/myartifact@1.0.0?in%20production=true",
+ "canonical_purl": null,
+ "type": "npm",
+ "namespace": null,
+ "name": "myartifact",
+ "version": "1.0.0",
+ "qualifiers": {
+ "in production": "true"
+ },
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid conan purl",
+ "purl": "pkg:conan/cctz@2.3",
+ "canonical_purl": "pkg:conan/cctz@2.3",
+ "type": "conan",
+ "namespace": null,
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid conan purl with namespace and qualifier channel",
+ "purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable",
+ "canonical_purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable",
+ "type": "conan",
+ "namespace": "bincrafters",
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": {
+ "channel": "stable"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "invalid conan purl only namespace",
+ "purl": "pkg:conan/bincrafters/cctz@2.3",
+ "canonical_purl": "pkg:conan/bincrafters/cctz@2.3",
+ "type": "conan",
+ "namespace": "bincrafters",
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid conan purl only channel qualifier",
+ "purl": "pkg:conan/cctz@2.3?channel=stable",
+ "canonical_purl": "pkg:conan/cctz@2.3?channel=stable",
+ "type": "conan",
+ "namespace": null,
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": {
+ "channel": "stable"
+ },
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid conda purl with qualifiers",
+ "purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2",
+ "canonical_purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2",
+ "type": "conda",
+ "namespace": null,
+ "name": "absl-py",
+ "version": "0.4.1",
+ "qualifiers": {
+ "build": "py36h06a4308_0",
+ "channel": "main",
+ "subdir": "linux-64",
+ "type": "tar.bz2"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid cran purl",
+ "purl": "pkg:cran/A3@0.9.1",
+ "canonical_purl": "pkg:cran/A3@0.9.1",
+ "type": "cran",
+ "namespace": null,
+ "name": "A3",
+ "version": "0.9.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "invalid cran purl without name",
+ "purl": "pkg:cran/@0.9.1",
+ "canonical_purl": "pkg:cran/@0.9.1",
+ "type": "cran",
+ "namespace": null,
+ "name": null,
+ "version": "0.9.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid cran purl without version",
+ "purl": "pkg:cran/A3",
+ "canonical_purl": "pkg:cran/A3",
+ "type": "cran",
+ "namespace": null,
+ "name": "A3",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid swift purl",
+ "purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3",
+ "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3",
+ "type": "swift",
+ "namespace": "github.com/Alamofire",
+ "name": "Alamofire",
+ "version": "5.4.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "invalid swift purl without namespace",
+ "purl": "pkg:swift/Alamofire@5.4.3",
+ "canonical_purl": "pkg:swift/Alamofire@5.4.3",
+ "type": "swift",
+ "namespace": null,
+ "name": "Alamofire",
+ "version": "5.4.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid swift purl without name",
+ "purl": "pkg:swift/github.com/Alamofire/@5.4.3",
+ "canonical_purl": "pkg:swift/github.com/Alamofire/@5.4.3",
+ "type": "swift",
+ "namespace": "github.com/Alamofire",
+ "name": null,
+ "version": "5.4.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid swift purl without version",
+ "purl": "pkg:swift/github.com/Alamofire/Alamofire",
+ "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire",
+ "type": "swift",
+ "namespace": "github.com/Alamofire",
+ "name": "Alamofire",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid hackage purl",
+ "purl": "pkg:hackage/AC-HalfInteger@1.2.1",
+ "canonical_purl": "pkg:hackage/AC-HalfInteger@1.2.1",
+ "type": "hackage",
+ "namespace": null,
+ "name": "AC-HalfInteger",
+ "version": "1.2.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "name and version are always required",
+ "purl": "pkg:hackage",
+ "canonical_purl": "pkg:hackage",
+ "type": "hackage",
+ "namespace": null,
+ "name": null,
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ }
+] \ No newline at end of file
diff --git a/spec/fixtures/markdown/markdown_golden_master_examples.yml b/spec/fixtures/markdown/markdown_golden_master_examples.yml
index 6a1e75348cf..0c7e6ab5cd2 100644
--- a/spec/fixtures/markdown/markdown_golden_master_examples.yml
+++ b/spec/fixtures/markdown/markdown_golden_master_examples.yml
@@ -297,7 +297,7 @@
```
html: |-
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
+ <pre data-sourcepos="1:1-3:3" lang="javascript" class="code highlight js-syntax-highlight language-javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
<copy-code></copy-code>
</div>
@@ -308,7 +308,7 @@
```
html: |-
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"> plaintext</span></code></pre>
+ <pre data-sourcepos="1:1-3:3" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"> plaintext</span></code></pre>
<copy-code></copy-code>
</div>
@@ -319,7 +319,7 @@
```
html: |-
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" data-canonical-lang="foobar" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"> custom_language = &gt;&gt; this &lt;&lt;</span></code></pre>
+ <pre data-sourcepos="1:1-3:3" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="foobar" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"> custom_language = &gt;&gt; this &lt;&lt;</span></code></pre>
<copy-code></copy-code>
</div>
@@ -531,7 +531,7 @@
;;;
html: |-
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-5:3" class="code highlight js-syntax-highlight language-json" lang="json" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="json"><span class="p">{</span></span>
+ <pre data-sourcepos="1:1-5:3" lang="json" class="code highlight js-syntax-highlight language-json" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="json"><span class="p">{</span></span>
<span id="LC2" class="line" lang="json"><span class="w"> </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Page title"</span></span>
<span id="LC3" class="line" lang="json"><span class="p">}</span></span></code></pre>
<copy-code></copy-code>
@@ -544,7 +544,7 @@
+++
html: |-
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-toml" lang="toml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="toml"><span class="py">title</span> <span class="p">=</span> <span class="s">"Page title"</span></span></code></pre>
+ <pre data-sourcepos="1:1-3:3" lang="toml" class="code highlight js-syntax-highlight language-toml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="toml"><span class="py">title</span> <span class="p">=</span> <span class="s">"Page title"</span></span></code></pre>
<copy-code></copy-code>
</div>
@@ -555,7 +555,7 @@
---
html: |-
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-yaml" lang="yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">Page title</span></span></code></pre>
+ <pre data-sourcepos="1:1-3:3" lang="yaml" class="code highlight js-syntax-highlight language-yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">Page title</span></span></code></pre>
<copy-code></copy-code>
</div>
@@ -690,7 +690,7 @@
<p data-sourcepos="1:1-1:36" dir="auto">This math is inline <code class="code math js-render-math" data-math-style="inline">a^2+b^2=c^2</code>.</p>
<p data-sourcepos="3:1-3:27" dir="auto">This is on a separate line:</p>
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="5:1-7:3" class="code highlight js-syntax-highlight language-math js-render-math" lang="math" v-pre="true" data-math-style="display"><code><span id="LC1" class="line" lang="math">a^2+b^2=c^2</span></code></pre>
+ <pre data-sourcepos="5:1-7:3" lang="math" data-math-style="display" class="js-render-math code highlight js-syntax-highlight language-math" v-pre="true"><code><span id="LC1" class="line" lang="math">a^2+b^2=c^2</span></code></pre>
<copy-code></copy-code>
</div>
diff --git a/spec/fixtures/packages/rpm/payload.json b/spec/fixtures/packages/rpm/payload.json
index 0240dbaca21..ef948c0bb6f 100644
--- a/spec/fixtures/packages/rpm/payload.json
+++ b/spec/fixtures/packages/rpm/payload.json
@@ -1,11 +1,23 @@
{
"files": [
- "/usr/bin/hello.sh"
+ "/usr/bin/test",
+ "/usr/bin/test/hello.sh"
+ ],
+ "directories": [
+ "/usr/bin/test/"
],
"changelogs": [
{
"changelogtext": "First build",
"changelogtime": 1662552000
+ },
+ {
+ "changelogtext": "Next build",
+ "changelogtime": 1662552123
+ },
+ {
+ "changelogtext": "Last build",
+ "changelogtime": 1662552321
}
],
"requirements": [
@@ -43,5 +55,7 @@
"group": "Unspecified",
"buildhost": "localhost",
"packager": null,
- "vendor": null
+ "vendor": null,
+ "pkgid": "qwe123wer234ert345",
+ "epoch": "1"
} \ No newline at end of file
diff --git a/spec/fixtures/packages/rpm/repodata/repomd.xml b/spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml
index 4554ee9a6d0..177a9be4723 100644
--- a/spec/fixtures/packages/rpm/repodata/repomd.xml
+++ b/spec/fixtures/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml
@@ -1,4 +1,7 @@
-<repomd xmlns="http://gitlab.com/api/v4/projects/1/packages/rpm/repodata/repomd.xml" xmlns:rpm="http://gitlab.com/api/v4/projects/1/packages/rpm/repodata/repomd.xml">
+<repomd
+ xmlns="http://gitlab.com/api/v4/projects/1/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml"
+ xmlns:rpm="http://gitlab.com/api/v4/projects/1/packages/rpm/repodata/364c77dd49e8f814d56e621d0b3306c4fd0696dcad506f527329b818eb0f5db3-repomd.xml"
+>
<revision>1644602779</revision>
<data type="filelists">
<checksum type="sha256">6503673de76312406ff8ecb06d9733c32b546a65abae4d4170d9b51fb75bf253</checksum>
diff --git a/spec/frontend/__helpers__/raw_transformer.js b/spec/frontend/__helpers__/raw_transformer.js
new file mode 100644
index 00000000000..09101b7a64f
--- /dev/null
+++ b/spec/frontend/__helpers__/raw_transformer.js
@@ -0,0 +1,6 @@
+/* eslint-disable import/no-commonjs */
+module.exports = {
+ process: (content) => {
+ return `module.exports = ${JSON.stringify(content)}`;
+ },
+};
diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
index 2bd2b17a12d..42818c14029 100644
--- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
+++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
@@ -21,6 +21,7 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi
mindate="Mon Jul 06 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
placeholder="YYYY-MM-DD"
showclearbutton="true"
+ size="medium"
theme=""
/>
</gl-form-group-stub>
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
index b4af11169ad..e4313bdfa26 100644
--- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -73,7 +73,6 @@ describe('~/access_tokens/components/new_access_token_app', () => {
expect(InputCopyToggleVisibilityComponent.props('copyButtonTitle')).toBe(
sprintf(__('Copy %{accessTokenType}'), { accessTokenType }),
);
- expect(InputCopyToggleVisibilityComponent.props('initialVisibility')).toBe(true);
expect(InputCopyToggleVisibilityComponent.attributes('label')).toBe(
sprintf(__('Your new %{accessTokenType}'), { accessTokenType }),
);
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
index 411126d0c89..e6718f62b91 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal, GlLink } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { shallowMount, mount, createWrapper } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
@@ -36,6 +36,7 @@ describe('Signup Form', () => {
const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group');
const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group');
const findUserCapInput = () => wrapper.findByTestId('user-cap-input');
+ const findUserCapFormGroup = () => wrapper.findByTestId('user-cap-form-group');
const findModal = () => wrapper.findComponent(GlModal);
afterEach(() => {
@@ -214,4 +215,19 @@ describe('Signup Form', () => {
});
});
});
+
+ describe('rendering help links within user cap description', () => {
+ beforeEach(() => {
+ mountComponent({ mountFn: mount });
+ });
+
+ it('renders projectSharingHelpLink and groupSharingHelpLink', () => {
+ const [projectSharingLink, groupSharingLink] = findUserCapFormGroup().findAllComponents(
+ GlLink,
+ ).wrappers;
+
+ expect(projectSharingLink.attributes('href')).toBe(mockData.projectSharingHelpLink);
+ expect(groupSharingLink.attributes('href')).toBe(mockData.groupSharingHelpLink);
+ });
+ });
});
diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js
index 9e001e122a4..dd1ed317497 100644
--- a/spec/frontend/admin/signup_restrictions/mock_data.js
+++ b/spec/frontend/admin/signup_restrictions/mock_data.js
@@ -4,6 +4,7 @@ export const rawMockData = {
signupEnabled: 'true',
requireAdminApprovalAfterUserSignup: 'true',
sendUserConfirmationEmail: 'true',
+ emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
minimumPasswordLengthMax: '10',
@@ -22,6 +23,8 @@ export const rawMockData = {
passwordLowercaseRequired: 'true',
passwordUppercaseRequired: 'true',
passwordSymbolRequired: 'true',
+ projectSharingHelpLink: 'project-sharing/help/link',
+ groupSharingHelpLink: 'group-sharing/help/link',
};
export const mockData = {
@@ -30,6 +33,7 @@ export const mockData = {
signupEnabled: true,
requireAdminApprovalAfterUserSignup: true,
sendUserConfirmationEmail: true,
+ emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
minimumPasswordLengthMax: '10',
@@ -48,4 +52,6 @@ export const mockData = {
passwordLowercaseRequired: true,
passwordUppercaseRequired: true,
passwordSymbolRequired: true,
+ projectSharingHelpLink: 'project-sharing/help/link',
+ groupSharingHelpLink: 'group-sharing/help/link',
};
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index 4967753b91c..8e9652332c1 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -1,13 +1,13 @@
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Actions from '~/admin/users/components/actions';
+import Delete from '~/admin/users/components/actions/delete.vue';
import eventHub, {
EVENT_OPEN_DELETE_USER_MODAL,
} from '~/admin/users/components/modals/delete_user_modal_event_hub';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
-import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
-import { paths } from '../../mock_data';
+import { CONFIRMATION_ACTIONS } from '../../constants';
+import { paths, userDeletionObstacles } from '../../mock_data';
describe('Action components', () => {
let wrapper;
@@ -41,40 +41,33 @@ describe('Action components', () => {
});
});
- describe('DELETE_ACTION_COMPONENTS', () => {
+ describe('DELETE', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation();
});
- const userDeletionObstacles = [
- { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
- { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
- ];
-
- it.each(DELETE_ACTIONS)(
- 'renders a dropdown item that opens the delete user modal when clicked for "%s"',
- async (action) => {
- initComponent({
- component: Actions[capitalizeFirstCharacter(action)],
- props: {
- username: 'John Doe',
- paths,
- userDeletionObstacles,
- },
- });
+ it('renders a dropdown item that opens the delete user modal when Delete is clicked', async () => {
+ initComponent({
+ component: Delete,
+ props: {
+ username: 'John Doe',
+ userId: 1,
+ paths,
+ userDeletionObstacles,
+ },
+ });
- await findDropdownItem().vm.$emit('click');
+ await findDropdownItem().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith(
- EVENT_OPEN_DELETE_USER_MODAL,
- expect.objectContaining({
- username: 'John Doe',
- blockPath: paths.block,
- deletePath: paths[action],
- userDeletionObstacles,
- }),
- );
- },
- );
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ EVENT_OPEN_DELETE_USER_MODAL,
+ expect.objectContaining({
+ username: 'John Doe',
+ blockPath: paths.block,
+ deletePath: paths.delete,
+ userDeletionObstacles,
+ }),
+ );
+ });
});
});
diff --git a/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
new file mode 100644
index 00000000000..64a88aab2c2
--- /dev/null
+++ b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
@@ -0,0 +1,107 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeleteWithContributions from '~/admin/users/components/actions/delete_with_contributions.vue';
+import eventHub, {
+ EVENT_OPEN_DELETE_USER_MODAL,
+} from '~/admin/users/components/modals/delete_user_modal_event_hub';
+import { associationsCount } from '~/api/user_api';
+import {
+ paths,
+ associationsCount as associationsCountData,
+ userDeletionObstacles,
+} from '../../mock_data';
+
+jest.mock('~/admin/users/components/modals/delete_user_modal_event_hub', () => ({
+ ...jest.requireActual('~/admin/users/components/modals/delete_user_modal_event_hub'),
+ __esModule: true,
+ default: {
+ $emit: jest.fn(),
+ },
+}));
+
+jest.mock('~/api/user_api', () => ({
+ associationsCount: jest.fn(),
+}));
+
+describe('DeleteWithContributions', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ username: 'John Doe',
+ userId: 1,
+ paths,
+ userDeletionObstacles,
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(DeleteWithContributions, { propsData: defaultPropsData });
+ };
+
+ describe('when action is clicked', () => {
+ describe('when API request is loading', () => {
+ beforeEach(() => {
+ associationsCount.mockReturnValueOnce(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ it('displays loading icon and disables button', async () => {
+ await wrapper.trigger('click');
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findByRole('menuitem').attributes()).toMatchObject({
+ disabled: 'disabled',
+ 'aria-busy': 'true',
+ });
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(() => {
+ associationsCount.mockResolvedValueOnce({
+ data: associationsCountData,
+ });
+
+ createComponent();
+ });
+
+ it('emits event with association counts', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+
+ expect(associationsCount).toHaveBeenCalledWith(defaultPropsData.userId);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ EVENT_OPEN_DELETE_USER_MODAL,
+ expect.objectContaining({
+ associationsCount: associationsCountData,
+ username: defaultPropsData.username,
+ blockPath: paths.block,
+ deletePath: paths.deleteWithContributions,
+ userDeletionObstacles,
+ }),
+ );
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ beforeEach(() => {
+ associationsCount.mockRejectedValueOnce();
+
+ createComponent();
+ });
+
+ it('emits event with error', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ EVENT_OPEN_DELETE_USER_MODAL,
+ expect.objectContaining({
+ associationsCount: new Error(),
+ }),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap
new file mode 100644
index 00000000000..4237685e45c
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AssociationsListItem renders interpolated message in a \`li\` element 1`] = `"<li><strong>5</strong> groups</li>"`;
diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap
new file mode 100644
index 00000000000..dc98d367af7
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AssociationsList when counts are 0 does not render items 1`] = `""`;
+
+exports[`AssociationsList when counts are plural renders plural counts 1`] = `
+"<ul class=\\"gl-mb-5\\">
+ <li><strong>2</strong> groups</li>
+ <li><strong>3</strong> projects</li>
+ <li><strong>4</strong> issues</li>
+ <li><strong>5</strong> merge requests</li>
+</ul>"
+`;
+
+exports[`AssociationsList when counts are singular renders singular counts 1`] = `
+"<ul class=\\"gl-mb-5\\">
+ <li><strong>1</strong> group</li>
+ <li><strong>1</strong> project</li>
+ <li><strong>1</strong> issue</li>
+ <li><strong>1</strong> merge request</li>
+</ul>"
+`;
+
+exports[`AssociationsList when there is an error displays an alert 1`] = `
+"<div class=\\"gl-mb-5 gl-alert gl-alert-not-dismissible gl-alert-danger\\"><svg data-testid=\\"error-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"gl-icon s16 gl-alert-icon gl-alert-icon-no-title\\">
+ <use href=\\"#error\\"></use>
+ </svg>
+ <div role=\\"alert\\" aria-live=\\"assertive\\" class=\\"gl-alert-content\\">
+ <!---->
+ <div class=\\"gl-alert-body\\">An error occurred while fetching this user's contributions, and the request cannot return the number of issues, merge requests, groups, and projects linked to this user. If you proceed with deleting the user, all their contributions will still be deleted.</div>
+ <!---->
+ </div>
+ <!---->
+</div>"
+`;
diff --git a/spec/frontend/admin/users/components/associations/associations_list_item_spec.js b/spec/frontend/admin/users/components/associations/associations_list_item_spec.js
new file mode 100644
index 00000000000..5126df12c24
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/associations_list_item_spec.js
@@ -0,0 +1,25 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import AssociationsListItem from '~/admin/users/components/associations/associations_list_item.vue';
+import { n__ } from '~/locale';
+
+describe('AssociationsListItem', () => {
+ let wrapper;
+ const count = 5;
+
+ const createComponent = () => {
+ wrapper = mountExtended(AssociationsListItem, {
+ propsData: {
+ message: n__('%{count} group', '%{count} groups', count),
+ count,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders interpolated message in a `li` element', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/admin/users/components/associations/associations_list_spec.js b/spec/frontend/admin/users/components/associations/associations_list_spec.js
new file mode 100644
index 00000000000..d77a645111f
--- /dev/null
+++ b/spec/frontend/admin/users/components/associations/associations_list_spec.js
@@ -0,0 +1,78 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import AssociationsList from '~/admin/users/components/associations/associations_list.vue';
+
+describe('AssociationsList', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ associationsCount: {
+ groups_count: 1,
+ projects_count: 1,
+ issues_count: 1,
+ merge_requests_count: 1,
+ },
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(AssociationsList, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('when there is an error', () => {
+ it('displays an alert', () => {
+ createComponent({
+ propsData: {
+ associationsCount: new Error(),
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('when counts are singular', () => {
+ it('renders singular counts', () => {
+ createComponent();
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('when counts are plural', () => {
+ it('renders plural counts', () => {
+ createComponent({
+ propsData: {
+ associationsCount: {
+ groups_count: 2,
+ projects_count: 3,
+ issues_count: 4,
+ merge_requests_count: 5,
+ },
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('when counts are 0', () => {
+ it('does not render items', () => {
+ createComponent({
+ propsData: {
+ associationsCount: {
+ groups_count: 0,
+ projects_count: 0,
+ issues_count: 0,
+ merge_requests_count: 0,
+ },
+ },
+ });
+
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index 70ed9eeb3e1..2e892e292d7 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -1,10 +1,12 @@
import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import eventHub, {
EVENT_OPEN_DELETE_USER_MODAL,
} from '~/admin/users/components/modals/delete_user_modal_event_hub';
import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+import AssociationsList from '~/admin/users/components/associations/associations_list.vue';
import ModalStub from './stubs/modal_stub';
const TEST_DELETE_USER_URL = 'delete-url';
@@ -200,4 +202,24 @@ describe('Delete user modal', () => {
expect(obstacles.props('obstacles')).toEqual(userDeletionObstacles);
});
});
+
+ it('renders `AssociationsList` component and passes `associationsCount` prop', async () => {
+ const associationsCount = {
+ groups_count: 5,
+ projects_count: 0,
+ issues_count: 5,
+ merge_requests_count: 5,
+ };
+
+ createComponent();
+ emitOpenModalEvent({
+ ...mockModalData,
+ associationsCount,
+ });
+ await nextTick();
+
+ expect(wrapper.findComponent(AssociationsList).props('associationsCount')).toEqual(
+ associationsCount,
+ );
+ });
});
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index ffc05e744c8..1b080b05c95 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -121,8 +121,11 @@ describe('AdminUserActions component', () => {
it.each(DELETE_ACTIONS)('renders a delete action component item for "%s"', (action) => {
const component = wrapper.findComponent(Actions[capitalizeFirstCharacter(action)]);
- expect(component.props('username')).toBe(user.name);
- expect(component.props('paths')).toEqual(userPaths);
+ expect(component.props()).toMatchObject({
+ username: user.name,
+ userId: user.id,
+ paths: userPaths,
+ });
expect(component.text()).toBe(I18N_USER_ACTIONS[action]);
});
});
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index 73fa73c0b47..193ac3fa043 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -1,3 +1,5 @@
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+
export const users = [
{
id: 2177,
@@ -48,3 +50,15 @@ export const createGroupCountResponse = (groupCounts) => ({
},
},
});
+
+export const associationsCount = {
+ groups_count: 5,
+ projects_count: 5,
+ issues_count: 5,
+ merge_requests_count: 5,
+};
+
+export const userDeletionObstacles = [
+ { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
+ { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
+];
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
index 7a09fe3319d..562e86529ee 100644
--- a/spec/frontend/analytics/shared/components/daterange_spec.js
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -77,7 +77,7 @@ describe('Daterange component', () => {
it('sets the tooltip', () => {
const tooltip = findDaterangePicker().props('tooltip');
expect(tooltip).toBe(
- 'Showing data for workflow items created in this date range. Date range limited to 30 days.',
+ 'Showing data for workflow items completed in this date range. Date range limited to 30 days.',
);
});
});
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index e14ead0b8eb..9de588a02aa 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -1,10 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
+import getGroupTransferLocationsResponse from 'test_fixtures/api/groups/transfer_locations.json';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
-import { updateGroup } from '~/api/groups_api';
+import { DEFAULT_PER_PAGE } from '~/api';
+import { updateGroup, getGroupTransferLocations } from '~/api/groups_api';
const mockApiVersion = 'v4';
const mockUrlRoot = '/gitlab';
+const mockGroupId = '99';
describe('GroupsApi', () => {
let originalGon;
@@ -27,7 +30,6 @@ describe('GroupsApi', () => {
});
describe('updateGroup', () => {
- const mockGroupId = '99';
const mockData = { attr: 'value' };
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}`;
@@ -43,4 +45,25 @@ describe('GroupsApi', () => {
expect(res.data).toMatchObject({ id: mockGroupId, ...mockData });
});
});
+
+ describe('getGroupTransferLocations', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ });
+
+ it('retrieves transfer locations from the correct URL and returns them in the response data', async () => {
+ const params = { page: 1 };
+ const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/transfer_locations`;
+
+ mock.onGet(expectedUrl).replyOnce(200, { data: getGroupTransferLocationsResponse });
+
+ await expect(getGroupTransferLocations(mockGroupId, params)).resolves.toMatchObject({
+ data: { data: getGroupTransferLocationsResponse },
+ });
+
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
+ params: { ...params, per_page: DEFAULT_PER_PAGE },
+ });
+ });
+ });
});
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index ee7194bdf5f..ba6b73e8c1a 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -1,7 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
-import { followUser, unfollowUser } from '~/api/user_api';
+import { followUser, unfollowUser, associationsCount } from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
+import { associationsCount as associationsCountData } from 'jest/admin/users/mock_data';
describe('~/api/user_api', () => {
let axiosMock;
@@ -47,4 +48,18 @@ describe('~/api/user_api', () => {
expect(axiosMock.history.post[0].url).toBe(expectedUrl);
});
});
+
+ describe('associationsCount', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/users/1/associations_count';
+ const expectedResponse = { data: associationsCountData };
+
+ axiosMock.onGet(expectedUrl).replyOnce(200, expectedResponse);
+
+ await expect(associationsCount(1)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.get[0].url).toBe(expectedUrl);
+ });
+ });
});
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/artifacts/components/artifact_row_spec.js
new file mode 100644
index 00000000000..dcc0d684f13
--- /dev/null
+++ b/spec/frontend/artifacts/components/artifact_row_spec.js
@@ -0,0 +1,67 @@
+import { GlBadge, GlButton, GlFriendlyWrap } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactRow from '~/artifacts/components/artifact_row.vue';
+
+describe('ArtifactRow component', () => {
+ let wrapper;
+
+ const artifact = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0];
+
+ const findName = () => wrapper.findByTestId('job-artifact-row-name');
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findSize = () => wrapper.findByTestId('job-artifact-row-size');
+ const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+
+ const createComponent = (mountFn = shallowMountExtended) => {
+ wrapper = mountFn(ArtifactRow, {
+ propsData: {
+ artifact,
+ isLoading: false,
+ isLastRow: false,
+ },
+ stubs: { GlBadge, GlButton, GlFriendlyWrap },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('artifact details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('displays the artifact name and type', () => {
+ expect(findName().text()).toContain(artifact.name);
+ expect(findBadge().text()).toBe(artifact.fileType.toLowerCase());
+ });
+
+ it('displays the artifact size', () => {
+ expect(findSize().text()).toBe(numberToHumanSize(artifact.size));
+ });
+
+ it('displays the download button as a link to the download path', () => {
+ expect(findDownloadButton().attributes('href')).toBe(artifact.downloadPath);
+ });
+
+ it('displays the delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('emits the delete event when the delete button is clicked', async () => {
+ expect(wrapper.emitted('delete')).toBeUndefined();
+
+ findDeleteButton().trigger('click');
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
new file mode 100644
index 00000000000..c6ad13462f9
--- /dev/null
+++ b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
@@ -0,0 +1,122 @@
+import { GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactRow from '~/artifacts/components/artifact_row.vue';
+import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
+import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants';
+import { createAlert } from '~/flash';
+
+jest.mock('~/flash');
+
+const { artifacts } = getJobArtifactsResponse.data.project.jobs.nodes[0];
+const refetchArtifacts = jest.fn();
+
+Vue.use(VueApollo);
+
+describe('ArtifactsTableRowDetails component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const createComponent = (
+ handlers = {
+ destroyArtifactMutation: jest.fn(),
+ },
+ ) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(ArtifactsTableRowDetails, {
+ apolloProvider: createMockApollo([
+ [destroyArtifactMutation, requestHandlers.destroyArtifactMutation],
+ ]),
+ propsData: {
+ artifacts,
+ refetchArtifacts,
+ queryVariables: {},
+ },
+ data() {
+ return { deletingArtifactId: null };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('passes correct props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('to the artifact rows', () => {
+ [0, 1, 2].forEach((index) => {
+ expect(wrapper.findAllComponents(ArtifactRow).at(index).props()).toMatchObject({
+ artifact: artifacts.nodes[index],
+ });
+ });
+ });
+ });
+
+ describe('when the artifact row emits the delete event', () => {
+ it('shows the artifact delete modal', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(false);
+
+ await wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+
+ expect(findModal().props('visible')).toBe(true);
+ expect(findModal().props('title')).toBe(I18N_MODAL_TITLE(artifacts.nodes[0].name));
+ });
+ });
+
+ describe('when the artifact delete modal emits its primary event', () => {
+ it('triggers the destroyArtifact GraphQL mutation', async () => {
+ createComponent();
+ await waitForPromises();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+
+ expect(requestHandlers.destroyArtifactMutation).toHaveBeenCalledWith({
+ id: artifacts.nodes[0].id,
+ });
+ });
+
+ it('displays a flash message and refetches artifacts when the mutation fails', async () => {
+ createComponent({
+ destroyArtifactMutation: jest.fn().mockRejectedValue(new Error('Error!')),
+ });
+ await waitForPromises();
+
+ expect(wrapper.emitted('refetch')).toBeUndefined();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_DESTROY_ERROR });
+ expect(wrapper.emitted('refetch')).toBeDefined();
+ });
+ });
+
+ describe('when the artifact delete modal is cancelled', () => {
+ it('does not trigger the destroyArtifact GraphQL mutation', async () => {
+ createComponent();
+ await waitForPromises();
+
+ wrapper.findComponent(ArtifactRow).vm.$emit('delete');
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('cancel');
+
+ expect(requestHandlers.destroyArtifactMutation).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
new file mode 100644
index 00000000000..131b4b99bb2
--- /dev/null
+++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
@@ -0,0 +1,341 @@
+import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination, GlModal } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
+import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ARCHIVE_FILE_TYPE, JOBS_PER_PAGE, I18N_FETCH_ERROR } from '~/artifacts/constants';
+import { totalArtifactsSizeForJob } from '~/artifacts/utils';
+import { createAlert } from '~/flash';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+describe('JobArtifactsTable component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
+ const findDetailsInRow = (i) =>
+ findTable().findAll('tbody tr').at(i).findComponent(ArtifactsTableRowDetails);
+
+ const findCount = () => wrapper.findByTestId('job-artifacts-count');
+ const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i);
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
+ const findSuccessfulJobStatus = () => findStatuses().at(0);
+ const findFailedJobStatus = () => findStatuses().at(1);
+
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findJobLink = () => findLinks().at(0);
+ const findPipelineLink = () => findLinks().at(1);
+ const findRefLink = () => findLinks().at(2);
+ const findCommitLink = () => findLinks().at(3);
+
+ const findSize = () => wrapper.findByTestId('job-artifacts-size');
+ const findCreated = () => wrapper.findByTestId('job-artifacts-created');
+
+ const findDownloadButton = () => wrapper.findByTestId('job-artifacts-download-button');
+ const findBrowseButton = () => wrapper.findByTestId('job-artifacts-browse-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button');
+ const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const setPage = async (page) => {
+ findPagination().vm.$emit('input', page);
+ await waitForPromises();
+ };
+
+ let enoughJobsToPaginate = [...getJobArtifactsResponse.data.project.jobs.nodes];
+ while (enoughJobsToPaginate.length <= JOBS_PER_PAGE) {
+ enoughJobsToPaginate = [
+ ...enoughJobsToPaginate,
+ ...getJobArtifactsResponse.data.project.jobs.nodes,
+ ];
+ }
+ const getJobArtifactsResponseThatPaginates = {
+ data: { project: { jobs: { nodes: enoughJobsToPaginate } } },
+ };
+
+ const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
+ const archiveArtifact = job.artifacts.nodes.find(
+ (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
+ );
+
+ const createComponent = (
+ handlers = {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ },
+ data = {},
+ ) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(JobArtifactsTable, {
+ apolloProvider: createMockApollo([
+ [getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
+ ]),
+ provide: { projectPath: 'project/path' },
+ data() {
+ return data;
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('when loading, shows a loading state', () => {
+ createComponent();
+
+ expect(findLoadingState().exists()).toBe(true);
+ });
+
+ it('on error, shows an alert', async () => {
+ createComponent({
+ getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')),
+ });
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_FETCH_ERROR });
+ });
+
+ it('with data, renders the table', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
+
+ describe('job details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('shows the artifact count', () => {
+ expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`);
+ });
+
+ it('shows the job status as an icon for a successful job', () => {
+ expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true);
+ expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false);
+ });
+
+ it('shows the job status as a badge for other job statuses', () => {
+ expect(findFailedJobStatus().findComponent(GlBadge).exists()).toBe(true);
+ expect(findFailedJobStatus().findComponent(CiIcon).exists()).toBe(false);
+ });
+
+ it('shows links to the job, pipeline, ref, and commit', () => {
+ expect(findJobLink().text()).toBe(job.name);
+ expect(findJobLink().attributes('href')).toBe(job.webPath);
+
+ expect(findPipelineLink().text()).toBe(`#${getIdFromGraphQLId(job.pipeline.id)}`);
+ expect(findPipelineLink().attributes('href')).toBe(job.pipeline.path);
+
+ expect(findRefLink().text()).toBe(job.refName);
+ expect(findRefLink().attributes('href')).toBe(job.refPath);
+
+ expect(findCommitLink().text()).toBe(job.shortSha);
+ expect(findCommitLink().attributes('href')).toBe(job.commitPath);
+ });
+
+ it('shows the total size of artifacts', () => {
+ expect(findSize().text()).toBe(totalArtifactsSizeForJob(job));
+ });
+
+ it('shows the created time', () => {
+ expect(findCreated().text()).toBe('5 years ago');
+ });
+
+ describe('row expansion', () => {
+ it('toggles the visibility of the row details', async () => {
+ expect(findDetailsRows().length).toBe(0);
+
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsRows().length).toBe(1);
+
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsRows().length).toBe(0);
+ });
+
+ it('expands and collapses jobs', async () => {
+ // both jobs start collapsed
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+
+ findCountAt(0).trigger('click');
+ await waitForPromises();
+
+ // first job is expanded, second row has its details
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+
+ findCountAt(1).trigger('click');
+ await waitForPromises();
+
+ // both jobs are expanded, each has details below it
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+ expect(findDetailsInRow(3).exists()).toBe(true);
+
+ findCountAt(0).trigger('click');
+ await waitForPromises();
+
+ // first job collapsed, second job expanded
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+ expect(findDetailsInRow(2).exists()).toBe(true);
+ });
+
+ it('keeps the job expanded when an artifact is deleted', async () => {
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+
+ findArtifactDeleteButton().trigger('click');
+ await waitForPromises();
+
+ expect(findModal().props('visible')).toBe(true);
+
+ wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('download button', () => {
+ it('is a link to the download path for the archive artifact', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('href')).toBe(archiveArtifact.downloadPath);
+ });
+
+ it('is disabled when there is no download path', async () => {
+ const jobWithoutDownloadPath = {
+ ...job,
+ archive: { downloadPath: null },
+ };
+
+ createComponent(
+ { getJobArtifactsQuery: jest.fn() },
+ { jobArtifacts: [jobWithoutDownloadPath] },
+ );
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('browse button', () => {
+ it('is a link to the browse path for the job', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('href')).toBe(job.browseArtifactsPath);
+ });
+
+ it('is disabled when there is no browse path', async () => {
+ const jobWithoutBrowsePath = {
+ ...job,
+ browseArtifactsPath: null,
+ };
+
+ createComponent(
+ { getJobArtifactsQuery: jest.fn() },
+ { jobArtifacts: [jobWithoutBrowsePath] },
+ );
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('delete button', () => {
+ it('shows a disabled delete button for now (coming soon)', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDeleteButton().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('pagination', () => {
+ const { pageInfo } = getJobArtifactsResponse.data.project.jobs;
+
+ beforeEach(async () => {
+ createComponent(
+ {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates),
+ },
+ {
+ count: enoughJobsToPaginate.length,
+ pageInfo,
+ },
+ );
+
+ await waitForPromises();
+ });
+
+ it('renders pagination and passes page props', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toMatchObject({
+ value: wrapper.vm.pagination.currentPage,
+ prevPage: wrapper.vm.prevPage,
+ nextPage: wrapper.vm.nextPage,
+ });
+ });
+
+ it('updates query variables when going to previous page', () => {
+ return setPage(1).then(() => {
+ expect(wrapper.vm.queryVariables).toMatchObject({
+ projectPath: 'project/path',
+ nextPageCursor: undefined,
+ prevPageCursor: pageInfo.startCursor,
+ });
+ });
+ });
+
+ it('updates query variables when going to next page', () => {
+ return setPage(2).then(() => {
+ expect(wrapper.vm.queryVariables).toMatchObject({
+ lastPageSize: null,
+ nextPageCursor: pageInfo.endCursor,
+ prevPageCursor: '',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/graphql/cache_update_spec.js b/spec/frontend/artifacts/graphql/cache_update_spec.js
new file mode 100644
index 00000000000..4d610328298
--- /dev/null
+++ b/spec/frontend/artifacts/graphql/cache_update_spec.js
@@ -0,0 +1,67 @@
+import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { removeArtifactFromStore } from '~/artifacts/graphql/cache_update';
+
+describe('Artifact table cache updates', () => {
+ let store;
+
+ const cacheMock = {
+ project: {
+ jobs: {
+ nodes: [
+ { artifacts: { nodes: [{ id: 'foo' }] } },
+ { artifacts: { nodes: [{ id: 'bar' }] } },
+ ],
+ },
+ },
+ };
+
+ const query = getJobArtifactsQuery;
+ const variables = { fullPath: 'path/to/project' };
+
+ beforeEach(() => {
+ store = {
+ readQuery: jest.fn().mockReturnValue(cacheMock),
+ writeQuery: jest.fn(),
+ };
+ });
+
+ describe('removeArtifactFromStore', () => {
+ it('calls readQuery', () => {
+ removeArtifactFromStore(store, 'foo', query, variables);
+ expect(store.readQuery).toHaveBeenCalledWith({ query, variables });
+ });
+
+ it('writes the correct result in the cache', () => {
+ removeArtifactFromStore(store, 'foo', query, variables);
+ expect(store.writeQuery).toHaveBeenCalledWith({
+ query,
+ variables,
+ data: {
+ project: {
+ jobs: {
+ nodes: [{ artifacts: { nodes: [] } }, { artifacts: { nodes: [{ id: 'bar' }] } }],
+ },
+ },
+ },
+ });
+ });
+
+ it('does not remove an unknown artifact', () => {
+ removeArtifactFromStore(store, 'baz', query, variables);
+ expect(store.writeQuery).toHaveBeenCalledWith({
+ query,
+ variables,
+ data: {
+ project: {
+ jobs: {
+ nodes: [
+ { artifacts: { nodes: [{ id: 'foo' }] } },
+ { artifacts: { nodes: [{ id: 'bar' }] } },
+ ],
+ },
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
index 2b9442162aa..de0e5063e49 100644
--- a/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
+++ b/spec/frontend/behaviors/markdown/render_sandboxed_mermaid_spec.js
@@ -1,34 +1,127 @@
-import $ from 'jquery';
+import { createWrapper } from '@vue/test-utils';
+import { __ } from '~/locale';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import renderMermaid from '~/behaviors/markdown/render_sandboxed_mermaid';
+import renderMermaid, {
+ MAX_CHAR_LIMIT,
+ MAX_MERMAID_BLOCK_LIMIT,
+ LAZY_ALERT_SHOWN_CLASS,
+} from '~/behaviors/markdown/render_sandboxed_mermaid';
-describe('Render mermaid diagrams for Gitlab Flavoured Markdown', () => {
- it('Does something', () => {
- document.body.dataset.page = '';
- setHTMLFixture(`
- <div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-7:3" class="code highlight js-syntax-highlight language-mermaid white" lang="mermaid" id="code-4">
- <code class="js-render-mermaid">
- <span id="LC1" class="line" lang="mermaid">graph TD;</span>
- <span id="LC2" class="line" lang="mermaid">A--&gt;B</span>
- <span id="LC3" class="line" lang="mermaid">A--&gt;C</span>
- <span id="LC4" class="line" lang="mermaid">B--&gt;D</span>
- <span id="LC5" class="line" lang="mermaid">C--&gt;D</span>
- </code>
- </pre>
- <copy-code>
- <button type="button" class="btn btn-default btn-md gl-button btn-icon has-tooltip" data-title="Copy to clipboard" data-clipboard-target="pre#code-4">
- <svg><use xlink:href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#copy-to-clipboard"></use></svg>
- </button>
- </copy-code>
- </div>`);
- const els = $('pre.js-syntax-highlight').find('.js-render-mermaid');
-
- renderMermaid(els);
+describe('Mermaid diagrams renderer', () => {
+ // Finders
+ const findMermaidIframes = () => document.querySelectorAll('iframe[src="/-/sandbox/mermaid"]');
+ const findDangerousMermaidAlert = () =>
+ createWrapper(document.querySelector('[data-testid="alert-warning"]'));
+ // Helpers
+ const renderDiagrams = () => {
+ renderMermaid([...document.querySelectorAll('.js-render-mermaid')]);
jest.runAllTimers();
- expect(document.querySelector('pre.js-syntax-highlight').classList).toContain('gl-sr-only');
+ };
+
+ beforeEach(() => {
+ document.body.dataset.page = '';
+ });
+ afterEach(() => {
resetHTMLFixture();
});
+
+ it('renders a mermaid diagram', () => {
+ setHTMLFixture('<pre><code class="js-render-mermaid"></code></pre>');
+
+ expect(findMermaidIframes()).toHaveLength(0);
+
+ renderDiagrams();
+
+ expect(document.querySelector('pre').classList).toContain('gl-sr-only');
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+
+ describe('within a details element', () => {
+ beforeEach(() => {
+ setHTMLFixture('<details><pre><code class="js-render-mermaid"></code></pre></details>');
+ renderDiagrams();
+ });
+
+ it('does not render the diagram on load', () => {
+ expect(findMermaidIframes()).toHaveLength(0);
+ });
+
+ it('render the diagram when the details element is opened', () => {
+ document.querySelector('details').setAttribute('open', true);
+ document.querySelector('details').dispatchEvent(new Event('toggle'));
+ jest.runAllTimers();
+
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+ });
+
+ describe('dangerous diagrams', () => {
+ describe(`when the diagram's source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
+ beforeEach(() => {
+ setHTMLFixture(
+ `<pre>
+ <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT + 1)
+ .fill('a')
+ .join('')}</code>
+ </pre>`,
+ );
+ renderDiagrams();
+ });
+ it('does not render the diagram on load', () => {
+ expect(findMermaidIframes()).toHaveLength(0);
+ });
+
+ it('shows a warning about performance impact when rendering the diagram', () => {
+ expect(document.querySelector('pre').classList).toContain(LAZY_ALERT_SHOWN_CLASS);
+ expect(findDangerousMermaidAlert().exists()).toBe(true);
+ expect(findDangerousMermaidAlert().text()).toContain(
+ __('Warning: Displaying this diagram might cause performance issues on this page.'),
+ );
+ });
+
+ it("renders the diagram when clicking on the alert's button", () => {
+ findDangerousMermaidAlert().find('button').trigger('click');
+ jest.runAllTimers();
+
+ expect(findMermaidIframes()).toHaveLength(1);
+ });
+ });
+
+ it(`stops rendering diagrams once the total rendered source exceeds ${MAX_CHAR_LIMIT} characters`, () => {
+ setHTMLFixture(
+ `<pre>
+ <code class="js-render-mermaid">${Array(MAX_CHAR_LIMIT - 1)
+ .fill('a')
+ .join('')}</code>
+ <code class="js-render-mermaid">2</code>
+ <code class="js-render-mermaid">3</code>
+ <code class="js-render-mermaid">4</code>
+ </pre>`,
+ );
+ renderDiagrams();
+
+ expect(findMermaidIframes()).toHaveLength(3);
+ });
+
+ // Note: The test case below is provided for convenience but should remain skipped as the DOM
+ // operations it requires are too expensive and would significantly slow down the test suite.
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip(`stops rendering diagrams when the rendered diagrams count exceeds ${MAX_MERMAID_BLOCK_LIMIT}`, () => {
+ setHTMLFixture(
+ `<pre>
+ ${Array(MAX_MERMAID_BLOCK_LIMIT + 1)
+ .fill('<code class="js-render-mermaid"></code>')
+ .join('')}
+ </pre>`,
+ );
+ renderDiagrams();
+
+ expect([...document.querySelectorAll('.js-render-mermaid')]).toHaveLength(
+ MAX_MERMAID_BLOCK_LIMIT + 1,
+ );
+ expect(findMermaidIframes()).toHaveLength(MAX_MERMAID_BLOCK_LIMIT);
+ });
+ });
});
diff --git a/spec/frontend/blob/blob_blame_link_spec.js b/spec/frontend/blob/blob_blame_link_spec.js
index 060e8803520..18adeed1f02 100644
--- a/spec/frontend/blob/blob_blame_link_spec.js
+++ b/spec/frontend/blob/blob_blame_link_spec.js
@@ -1,5 +1,5 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import addBlameLink from '~/blob/blob_blame_link';
+import { addBlameLink } from '~/blob/blob_blame_link';
describe('Blob links', () => {
const mouseoverEvent = new MouseEvent('mouseover', {
@@ -10,9 +10,10 @@ describe('Blob links', () => {
beforeEach(() => {
setHTMLFixture(`
- <div id="blob-content-holder">
+ <div id="blob-content-holder" class="js-per-page" data-blame-per-page="1000">
<div class="line-numbers" data-blame-path="/blamePath">
<a id="L5" href="#L5" data-line-number="5" class="file-line-num js-line-links">5</a>
+ <a id="L1005" href="#L1005" data-line-number="1005" class="file-line-num js-line-links">1005</a>
</div>
<pre id="LC5">Line 5 content</pre>
</div>
@@ -44,4 +45,11 @@ describe('Blob links', () => {
expect(lineLink).not.toBeNull();
expect(lineLink.getAttribute('href')).toBe('#L5');
});
+
+ it('adds page parameter when needed', () => {
+ document.querySelectorAll('.file-line-num')[1].dispatchEvent(mouseoverEvent);
+ const blameLink = document.querySelectorAll('.file-line-blame')[1];
+ expect(blameLink).not.toBeNull();
+ expect(blameLink.getAttribute('href')).toBe('/blamePath?page=2#L1005');
+ });
});
diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap
deleted file mode 100644
index 72761c18b3d..00000000000
--- a/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Blob Header Editing rendering matches the snapshot 1`] = `
-<div
- class="file-content code"
->
- <div
- data-editor-loading=""
- id="editor"
- >
- <pre
- class="editor-loading-content"
- >
- Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- </pre>
- </div>
-</div>
-`;
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js
deleted file mode 100644
index 5017b624292..00000000000
--- a/spec/frontend/blob/components/blob_edit_content_spec.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import BlobEditContent from '~/blob/components/blob_edit_content.vue';
-import * as utils from '~/blob/utils';
-
-jest.mock('~/editor/source_editor');
-
-describe('Blob Header Editing', () => {
- let wrapper;
- const onDidChangeModelContent = jest.fn();
- const updateModelLanguage = jest.fn();
- const getValue = jest.fn();
- const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
- const fileName = 'lorem.txt';
- const fileGlobalId = 'snippet_777';
-
- function createComponent(props = {}) {
- wrapper = shallowMount(BlobEditContent, {
- propsData: {
- value,
- fileName,
- fileGlobalId,
- ...props,
- },
- });
- }
-
- beforeEach(() => {
- jest.spyOn(utils, 'initSourceEditor').mockImplementation(() => ({
- onDidChangeModelContent,
- updateModelLanguage,
- getValue,
- dispose: jest.fn(),
- }));
-
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const triggerChangeContent = (val) => {
- getValue.mockReturnValue(val);
- const [cb] = onDidChangeModelContent.mock.calls[0];
-
- cb();
-
- jest.runOnlyPendingTimers();
- };
-
- describe('rendering', () => {
- it('matches the snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders content', () => {
- expect(wrapper.text()).toContain(value);
- });
- });
-
- describe('functionality', () => {
- it('does not fail without content', () => {
- const spy = jest.spyOn(global.console, 'error');
- createComponent({ value: undefined });
-
- expect(spy).not.toHaveBeenCalled();
- expect(wrapper.find('#editor').exists()).toBe(true);
- });
-
- it('initialises Source Editor', () => {
- const el = wrapper.findComponent({ ref: 'editor' }).element;
- expect(utils.initSourceEditor).toHaveBeenCalledWith({
- el,
- blobPath: fileName,
- blobGlobalId: fileGlobalId,
- blobContent: value,
- });
- });
-
- it('reacts to the changes in fileName', () => {
- const newFileName = 'ipsum.txt';
-
- wrapper.setProps({
- fileName: newFileName,
- });
-
- return nextTick().then(() => {
- expect(updateModelLanguage).toHaveBeenCalledWith(newFileName);
- });
- });
-
- it('registers callback with editor onChangeContent', () => {
- expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
- });
-
- it('emits input event when the blob content is changed', () => {
- expect(wrapper.emitted().input).toBeUndefined();
-
- triggerChangeContent(value);
-
- expect(wrapper.emitted().input).toEqual([[value]]);
- });
- });
-});
diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js
index a543c0060cb..24f70acb093 100644
--- a/spec/frontend/blob/utils_spec.js
+++ b/spec/frontend/blob/utils_spec.js
@@ -1,44 +1,32 @@
import * as utils from '~/blob/utils';
-import Editor from '~/editor/source_editor';
-
-jest.mock('~/editor/source_editor');
describe('Blob utilities', () => {
- describe('initSourceEditor', () => {
- let editorEl;
- const blobPath = 'foo.txt';
- const blobContent = 'Foo bar';
- const blobGlobalId = 'snippet_777';
-
- beforeEach(() => {
- editorEl = document.createElement('div');
+ describe('getPageParamValue', () => {
+ it('returns empty string if no perPage parameter is provided', () => {
+ const pageParamValue = utils.getPageParamValue(5);
+ expect(pageParamValue).toEqual('');
});
-
- describe('Monaco editor', () => {
- it('initializes the Source Editor', () => {
- utils.initSourceEditor({ el: editorEl });
- expect(Editor).toHaveBeenCalledWith({
- scrollbar: {
- alwaysConsumeMouseWheel: false,
- },
- });
- });
-
- it.each([[{}], [{ blobPath, blobContent, blobGlobalId }]])(
- 'creates the instance with the passed parameters %s',
- (extraParams) => {
- const params = {
- el: editorEl,
- ...extraParams,
- };
-
- expect(Editor.prototype.createInstance).not.toHaveBeenCalled();
-
- utils.initSourceEditor(params);
-
- expect(Editor.prototype.createInstance).toHaveBeenCalledWith(params);
- },
- );
+ it('returns empty string if page is equal 1', () => {
+ const pageParamValue = utils.getPageParamValue(1000, 1000);
+ expect(pageParamValue).toEqual('');
+ });
+ it('returns correct page parameter value', () => {
+ const pageParamValue = utils.getPageParamValue(1001, 1000);
+ expect(pageParamValue).toEqual(2);
+ });
+ it('accepts strings as a parameter and returns correct result', () => {
+ const pageParamValue = utils.getPageParamValue('1001', '1000');
+ expect(pageParamValue).toEqual(2);
+ });
+ });
+ describe('getPageSearchString', () => {
+ it('returns empty search string if page parameter is empty value', () => {
+ const path = utils.getPageSearchString('/blamePath', '');
+ expect(path).toEqual('');
+ });
+ it('returns correct search string if value is provided', () => {
+ const searchString = utils.getPageSearchString('/blamePath', 3);
+ expect(searchString).toEqual('?page=3');
});
});
});
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index c031cae11df..dda46e97b85 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -1,3 +1,4 @@
+import MockAdapter from 'axios-mock-adapter';
import { Emitter } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
@@ -8,6 +9,7 @@ import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markd
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
+import axios from '~/lib/utils/axios_utils';
jest.mock('~/editor/source_editor');
jest.mock('~/editor/extensions/source_editor_extension_base');
@@ -32,6 +34,7 @@ const markdownExtensions = [
describe('Blob Editing', () => {
let blobInstance;
+ let mock;
const useMock = jest.fn(() => markdownExtensions);
const unuseMock = jest.fn();
const emitter = new Emitter();
@@ -44,7 +47,10 @@ describe('Blob Editing', () => {
onDidChangeModelLanguage: emitter.event,
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
setHTMLFixture(`
+ <div class="js-edit-mode-pane"></div>
+ <div class="js-edit-mode"><a href="#write">Write</a><a href="#preview">Preview</a></div>
<form class="js-edit-blob-form">
<div id="file_path"></div>
<div id="editor"></div>
@@ -54,6 +60,7 @@ describe('Blob Editing', () => {
jest.spyOn(SourceEditor.prototype, 'createInstance').mockReturnValue(mockInstance);
});
afterEach(() => {
+ mock.restore();
jest.clearAllMocks();
unuseMock.mockClear();
useMock.mockClear();
@@ -108,6 +115,47 @@ describe('Blob Editing', () => {
});
});
+ describe('correctly handles toggling the live-preview panel for different file types', () => {
+ it.each`
+ desc | isMarkdown | isPreviewOpened | tabToClick | shouldOpenPreview | shouldClosePreview | expectedDesc
+ ${'not markdown with preview closed'} | ${false} | ${false} | ${'#write'} | ${false} | ${false} | ${'not toggle preview'}
+ ${'not markdown with preview closed'} | ${false} | ${false} | ${'#preview'} | ${false} | ${false} | ${'not toggle preview'}
+ ${'markdown with preview closed'} | ${true} | ${false} | ${'#write'} | ${false} | ${false} | ${'not toggle preview'}
+ ${'markdown with preview closed'} | ${true} | ${false} | ${'#preview'} | ${true} | ${false} | ${'open preview'}
+ ${'markdown with preview opened'} | ${true} | ${true} | ${'#write'} | ${false} | ${true} | ${'close preview'}
+ ${'markdown with preview opened'} | ${true} | ${true} | ${'#preview'} | ${false} | ${false} | ${'not toggle preview'}
+ `(
+ 'when $desc, clicking $tabToClick should $expectedDesc',
+ async ({
+ isMarkdown,
+ isPreviewOpened,
+ tabToClick,
+ shouldOpenPreview,
+ shouldClosePreview,
+ }) => {
+ const fire = jest.fn();
+ SourceEditor.prototype.createInstance = jest.fn().mockReturnValue({
+ ...mockInstance,
+ markdownPreview: {
+ eventEmitter: {
+ fire,
+ },
+ },
+ });
+ await initEditor(isMarkdown);
+ blobInstance.markdownLivePreviewOpened = isPreviewOpened;
+ const elToClick = document.querySelector(`a[href='${tabToClick}']`);
+ elToClick.dispatchEvent(new Event('click'));
+
+ if (shouldOpenPreview || shouldClosePreview) {
+ expect(fire).toHaveBeenCalled();
+ } else {
+ expect(fire).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
+
it('adds trailing newline to the blob content on submit', async () => {
const form = document.querySelector('.js-edit-blob-form');
const fileContentEl = document.getElementById('file-content');
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 3ebc51c4bcb..d05e057095d 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -7,7 +7,6 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
-import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
@@ -49,12 +48,11 @@ describe('Board card component', () => {
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon');
- const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition);
const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const performSearchMock = jest.fn();
- const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => {
+ const createStore = ({ isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
actions: {
@@ -67,13 +65,12 @@ describe('Board card component', () => {
},
getters: {
isGroupBoard: () => true,
- isEpicBoard: () => isEpicBoard,
isProjectBoard: () => isProjectBoard,
},
});
};
- const createWrapper = (props = {}) => {
+ const createWrapper = ({ props = {}, isEpicBoard = false } = {}) => {
wrapper = mountExtended(BoardCardInner, {
store,
propsData: {
@@ -99,6 +96,7 @@ describe('Board card component', () => {
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
+ isEpicBoard,
},
});
};
@@ -113,7 +111,7 @@ describe('Board card component', () => {
};
createStore();
- createWrapper({ item: issue, list });
+ createWrapper({ props: { item: issue, list } });
});
afterEach(() => {
@@ -143,16 +141,12 @@ describe('Board card component', () => {
expect(findHiddenIssueIcon().exists()).toBe(false);
});
- it('renders the move to position icon', () => {
- expect(findMoveToPositionComponent().exists()).toBe(true);
- });
-
it('does not render the work type icon by default', () => {
expect(findWorkItemIcon().exists()).toBe(false);
});
it('renders the work type icon when props is passed', () => {
- createWrapper({ item: issue, list, showWorkItemTypeIcon: true });
+ createWrapper({ props: { item: issue, list, showWorkItemTypeIcon: true } });
expect(findWorkItemIcon().exists()).toBe(true);
expect(findWorkItemIcon().props('workItemType')).toBe(issue.type);
});
@@ -183,9 +177,11 @@ describe('Board card component', () => {
describe('blocked', () => {
it('renders blocked icon if issue is blocked', async () => {
createWrapper({
- item: {
- ...issue,
- blocked: true,
+ props: {
+ item: {
+ ...issue,
+ blocked: true,
+ },
},
});
@@ -194,9 +190,11 @@ describe('Board card component', () => {
it('does not show blocked icon if issue is not blocked', () => {
createWrapper({
- item: {
- ...issue,
- blocked: false,
+ props: {
+ item: {
+ ...issue,
+ blocked: false,
+ },
},
});
@@ -207,9 +205,11 @@ describe('Board card component', () => {
describe('confidential issue', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- confidential: true,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ confidential: true,
+ },
},
});
});
@@ -222,9 +222,11 @@ describe('Board card component', () => {
describe('hidden issue', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- hidden: true,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ hidden: true,
+ },
},
});
});
@@ -247,11 +249,13 @@ describe('Board card component', () => {
describe('with avatar', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees: [user],
- updateData(newData) {
- Object.assign(this, newData);
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees: [user],
+ updateData(newData) {
+ Object.assign(this, newData);
+ },
},
},
});
@@ -300,15 +304,17 @@ describe('Board card component', () => {
global.gon.default_avatar_url = 'default_avatar';
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees: [
- {
- id: 1,
- name: 'testing 123',
- username: 'test',
- },
- ],
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees: [
+ {
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ },
+ ],
+ },
},
});
});
@@ -329,28 +335,30 @@ describe('Board card component', () => {
describe('multiple assignees', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees: [
- {
- id: 2,
- name: 'user2',
- username: 'user2',
- avatarUrl: 'test_image',
- },
- {
- id: 3,
- name: 'user3',
- username: 'user3',
- avatarUrl: 'test_image',
- },
- {
- id: 4,
- name: 'user4',
- username: 'user4',
- avatarUrl: 'test_image',
- },
- ],
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees: [
+ {
+ id: 2,
+ name: 'user2',
+ username: 'user2',
+ avatarUrl: 'test_image',
+ },
+ {
+ id: 3,
+ name: 'user3',
+ username: 'user3',
+ avatarUrl: 'test_image',
+ },
+ {
+ id: 4,
+ name: 'user4',
+ username: 'user4',
+ avatarUrl: 'test_image',
+ },
+ ],
+ },
},
});
});
@@ -370,9 +378,11 @@ describe('Board card component', () => {
});
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees,
+ },
},
});
});
@@ -396,9 +406,11 @@ describe('Board card component', () => {
})),
];
createWrapper({
- item: {
- ...wrapper.props('item'),
- assignees,
+ props: {
+ item: {
+ ...wrapper.props('item'),
+ assignees,
+ },
},
});
@@ -411,7 +423,7 @@ describe('Board card component', () => {
describe('labels', () => {
beforeEach(() => {
- createWrapper({ item: { ...issue, labels: [list.label, label1] } });
+ createWrapper({ props: { item: { ...issue, labels: [list.label, label1] } } });
});
it('does not render list label but renders all other labels', () => {
@@ -423,7 +435,7 @@ describe('Board card component', () => {
});
it('does not render label if label does not have an ID', async () => {
- createWrapper({ item: { ...issue, labels: [label1, { title: 'closed' }] } });
+ createWrapper({ props: { item: { ...issue, labels: [label1, { title: 'closed' }] } } });
await nextTick();
@@ -435,11 +447,13 @@ describe('Board card component', () => {
describe('filterByLabel method', () => {
beforeEach(() => {
createWrapper({
- item: {
- ...issue,
- labels: [label1],
+ props: {
+ item: {
+ ...issue,
+ labels: [label1],
+ },
+ updateFilters: true,
},
- updateFilters: true,
});
});
@@ -486,9 +500,11 @@ describe('Board card component', () => {
describe('loading', () => {
it('renders loading icon', async () => {
createWrapper({
- item: {
- ...issue,
- isLoading: true,
+ props: {
+ item: {
+ ...issue,
+ isLoading: true,
+ },
},
});
@@ -510,17 +526,20 @@ describe('Board card component', () => {
};
beforeEach(() => {
- createStore({ isEpicBoard: true });
+ createStore();
});
it('should render if the item has issues', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum,
- hasIssues: true,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts,
+ descendantWeightSum,
+ hasIssues: true,
+ },
},
+ isEpicBoard: true,
});
expect(findEpicCountables().exists()).toBe(true);
@@ -541,18 +560,21 @@ describe('Board card component', () => {
it('shows render item countBadge, weights, and progress correctly', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
- },
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts: {
+ ...descendantCounts,
+ openedIssues: 1,
+ },
+ descendantWeightSum: {
+ closedIssues: 10,
+ openedIssues: 5,
+ },
+ hasIssues: true,
},
- hasIssues: true,
},
+ isEpicBoard: true,
});
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
@@ -562,15 +584,18 @@ describe('Board card component', () => {
it('does not render progress when weight is zero', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts: {
- ...descendantCounts,
- openedIssues: 1,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts: {
+ ...descendantCounts,
+ openedIssues: 1,
+ },
+ descendantWeightSum,
+ hasIssues: true,
},
- descendantWeightSum,
- hasIssues: true,
},
+ isEpicBoard: true,
});
expect(findEpicBadgeProgress().exists()).toBe(false);
@@ -578,15 +603,18 @@ describe('Board card component', () => {
it('renders the tooltip with the correct data', () => {
createWrapper({
- item: {
- ...issue,
- descendantCounts,
- descendantWeightSum: {
- closedIssues: 10,
- openedIssues: 5,
+ props: {
+ item: {
+ ...issue,
+ descendantCounts,
+ descendantWeightSum: {
+ closedIssues: 10,
+ openedIssues: 5,
+ },
+ hasIssues: true,
},
- hasIssues: true,
},
+ isEpicBoard: true,
});
const tooltip = findEpicCountablesTotalTooltip();
@@ -595,10 +623,5 @@ describe('Board card component', () => {
expect(findEpicCountablesTotalWeight().text()).toBe('15');
expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed');
});
-
- it('does not render the move to position icon', () => {
- createWrapper();
- expect(findMoveToPositionComponent().exists()).toBe(false);
- });
});
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 65a41c49e7f..c5c3faf1712 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -101,6 +101,8 @@ export default function createComponent({
weightFeatureAvailable: false,
boardWeight: null,
canAdminList: true,
+ isIssueBoard: true,
+ isEpicBoard: false,
...provide,
},
stubs,
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 9b0c0b93ffb..3a2beb714e9 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createComponent from 'jest/boards/board_list_helper';
import BoardCard from '~/boards/components/board_card.vue';
import eventHub from '~/boards/eventhub';
+import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import { mockIssues } from './mock_data';
@@ -15,6 +16,7 @@ describe('Board list component', () => {
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]');
const findDraggable = () => wrapper.findComponent(Draggable);
+ const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition);
const startDrag = (
params = {
@@ -99,6 +101,10 @@ describe('Board list component', () => {
await nextTick();
expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
});
+
+ it('renders the move to position icon', () => {
+ expect(findMoveToPositionComponent().exists()).toBe(true);
+ });
});
describe('load more issues', () => {
diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js
index 5fae1c4359f..a3b2988ce75 100644
--- a/spec/frontend/boards/components/board_add_new_column_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_spec.js
@@ -53,11 +53,11 @@ describe('Board card layout', () => {
state: {
labels,
labelsLoading: false,
- isEpicBoard: false,
},
}),
provide: {
scopedLabelsAvailable: true,
+ isEpicBoard: false,
},
}),
);
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index dee097bfb08..c209f2f82e6 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -28,6 +28,7 @@ describe('BoardApp', () => {
store,
provide: {
...provide,
+ fullBoardId: 'gid://gitlab/Board/1',
},
});
};
diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js
index 7254b9486ef..8dee3c77787 100644
--- a/spec/frontend/boards/components/board_card_move_to_position_spec.js
+++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js
@@ -48,6 +48,7 @@ describe('Board Card Move to position', () => {
propsData: {
item: mockIssue2,
list: mockList,
+ listItemsLength: 3,
index: 0,
...propsData,
},
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 2feaa5dff8c..38b79e2e3f3 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -30,7 +30,6 @@ describe('Board card', () => {
},
actions: mockActions,
getters: {
- isEpicBoard: () => false,
isProjectBoard: () => false,
},
});
@@ -61,6 +60,7 @@ describe('Board card', () => {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
+ isEpicBoard: false,
...provide,
},
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 97d9e08f5d4..b2138700602 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,15 +1,20 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
+import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
-import { mockLists } from '../mock_data';
+import { mockLists, boardListsQueryResponse } from '../mock_data';
+Vue.use(VueApollo);
Vue.use(Vuex);
const actions = {
@@ -18,6 +23,7 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
+ let fakeApollo;
window.gon = {};
const defaultState = {
@@ -35,26 +41,68 @@ describe('BoardContent', () => {
});
};
- const createComponent = ({ state, props = {}, canAdminList = true } = {}) => {
+ const createComponent = ({
+ state,
+ props = {},
+ canAdminList = true,
+ isApolloBoard = false,
+ issuableType = 'issue',
+ isIssueBoard = true,
+ isEpicBoard = false,
+ boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
+ } = {}) => {
+ fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
+
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
+ apolloProvider: fakeApollo,
propsData: {
- lists: mockLists,
disabled: false,
+ boardId: 'gid://gitlab/Board/1',
...props,
},
provide: {
canAdminList,
+ boardType: 'group',
+ fullPath: 'gitlab-org/gitlab',
+ issuableType,
+ isIssueBoard,
+ isEpicBoard,
+ isApolloBoard,
},
store,
});
};
+ beforeAll(() => {
+ global.ResizeObserver = class MockResizeObserver {
+ constructor(callback) {
+ this.callback = callback;
+
+ this.entries = [];
+ }
+
+ observe(entry) {
+ this.entries.push(entry);
+ }
+
+ disconnect() {
+ this.entries = [];
+ this.callback = null;
+ }
+
+ trigger() {
+ this.callback(this.entries);
+ }
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
+ fakeApollo = null;
});
describe('default', () => {
@@ -74,11 +122,22 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(EpicsSwimlanes).exists()).toBe(false);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
+
+ it('resizes the list on resize', async () => {
+ window.innerHeight = 1000;
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
+
+ wrapper.vm.resizeObserver.trigger();
+
+ await nextTick();
+
+ expect(wrapper.findComponent({ ref: 'list' }).attributes('style')).toBe('height: 900px;');
+ });
});
describe('when issuableType is not issue', () => {
beforeEach(() => {
- createComponent({ state: { issuableType: 'foo' } });
+ createComponent({ issuableType: 'foo', isIssueBoard: false });
});
it('does not render BoardContentSidebar', () => {
@@ -105,4 +164,19 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(Draggable).exists()).toBe(false);
});
});
+
+ describe('when Apollo boards FF is on', () => {
+ beforeEach(async () => {
+ createComponent({ isApolloBoard: true });
+ await waitForPromises();
+ });
+
+ it('renders a BoardColumn component per list', () => {
+ expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
+ });
+
+ it('renders BoardContentSidebar', () => {
+ expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 1a07b9f0b78..6f17e4193a3 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -3,7 +3,19 @@ import Vue from 'vue';
import Vuex from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
import * as urlUtility from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
+import {
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_HEALTH,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_WEIGHT,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@@ -17,7 +29,7 @@ describe('BoardFilteredSearch', () => {
const tokens = [
{
icon: 'labels',
- title: __('Label'),
+ title: TOKEN_TITLE_LABEL,
type: 'label',
operators: [
{ value: '=', description: 'is' },
@@ -30,7 +42,7 @@ describe('BoardFilteredSearch', () => {
},
{
icon: 'pencil',
- title: __('Author'),
+ title: TOKEN_TITLE_AUTHOR,
type: 'author',
operators: [
{ value: '=', description: 'is' },
@@ -117,16 +129,16 @@ describe('BoardFilteredSearch', () => {
it('sets the url params to the correct results', async () => {
const mockFilters = [
- { type: 'author', value: { data: 'root', operator: '=' } },
- { type: 'assignee', value: { data: 'root', operator: '=' } },
- { type: 'label', value: { data: 'label', operator: '=' } },
- { type: 'label', value: { data: 'label&2', operator: '=' } },
- { type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
- { type: 'type', value: { data: 'INCIDENT', operator: '=' } },
- { type: 'weight', value: { data: '2', operator: '=' } },
- { type: 'iteration', value: { data: 'Any&3', operator: '=' } },
- { type: 'release', value: { data: 'v1.0.0', operator: '=' } },
- { type: 'health_status', value: { data: 'onTrack', operator: '=' } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'root', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label&2', operator: '=' } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'New Milestone', operator: '=' } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'INCIDENT', operator: '=' } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: '2', operator: '=' } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: 'Any&3', operator: '=' } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v1.0.0', operator: '=' } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: '=' } },
];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
@@ -170,9 +182,9 @@ describe('BoardFilteredSearch', () => {
it('passes the correct props to FilterSearchBar', () => {
expect(findFilteredSearch().props('initialFilterValue')).toEqual([
- { type: 'author', value: { data: 'root', operator: '=' } },
- { type: 'label', value: { data: 'label', operator: '=' } },
- { type: 'health_status', value: { data: 'Any', operator: '=' } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'label', operator: '=' } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'Any', operator: '=' } },
]);
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 50901f3fe84..4633612891c 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -59,7 +59,6 @@ describe('Board List Header Component', () => {
store = new Vuex.Store({
state: {},
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
- getters: { isEpicBoard: () => false },
});
fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
@@ -76,6 +75,7 @@ describe('Board List Header Component', () => {
boardId,
weightFeatureAvailable: false,
currentUserId,
+ isEpicBoard: false,
},
}),
);
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 4171a6236de..7d602042685 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -45,6 +45,7 @@ describe('BoardSettingsSidebar', () => {
provide: {
canAdminList,
scopedLabelsAvailable: false,
+ isIssueBoard: true,
},
directives: {
GlModal: createMockDirective(),
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index 997768a0cc7..08b5042f70f 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -15,18 +15,14 @@ describe('BoardTopBar', () => {
Vue.use(Vuex);
- const createStore = ({ mockGetters = {} } = {}) => {
+ const createStore = () => {
return new Vuex.Store({
state: {},
- getters: {
- isEpicBoard: () => false,
- ...mockGetters,
- },
});
};
- const createComponent = ({ provide = {}, mockGetters = {} } = {}) => {
- const store = createStore({ mockGetters });
+ const createComponent = ({ provide = {} } = {}) => {
+ const store = createStore();
wrapper = shallowMount(BoardTopBar, {
store,
provide: {
@@ -36,6 +32,7 @@ describe('BoardTopBar', () => {
fullPath: 'gitlab-org',
boardType: 'group',
releasesFetchPath: '/releases',
+ isIssueBoard: true,
...provide,
},
stubs: { IssueBoardFilteredSearch },
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index dc1f3246be0..3c26fa97338 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,7 +1,22 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
-import { __ } from '~/locale';
+import {
+ OPERATOR_IS_AND_IS_NOT,
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_TYPE,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@@ -433,8 +448,11 @@ export const mockList = {
label: null,
assignee: null,
milestone: null,
+ iteration: null,
loading: false,
issuesCount: 1,
+ maxIssueCount: 0,
+ __typename: 'BoardList',
};
export const mockLabelList = {
@@ -449,11 +467,15 @@ export const mockLabelList = {
color: '#F0AD4E',
textColor: '#FFFFFF',
description: null,
+ descriptionHtml: null,
},
assignee: null,
milestone: null,
+ iteration: null,
loading: false,
issuesCount: 0,
+ maxIssueCount: 0,
+ __typename: 'BoardList',
};
export const mockMilestoneList = {
@@ -725,7 +747,7 @@ export const mockConfidentialToken = {
title: 'Confidential',
unique: true,
token: GlFilteredSearchToken,
- operators: [{ value: '=', description: 'is' }],
+ operators: OPERATOR_IS_ONLY,
options: [
{ icon: 'eye-slash', value: 'yes', title: 'Yes' },
{ icon: 'eye', value: 'no', title: 'No' },
@@ -735,12 +757,9 @@ export const mockConfidentialToken = {
export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedIn) => [
{
icon: 'user',
- title: __('Assignee'),
- type: 'assignee',
- operators: [
- { value: '=', description: 'is' },
- { value: '!=', description: 'is not' },
- ],
+ title: TOKEN_TITLE_ASSIGNEE,
+ type: TOKEN_TYPE_ASSIGNEE,
+ operators: OPERATOR_IS_AND_IS_NOT,
token: AuthorToken,
unique: true,
fetchAuthors,
@@ -748,12 +767,9 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
},
{
icon: 'pencil',
- title: __('Author'),
- type: 'author',
- operators: [
- { value: '=', description: 'is' },
- { value: '!=', description: 'is not' },
- ],
+ title: TOKEN_TITLE_AUTHOR,
+ type: TOKEN_TYPE_AUTHOR,
+ operators: OPERATOR_IS_AND_IS_NOT,
symbol: '@',
token: AuthorToken,
unique: true,
@@ -762,12 +778,9 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
},
{
icon: 'labels',
- title: __('Label'),
- type: 'label',
- operators: [
- { value: '=', description: 'is' },
- { value: '!=', description: 'is not' },
- ],
+ title: TOKEN_TITLE_LABEL,
+ type: TOKEN_TYPE_LABEL,
+ operators: OPERATOR_IS_AND_IS_NOT,
token: LabelToken,
unique: false,
symbol: '~',
@@ -776,9 +789,9 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
...(isSignedIn ? [mockEmojiToken, mockConfidentialToken] : []),
{
icon: 'clock',
- title: __('Milestone'),
+ title: TOKEN_TITLE_MILESTONE,
symbol: '%',
- type: 'milestone',
+ type: TOKEN_TYPE_MILESTONE,
shouldSkipSort: true,
token: MilestoneToken,
unique: true,
@@ -786,8 +799,8 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
},
{
icon: 'issues',
- title: __('Type'),
- type: 'type',
+ title: TOKEN_TITLE_TYPE,
+ type: TOKEN_TYPE_TYPE,
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -796,8 +809,8 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones, isSignedI
],
},
{
- type: 'release',
- title: __('Release'),
+ type: TOKEN_TYPE_RELEASE,
+ title: TOKEN_TITLE_RELEASE,
icon: 'rocket',
token: ReleaseToken,
fetchReleases: expect.any(Function),
@@ -844,6 +857,22 @@ export const mockGroupLabelsResponse = {
},
};
+export const boardListsQueryResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/1',
+ board: {
+ id: 'gid://gitlab/Board/1',
+ hideBacklogList: false,
+ lists: {
+ nodes: mockLists,
+ },
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
export const boardListQueryResponse = (issuesCount = 20) => ({
data: {
boardList: {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 78859525a63..b3e90e34161 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -8,7 +8,6 @@ import {
ListType,
issuableTypes,
BoardType,
- listsQuery,
DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
@@ -21,7 +20,7 @@ import {
getMoveData,
updateListPosition,
} from 'ee_else_ce/boards/boards_util';
-import { gqlClient } from '~/boards/graphql';
+import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql';
import actions from '~/boards/stores/actions';
@@ -318,21 +317,18 @@ describe('fetchLists', () => {
};
const variables = {
- query: listsQuery[issuableType].query,
- variables: {
- fullPath: 'gitlab-org',
- boardId: fullBoardId,
- filters: {},
- isGroup,
- isProject,
- },
+ fullPath: 'gitlab-org',
+ boardId: fullBoardId,
+ filters: {},
+ isGroup,
+ isProject,
};
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
await actions.fetchLists({ commit, state, dispatch });
- expect(gqlClient.query).toHaveBeenCalledWith(variables);
+ expect(gqlClient.query).toHaveBeenCalledWith(expect.objectContaining({ variables }));
},
);
});
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
new file mode 100644
index 00000000000..6aab3b51806
--- /dev/null
+++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
@@ -0,0 +1,139 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Delete merged branches component Delete merged branches confirmation modal matches snapshot 1`] = `
+<div>
+ <b-button-stub
+ class="gl-mr-3 gl-button btn-danger-secondary"
+ data-qa-selector="delete_merged_branches_button"
+ size="md"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Delete merged branches
+
+ </span>
+ </b-button-stub>
+
+ <div>
+ <form
+ action="/namespace/project/-/merged_branches"
+ method="post"
+ >
+ <p>
+ You are about to
+ <strong>
+ delete all branches
+ </strong>
+ that were merged into
+ <code>
+ master
+ </code>
+ .
+ </p>
+
+ <p>
+
+ This may include merged branches that are not visible on the current screen.
+
+ </p>
+
+ <p>
+
+ A branch won't be deleted if it is protected or associated with an open merge request.
+
+ </p>
+
+ <p>
+ This bulk action is
+ <strong>
+ permanent and cannot be undone or recovered
+ </strong>
+ .
+ </p>
+
+ <p>
+ Plese type the following to confirm:
+ <code>
+ delete
+ </code>
+ .
+ <b-form-input-stub
+ aria-labelledby="input-label"
+ autocomplete="off"
+ class="gl-form-input gl-mt-2 gl-form-input-sm"
+ data-qa-selector="delete_merged_branches_input"
+ debounce="0"
+ formatter="[Function]"
+ type="text"
+ value=""
+ />
+ </p>
+
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="mock-csrf-token"
+ />
+ </form>
+ <div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3"
+ >
+ <b-button-stub
+ class="gl-button"
+ data-testid="delete-merged-branches-cancel-button"
+ size="md"
+ tag="button"
+ type="button"
+ variant="default"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Cancel
+
+ </span>
+ </b-button-stub>
+
+ <b-button-stub
+ class="gl-button"
+ data-qa-selector="delete_merged_branches_confirmation_button"
+ data-testid="delete-merged-branches-confirmation-button"
+ disabled="true"
+ size="md"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Delete merged branches
+ </span>
+ </b-button-stub>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
new file mode 100644
index 00000000000..4f1e772f4a4
--- /dev/null
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -0,0 +1,143 @@
+import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import DeleteMergedBranches, { i18n } from '~/branches/components/delete_merged_branches.vue';
+import { formPath, propsDataMock } from '../mock_data';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+let wrapper;
+
+const stubsData = {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+};
+
+const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
+ wrapper = mountFn(DeleteMergedBranches, {
+ propsData: {
+ ...propsDataMock,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs,
+ });
+};
+
+const findDeleteButton = () => wrapper.findComponent(GlButton);
+const findModal = () => wrapper.findComponent(GlModal);
+const findConfirmationButton = () =>
+ wrapper.findByTestId('delete-merged-branches-confirmation-button');
+const findCancelButton = () => wrapper.findByTestId('delete-merged-branches-cancel-button');
+const findFormInput = () => wrapper.findComponent(GlFormInput);
+const findForm = () => wrapper.find('form');
+const submitFormSpy = () => jest.spyOn(wrapper.vm.$refs.form, 'submit');
+
+describe('Delete merged branches component', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Delete merged branches button', () => {
+ it('has correct attributes, text and tooltip', () => {
+ expect(findDeleteButton().attributes()).toMatchObject({
+ category: 'secondary',
+ variant: 'danger',
+ });
+
+ expect(findDeleteButton().text()).toBe(i18n.deleteButtonText);
+ });
+
+ it('displays a tooltip', () => {
+ const tooltip = getBinding(findDeleteButton().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(wrapper.vm.buttonTooltipText);
+ });
+
+ it('opens modal when clicked', () => {
+ createComponent(mount);
+ jest.spyOn(wrapper.vm.$refs.modal, 'show');
+ findDeleteButton().trigger('click');
+
+ expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ });
+ });
+
+ describe('Delete merged branches confirmation modal', () => {
+ beforeEach(() => {
+ createComponent(shallowMountExtended, stubsData);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correct modal title and text', () => {
+ const modalText = findModal().text();
+ expect(findModal().props('title')).toBe(i18n.modalTitle);
+ expect(modalText).toContain(i18n.notVisibleBranchesWarning);
+ expect(modalText).toContain(i18n.protectedBranchWarning);
+ });
+
+ it('renders confirm and cancel buttons with correct text', () => {
+ expect(findConfirmationButton().text()).toContain(i18n.deleteButtonText);
+ expect(findCancelButton().text()).toContain(i18n.cancelButtonText);
+ });
+
+ it('renders form with correct attributes and hiden inputs', () => {
+ const form = findForm();
+ expect(form.attributes()).toEqual({
+ action: formPath,
+ method: 'post',
+ });
+ expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
+ expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
+ 'mock-csrf-token',
+ );
+ });
+
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a disabled confirm button by default', () => {
+ expect(findConfirmationButton().props('disabled')).toBe(true);
+ });
+
+ it('keeps disabled state when wrong input is provided', async () => {
+ findFormInput().vm.$emit('input', 'hello');
+ await waitForPromises();
+ expect(findConfirmationButton().props('disabled')).toBe(true);
+ findConfirmationButton().trigger('click');
+
+ expect(submitFormSpy()).not.toHaveBeenCalled();
+ findFormInput().trigger('keyup.enter');
+
+ expect(submitFormSpy()).not.toHaveBeenCalled();
+ });
+
+ it('submits form when correct amount is provided and the confirm button is clicked', async () => {
+ findFormInput().vm.$emit('input', 'delete');
+ await waitForPromises();
+ expect(findDeleteButton().props('disabled')).not.toBe(true);
+ findConfirmationButton().trigger('click');
+ expect(submitFormSpy()).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();
+ });
+ });
+});
diff --git a/spec/frontend/branches/mock_data.js b/spec/frontend/branches/mock_data.js
new file mode 100644
index 00000000000..9e8839d8ce9
--- /dev/null
+++ b/spec/frontend/branches/mock_data.js
@@ -0,0 +1,7 @@
+export const formPath = '/namespace/project/-/merged_branches';
+const defaultBranch = 'master';
+
+export const propsDataMock = {
+ formPath,
+ defaultBranch,
+};
diff --git a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
new file mode 100644
index 00000000000..ba948f12b33
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
@@ -0,0 +1,38 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import DeletePipelineScheduleModal from '~/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue';
+
+describe('Delete pipeline schedule modal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DeletePipelineScheduleModal, {
+ propsData: {
+ visible: true,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('emits the deleteSchedule event', async () => {
+ findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted()).toEqual({ deleteSchedule: [[]] });
+ });
+
+ it('emits the hideModal event', async () => {
+ findModal().vm.$emit('hide');
+
+ expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index 4b5a9611251..e5d9b378a42 100644
--- a/spec/frontend/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlForm } from '@gitlab/ui';
-import PipelineSchedulesForm from '~/pipeline_schedules/components/pipeline_schedules_form.vue';
+import PipelineSchedulesForm from '~/ci/pipeline_schedules/components/pipeline_schedules_form.vue';
describe('Pipeline schedules form', () => {
let wrapper;
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
new file mode 100644
index 00000000000..4aa4cdf89a1
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -0,0 +1,280 @@
+import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { trimText } from 'helpers/text_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import PipelineSchedules from '~/ci/pipeline_schedules/components/pipeline_schedules.vue';
+import DeletePipelineScheduleModal from '~/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue';
+import TakeOwnershipModal from '~/ci/pipeline_schedules/components/take_ownership_modal.vue';
+import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
+import deletePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql';
+import takeOwnershipMutation from '~/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql';
+import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
+import {
+ mockGetPipelineSchedulesGraphQLResponse,
+ mockPipelineScheduleNodes,
+ deleteMutationResponse,
+ takeOwnershipMutationResponse,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+const $toast = {
+ show: jest.fn(),
+};
+
+describe('Pipeline schedules app', () => {
+ let wrapper;
+
+ const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
+ const deleteMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const takeOwnershipMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(takeOwnershipMutationResponse);
+ const takeOwnershipMutationHandlerFailed = jest
+ .fn()
+ .mockRejectedValue(new Error('GraphQL error'));
+
+ const createMockApolloProvider = (
+ requestHandlers = [[getPipelineSchedulesQuery, successHandler]],
+ ) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers) => {
+ wrapper = mountExtended(PipelineSchedules, {
+ provide: {
+ fullPath: 'gitlab-org/gitlab',
+ },
+ mocks: {
+ $toast,
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ });
+ };
+
+ const findTable = () => wrapper.findComponent(PipelineSchedulesTable);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findDeleteModal = () => wrapper.findComponent(DeletePipelineScheduleModal);
+ const findTakeOwnershipModal = () => wrapper.findComponent(TakeOwnershipModal);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findNewButton = () => wrapper.findByTestId('new-schedule-button');
+ const findAllTab = () => wrapper.findByTestId('pipeline-schedules-all-tab');
+ const findActiveTab = () => wrapper.findByTestId('pipeline-schedules-active-tab');
+ const findInactiveTab = () => wrapper.findByTestId('pipeline-schedules-inactive-tab');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays table, tabs and new button', async () => {
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ expect(findNewButton().exists()).toBe(true);
+ expect(findTabs().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('handles loading state', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('fetching pipeline schedules', () => {
+ it('fetches query and passes an array of pipeline schedules', async () => {
+ createComponent();
+
+ expect(successHandler).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
+ });
+
+ it('shows query error alert', async () => {
+ createComponent([[getPipelineSchedulesQuery, failedHandler]]);
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem fetching pipeline schedules.');
+ });
+ });
+
+ describe('deleting a pipeline schedule', () => {
+ it('shows delete mutation error alert', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findDeleteModal().vm.$emit('deleteSchedule');
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem deleting the pipeline schedule.');
+ });
+
+ it('deletes pipeline schedule and refetches query', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
+ ]);
+
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
+
+ await waitForPromises();
+
+ const scheduleId = mockPipelineScheduleNodes[0].id;
+
+ findTable().vm.$emit('showDeleteModal', scheduleId);
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+
+ findDeleteModal().vm.$emit('deleteSchedule');
+
+ await waitForPromises();
+
+ expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: scheduleId,
+ });
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect($toast.show).toHaveBeenCalledWith('Pipeline schedule successfully deleted.');
+ });
+
+ it('handles delete modal visibility correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDeleteModal().props('visible')).toBe(false);
+
+ findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findDeleteModal().props('visible')).toBe(true);
+ expect(findTakeOwnershipModal().props('visible')).toBe(false);
+
+ findDeleteModal().vm.$emit('hideModal');
+
+ await nextTick();
+
+ expect(findDeleteModal().props('visible')).toBe(false);
+ });
+ });
+
+ describe('taking ownership of a pipeline schedule', () => {
+ it('shows take ownership mutation error alert', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [takeOwnershipMutation, takeOwnershipMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findTakeOwnershipModal().vm.$emit('takeOwnership');
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(
+ 'There was a problem taking ownership of the pipeline schedule.',
+ );
+ });
+
+ it('takes ownership of pipeline schedule and refetches query', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [takeOwnershipMutation, takeOwnershipMutationHandlerSuccess],
+ ]);
+
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
+
+ await waitForPromises();
+
+ const scheduleId = mockPipelineScheduleNodes[1].id;
+
+ findTable().vm.$emit('showTakeOwnershipModal', scheduleId);
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+
+ findTakeOwnershipModal().vm.$emit('takeOwnership');
+
+ await waitForPromises();
+
+ expect(takeOwnershipMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: scheduleId,
+ });
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect($toast.show).toHaveBeenCalledWith('Successfully taken ownership from Admin.');
+ });
+
+ it('handles take ownership modal visibility correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTakeOwnershipModal().props('visible')).toBe(false);
+
+ findTable().vm.$emit('showTakeOwnershipModal', mockPipelineScheduleNodes[0].id);
+
+ await nextTick();
+
+ expect(findTakeOwnershipModal().props('visible')).toBe(true);
+ expect(findDeleteModal().props('visible')).toBe(false);
+
+ findTakeOwnershipModal().vm.$emit('hideModal');
+
+ await nextTick();
+
+ expect(findTakeOwnershipModal().props('visible')).toBe(false);
+ });
+ });
+
+ describe('pipeline schedule tabs', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('displays All tab with count', () => {
+ expect(trimText(findAllTab().text())).toBe(`All ${mockPipelineScheduleNodes.length}`);
+ });
+
+ it('displays Active tab with no count', () => {
+ expect(findActiveTab().text()).toBe('Active');
+ });
+
+ it('displays Inactive tab with no count', () => {
+ expect(findInactiveTab().text()).toBe('Inactive');
+ });
+
+ it('should refetch the schedules query on a tab click', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(0);
+
+ await findAllTab().trigger('click');
+
+ expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index ecc1bdeb679..3364c61d155 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -1,7 +1,11 @@
import { GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineScheduleActions from '~/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
-import { mockPipelineScheduleNodes, mockPipelineScheduleAsGuestNodes } from '../../../mock_data';
+import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
+import {
+ mockPipelineScheduleNodes,
+ mockPipelineScheduleAsGuestNodes,
+ mockTakeOwnershipNodes,
+} from '../../../mock_data';
describe('Pipeline schedule actions', () => {
let wrapper;
@@ -20,6 +24,7 @@ describe('Pipeline schedule actions', () => {
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn');
+ const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
afterEach(() => {
wrapper.destroy();
@@ -46,4 +51,14 @@ describe('Pipeline schedule actions', () => {
showDeleteModal: [[mockPipelineScheduleNodes[0].id]],
});
});
+
+ it('take ownership button emits showTakeOwnershipModal event and schedule id', () => {
+ createComponent({ schedule: mockTakeOwnershipNodes[0] });
+
+ findTakeOwnershipBtn().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({
+ showTakeOwnershipModal: [[mockTakeOwnershipNodes[0].id]],
+ });
+ });
});
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index 5a47b24232f..17bf465baf3 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -1,6 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
-import PipelineScheduleLastPipeline from '~/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
+import PipelineScheduleLastPipeline from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
describe('Pipeline schedule last pipeline', () => {
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
index b1bdc1e91a0..1c06c411097 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineScheduleNextRun from '~/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue';
+import PipelineScheduleNextRun from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
index 3ab04958f5e..6c1991cb4ac 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
@@ -1,6 +1,6 @@
import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineScheduleOwner from '~/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue';
+import PipelineScheduleOwner from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
describe('Pipeline schedule owner', () => {
diff --git a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
index 6817e58790b..f531f04a736 100644
--- a/spec/frontend/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
@@ -1,6 +1,6 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineScheduleTarget from '~/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
+import PipelineScheduleTarget from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
describe('Pipeline schedule target', () => {
diff --git a/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
index 914897946ee..316b3bcf926 100644
--- a/spec/frontend/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
@@ -1,6 +1,6 @@
import { GlTableLite } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue';
+import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
import { mockPipelineScheduleNodes } from '../../mock_data';
describe('Pipeline schedules table', () => {
diff --git a/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
index d787611fe8f..7e6d4ec4bf8 100644
--- a/spec/frontend/pipeline_schedules/components/take_ownership_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
@@ -1,13 +1,13 @@
import { GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue';
+import TakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
describe('Take ownership modal', () => {
let wrapper;
const url = `/root/job-log-tester/-/pipeline_schedules/3/take_ownership`;
const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(TakeOwnershipModal, {
+ wrapper = shallowMountExtended(TakeOwnershipModalLegacy, {
propsData: {
ownershipUrl: url,
...props,
@@ -21,10 +21,6 @@ describe('Take ownership modal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a primary action set to a url and a post data-method', () => {
const actionPrimary = findModal().props('actionPrimary');
@@ -45,10 +41,4 @@ describe('Take ownership modal', () => {
'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
);
});
-
- it('emits the cancel event when clicking on cancel', async () => {
- findModal().vm.$emit('cancel');
-
- expect(findModal().emitted('cancel')).toHaveLength(1);
- });
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
new file mode 100644
index 00000000000..e3965d13c19
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import TakeOwnershipModal from '~/ci/pipeline_schedules/components/take_ownership_modal.vue';
+
+describe('Take ownership modal', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(TakeOwnershipModal, {
+ propsData: {
+ visible: true,
+ ...props,
+ },
+ });
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a take ownership message', () => {
+ expect(findModal().text()).toBe(
+ 'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
+ );
+ });
+
+ it('emits the takeOwnership event', async () => {
+ findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted()).toEqual({ takeOwnership: [[]] });
+ });
+
+ it('emits the hideModal event', async () => {
+ findModal().vm.$emit('hide');
+
+ expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
+ });
+});
diff --git a/spec/frontend/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 0a60998d8fb..3010f1d06c3 100644
--- a/spec/frontend/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -1,6 +1,7 @@
// Fixture located at spec/frontend/fixtures/pipeline_schedules.rb
import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
+import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json';
const {
data: {
@@ -18,10 +19,20 @@ const {
},
} = mockGetPipelineSchedulesAsGuestGraphQLResponse;
+const {
+ data: {
+ project: {
+ pipelineSchedules: { nodes: takeOwnershipNodes },
+ },
+ },
+} = mockGetPipelineSchedulesTakeOwnershipGraphQLResponse;
+
export const mockPipelineScheduleNodes = nodes;
export const mockPipelineScheduleAsGuestNodes = guestNodes;
+export const mockTakeOwnershipNodes = takeOwnershipNodes;
+
export const deleteMutationResponse = {
data: {
pipelineScheduleDelete: {
@@ -32,4 +43,20 @@ export const deleteMutationResponse = {
},
};
+export const takeOwnershipMutationResponse = {
+ data: {
+ pipelineScheduleTakeOwnership: {
+ pipelineSchedule: {
+ id: '1',
+ owner: {
+ id: '2',
+ name: 'Admin',
+ },
+ },
+ errors: [],
+ __typename: 'PipelineScheduleTakeOwnershipPayload',
+ },
+ },
+};
+
export { mockGetPipelineSchedulesGraphQLResponse };
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index 64f66d8f3ba..7081bc57467 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -8,23 +8,23 @@ import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerDetails from '~/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
-import RunnersJobs from '~/runner/components/runner_jobs.vue';
-
-import runnerQuery from '~/runner/graphql/show/runner.query.graphql';
-import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
+
+import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
+import AdminRunnerShowApp from '~/ci/runner/admin_runner_show/admin_runner_show_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerData } from '../mock_data';
-jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerData.data.runner;
diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 7afde3bdc96..9778a6fe66c 100644
--- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -14,18 +14,17 @@ import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
-import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
-import { createLocalState } from '~/runner/graphql/list/local_state';
-import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
-import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
-import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
-import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
-import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import { upgradeStatusTokenConfig } from 'ee_else_ce/ci/runner/components/search_tokens/upgrade_status_token_config';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+import AdminRunnersApp from '~/ci/runner/admin_runners/admin_runners_app.vue';
+import RunnerTypeTabs from '~/ci/runner/components/runner_type_tabs.vue';
+import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/ci/runner/components/runner_list.vue';
+import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
+import RunnerStats from '~/ci/runner/components/stat/runner_stats.vue';
+import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue';
+import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
@@ -45,10 +44,10 @@ import {
STATUS_ONLINE,
DEFAULT_MEMBERSHIP,
RUNNER_PAGE_SIZE,
-} from '~/runner/constants';
-import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql';
-import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
-import { captureException } from '~/runner/sentry_utils';
+} from '~/ci/runner/constants';
+import allRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
import {
allRunnersData,
@@ -69,7 +68,7 @@ const mockRunnersHandler = jest.fn();
const mockRunnersCountHandler = jest.fn();
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
@@ -84,7 +83,6 @@ describe('AdminRunnersApp', () => {
let wrapper;
let showToast;
- const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
@@ -142,11 +140,6 @@ describe('AdminRunnersApp', () => {
wrapper.destroy();
});
- it('shows the feedback banner', () => {
- createComponent();
- expect(findRunnerStackedLayoutBanner().exists()).toBe(true);
- });
-
it('shows the runner setup instructions', () => {
createComponent();
diff --git a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap b/spec/frontend/ci/runner/components/__snapshots__/runner_status_popover_spec.js.snap
index b27a1adf01b..b27a1adf01b 100644
--- a/spec/frontend/runner/components/__snapshots__/runner_status_popover_spec.js.snap
+++ b/spec/frontend/ci/runner/components/__snapshots__/runner_status_popover_spec.js.snap
diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/ci/runner/components/cells/link_cell_spec.js
index 46ab1adb6b6..61bb4432c8e 100644
--- a/spec/frontend/runner/components/cells/link_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/link_cell_spec.js
@@ -1,6 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import LinkCell from '~/runner/components/cells/link_cell.vue';
+import LinkCell from '~/ci/runner/components/cells/link_cell.vue';
describe('LinkCell', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
index 58974d4f85f..82e262d1b73 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
@@ -1,9 +1,9 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
+import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import { allRunnersData } from '../../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
diff --git a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
index e9965d8855d..3097e43e583 100644
--- a/spec/frontend/runner/components/cells/runner_owner_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
@@ -3,9 +3,9 @@ import { GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import RunnerOwnerCell from '~/runner/components/cells/runner_owner_cell.vue';
+import RunnerOwnerCell from '~/ci/runner/components/cells/runner_owner_cell.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
describe('RunnerOwnerCell', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js
index e7cadefc140..4aa354f9b62 100644
--- a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_stacked_summary_cell_spec.js
@@ -1,12 +1,12 @@
import { __ } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerStackedSummaryCell from '~/runner/components/cells/runner_stacked_summary_cell.vue';
+import RunnerStackedSummaryCell from '~/ci/runner/components/cells/runner_stacked_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import RunnerTags from '~/runner/components/runner_tags.vue';
-import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue';
+import RunnerTags from '~/ci/runner/components/runner_tags.vue';
+import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import { allRunnersData } from '../../mock_data';
diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 1d4e3762c91..2fb824a8fa5 100644
--- a/spec/frontend/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
-import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue';
+import RunnerStatusCell from '~/ci/runner/components/cells/runner_status_cell.vue';
-import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
-import RunnerPausedBadge from '~/runner/components/runner_paused_badge.vue';
+import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
+import RunnerPausedBadge from '~/ci/runner/components/runner_paused_badge.vue';
import {
I18N_PAUSED,
I18N_STATUS_ONLINE,
@@ -10,7 +10,7 @@ import {
INSTANCE_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerStatusCell', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
index b49addf112f..f536e0dcbcf 100644
--- a/spec/frontend/runner/components/cells/runner_summary_field_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
@@ -1,6 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue';
+import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerSummaryField', () => {
diff --git a/spec/frontend/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index d3f38bc1d26..cb46c668930 100644
--- a/spec/frontend/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -7,11 +7,11 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
-import RegistrationToken from '~/runner/components/registration/registration_token.vue';
-import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
+import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
+import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
+import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
diff --git a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 2510aaf0334..783a4d9252a 100644
--- a/spec/frontend/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -6,14 +6,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import RegistrationTokenResetDropdownItem from '~/runner/components/registration/registration_token_reset_dropdown_item.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants';
-import runnersRegistrationTokenResetMutation from '~/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
-import { captureException } from '~/runner/sentry_utils';
+import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
Vue.use(GlToast);
diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index 19344a68f79..d2a51c0d910 100644
--- a/spec/frontend/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RegistrationToken from '~/runner/components/registration/registration_token.vue';
+import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
const mockToken = '01234567890';
diff --git a/spec/frontend/runner/components/runner_assigned_item_spec.js b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
index cc09046c000..5df2e04c340 100644
--- a/spec/frontend/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
@@ -1,7 +1,7 @@
import { GlAvatar, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+import RunnerAssignedItem from '~/ci/runner/components/runner_assigned_item.vue';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
const mockHref = '/group/project';
diff --git a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_checkbox_spec.js
index 424a4e61ccd..dad36b0179f 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_checkbox_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_checkbox_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import { GlFormCheckbox } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
+import RunnerBulkDeleteCheckbox from '~/ci/runner/components/runner_bulk_delete_checkbox.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { createLocalState } from '~/runner/graphql/list/local_state';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
Vue.use(VueApollo);
diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 6df918c684f..64f5a0e3b57 100644
--- a/spec/frontend/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -5,10 +5,10 @@ import { createAlert } from '~/flash';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
-import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
+import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
-import BulkRunnerDeleteMutation from '~/runner/graphql/list/bulk_runner_delete.mutation.graphql';
-import { createLocalState } from '~/runner/graphql/list/local_state';
+import BulkRunnerDeleteMutation from '~/ci/runner/graphql/list/bulk_runner_delete.mutation.graphql';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
import waitForPromises from 'helpers/wait_for_promises';
import { allRunnersData } from '../mock_data';
@@ -72,7 +72,6 @@ describe('RunnerBulkDelete', () => {
afterEach(() => {
bulkRunnerDeleteHandler.mockReset();
- wrapper.destroy();
});
describe('When no runners are checked', () => {
@@ -126,50 +125,61 @@ describe('RunnerBulkDelete', () => {
let evt;
let mockHideModal;
+ const confirmDeletion = () => {
+ evt = {
+ preventDefault: jest.fn(),
+ };
+ findModal().vm.$emit('primary', evt);
+ };
+
beforeEach(() => {
mockCheckedRunnerIds = [mockId1, mockId2];
createComponent();
jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
- mockHideModal = jest.spyOn(findModal().vm, 'hide');
+ mockHideModal = jest.spyOn(findModal().vm, 'hide').mockImplementation(() => {});
});
- describe('when deletion is successful', () => {
+ describe('when deletion is confirmed', () => {
beforeEach(() => {
- bulkRunnerDeleteHandler.mockResolvedValue({
- data: {
- bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
- },
- });
-
- evt = {
- preventDefault: jest.fn(),
- };
- findModal().vm.$emit('primary', evt);
+ confirmDeletion();
});
- it('has loading state', async () => {
+ it('has loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
expect(findModal().props('actionCancel').attributes.loading).toBe(true);
-
- await waitForPromises();
-
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
- expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
it('modal is not prevented from closing', () => {
expect(evt.preventDefault).toHaveBeenCalledTimes(1);
});
- it('mutation is called', async () => {
+ it('mutation is called', () => {
expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
input: { ids: mockCheckedRunnerIds },
});
});
+ });
- it('user interface is updated', async () => {
+ describe('when deletion is successful', () => {
+ beforeEach(async () => {
+ bulkRunnerDeleteHandler.mockResolvedValue({
+ data: {
+ bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
+ },
+ });
+
+ confirmDeletion();
+ await waitForPromises();
+ });
+
+ it('removes loading state', () => {
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(false);
+ });
+
+ it('user interface is updated', () => {
const { evict, gc } = apolloCache;
expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length);
@@ -183,44 +193,80 @@ describe('RunnerBulkDelete', () => {
expect(gc).toHaveBeenCalledTimes(1);
});
+ it('emits deletion confirmation', () => {
+ expect(wrapper.emitted('deleted')).toEqual([
+ [{ message: expect.stringContaining(`${mockCheckedRunnerIds.length}`) }],
+ ]);
+ });
+
it('modal is hidden', () => {
expect(mockHideModal).toHaveBeenCalledTimes(1);
});
});
- describe('when deletion fails', () => {
- beforeEach(() => {
- bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!'));
-
- evt = {
- preventDefault: jest.fn(),
- };
- findModal().vm.$emit('primary', evt);
- });
-
- it('has loading state', async () => {
- expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
- expect(findModal().props('actionCancel').attributes.loading).toBe(true);
+ describe('when deletion fails partially', () => {
+ beforeEach(async () => {
+ bulkRunnerDeleteHandler.mockResolvedValue({
+ data: {
+ bulkRunnerDelete: {
+ deletedIds: [mockId1], // only one runner could be deleted
+ errors: ['Can only delete up to 1 runners per call. Ignored 1 runner(s).'],
+ },
+ },
+ });
+ confirmDeletion();
await waitForPromises();
+ });
+ it('removes loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
- it('modal is not prevented from closing', () => {
- expect(evt.preventDefault).toHaveBeenCalledTimes(1);
+ it('user interface is partially updated', () => {
+ const { evict, gc } = apolloCache;
+
+ expect(evict).toHaveBeenCalledTimes(1);
+ expect(evict).toHaveBeenCalledWith({
+ id: expect.stringContaining(mockId1),
+ });
+
+ expect(gc).toHaveBeenCalledTimes(1);
});
- it('mutation is called', () => {
- expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
- input: { ids: mockCheckedRunnerIds },
+ it('emits deletion confirmation', () => {
+ expect(wrapper.emitted('deleted')).toEqual([[{ message: expect.stringContaining('1') }]]);
+ });
+
+ it('alert is called', () => {
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.any(String),
+ captureError: true,
+ error: expect.any(Error),
});
});
- it('user interface is not updated', async () => {
+ it('modal is hidden', () => {
+ expect(mockHideModal).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when deletion fails', () => {
+ beforeEach(async () => {
+ bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!'));
+
+ confirmDeletion();
await waitForPromises();
+ });
+ it('resolves loading state', () => {
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ expect(findModal().props('actionCancel').attributes.loading).toBe(false);
+ });
+
+ it('user interface is not updated', () => {
const { evict, gc } = apolloCache;
expect(evict).not.toHaveBeenCalled();
@@ -228,9 +274,11 @@ describe('RunnerBulkDelete', () => {
expect(mockState.localMutations.clearChecked).not.toHaveBeenCalled();
});
- it('alert is called', async () => {
- await waitForPromises();
+ it('does not emit deletion confirmation', () => {
+ expect(wrapper.emitted('deleted')).toBeUndefined();
+ });
+ it('alert is called', () => {
expect(createAlert).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalledWith({
message: expect.any(String),
@@ -238,6 +286,10 @@ describe('RunnerBulkDelete', () => {
error: expect.any(Error),
});
});
+
+ it('modal is hidden', () => {
+ expect(mockHideModal).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/spec/frontend/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
index c8fb7a69379..02960ad427e 100644
--- a/spec/frontend/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -4,24 +4,25 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerDeleteMutation from '~/runner/graphql/shared/runner_delete.mutation.graphql';
+import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
-import { I18N_DELETE_RUNNER } from '~/runner/constants';
+import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
-import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
import { allRunnersData } from '../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
+const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
Vue.use(VueApollo);
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
let wrapper;
@@ -96,7 +97,7 @@ describe('RunnerDeleteButton', () => {
});
it('Displays a modal with the runner name', () => {
- expect(findModal().props('runnerName')).toBe(`#${mockRunnerId} (${mockRunner.shortSha})`);
+ expect(findModal().props('runnerName')).toBe(mockRunnerName);
});
it('Does not have tabindex when button is enabled', () => {
@@ -189,6 +190,10 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: mockErrorMsg,
+ });
});
});
@@ -217,6 +222,10 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: `${mockErrorMsg} ${mockErrorMsg2}`,
+ });
});
it('does not evict runner from apollo cache', () => {
diff --git a/spec/frontend/runner/components/runner_delete_modal_spec.js b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
index 3e5b634d815..f2fb0206763 100644
--- a/spec/frontend/runner/components/runner_delete_modal_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
@@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
+import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
describe('RunnerDeleteModal', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index e6cc936e260..65a81973869 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -3,13 +3,13 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { useFakeDate } from 'helpers/fake_date';
import { findDd } from 'helpers/dl_locator_helper';
-import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
+import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/ci/runner/constants';
-import RunnerDetails from '~/runner/components/runner_details.vue';
-import RunnerDetail from '~/runner/components/runner_detail.vue';
-import RunnerGroups from '~/runner/components/runner_groups.vue';
-import RunnerTags from '~/runner/components/runner_tags.vue';
-import RunnerTag from '~/runner/components/runner_tag.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerDetail from '~/ci/runner/components/runner_detail.vue';
+import RunnerGroups from '~/ci/runner/components/runner_groups.vue';
+import RunnerTags from '~/ci/runner/components/runner_tags.vue';
+import RunnerTag from '~/ci/runner/components/runner_tag.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_edit_button_spec.js b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
index 428c1ef07e9..907cdc90100 100644
--- a/spec/frontend/runner/components/runner_edit_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('RunnerEditButton', () => {
diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index c92e19f9263..496c144083e 100644
--- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,9 +1,9 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config';
-import TagToken from '~/runner/components/search_tokens/tag_token.vue';
-import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config';
+import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
+import { statusTokenConfig } from '~/ci/runner/components/search_tokens/status_token_config';
+import TagToken from '~/ci/runner/components/search_tokens/tag_token.vue';
+import { tagTokenConfig } from '~/ci/runner/components/search_tokens/tag_token_config';
import {
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -12,7 +12,7 @@ import {
DEFAULT_MEMBERSHIP,
DEFAULT_SORT,
CONTACTED_DESC,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
diff --git a/spec/frontend/runner/components/runner_groups_spec.js b/spec/frontend/ci/runner/components/runner_groups_spec.js
index b83733b9972..0991feb2e55 100644
--- a/spec/frontend/runner/components/runner_groups_spec.js
+++ b/spec/frontend/ci/runner/components/runner_groups_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerGroups from '~/runner/components/runner_groups.vue';
-import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
+import RunnerGroups from '~/ci/runner/components/runner_groups.vue';
+import RunnerAssignedItem from '~/ci/runner/components/runner_assigned_item.vue';
import { runnerData, runnerWithGroupData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index 701d39108cb..a04011de1cd 100644
--- a/spec/frontend/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -1,13 +1,18 @@
import { GlSprintf } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { I18N_STATUS_ONLINE, I18N_GROUP_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants';
+import {
+ I18N_STATUS_ONLINE,
+ I18N_GROUP_TYPE,
+ GROUP_TYPE,
+ STATUS_ONLINE,
+} from '~/ci/runner/constants';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
-import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
+import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
import { runnerData } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/ci/runner/components/runner_jobs_spec.js
index 4d38afb25ee..bdb8a4a31a3 100644
--- a/spec/frontend/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_spec.js
@@ -5,18 +5,18 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import RunnerJobs from '~/runner/components/runner_jobs.vue';
-import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants';
+import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
+import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants';
-import runnerJobsQuery from '~/runner/graphql/show/runner_jobs.query.graphql';
+import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql';
import { runnerData, runnerJobsData } from '../mock_data';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
const mockRunnerWithJobs = runnerJobsData.data.runner;
diff --git a/spec/frontend/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 5f4905ad2a8..8defe568df8 100644
--- a/spec/frontend/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -6,7 +6,7 @@ import {
} from 'helpers/vue_test_utils_helper';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
+import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
import { useFakeDate } from 'helpers/fake_date';
import { runnerJobsData } from '../mock_data';
@@ -61,6 +61,8 @@ describe('RunnerJobsTable', () => {
__('Project'),
__('Commit'),
s__('Job|Finished at'),
+ s__('Job|Duration'),
+ s__('Job|Queued'),
s__('Runners|Tags'),
]);
});
@@ -108,6 +110,22 @@ describe('RunnerJobsTable', () => {
expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago');
});
+ it('Formats duration time', () => {
+ mockJobsCopy[0].duration = 60;
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'duration' }).text()).toBe('00:01:00');
+ });
+
+ it('Formats queued time', () => {
+ mockJobsCopy[0].queuedDuration = 30;
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'queued' }).text()).toBe('00:00:30');
+ });
+
it('Formats tags', () => {
mockJobsCopy[0].tags = ['tag-1', 'tag-2'];
diff --git a/spec/frontend/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index 038162b889e..d351f7b6908 100644
--- a/spec/frontend/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -4,7 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
+import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
const mockSvgPath = 'mock-svg-path.svg';
const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index a31990f8f7e..d53a0ce8f4f 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -8,13 +8,13 @@ import {
import createMockApollo from 'helpers/mock_apollo_helper';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createLocalState } from '~/runner/graphql/list/local_state';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
-import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
-import RunnerBulkDeleteCheckbox from '~/runner/components/runner_bulk_delete_checkbox.vue';
+import RunnerList from '~/ci/runner/components/runner_list.vue';
+import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
+import RunnerBulkDeleteCheckbox from '~/ci/runner/components/runner_bulk_delete_checkbox.vue';
-import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants';
+import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/ci/runner/constants';
import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
const mockRunners = allRunnersData.data.runners.nodes;
diff --git a/spec/frontend/runner/components/runner_membership_toggle_spec.js b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
index 1a7ae22618a..f089becd400 100644
--- a/spec/frontend/runner/components/runner_membership_toggle_spec.js
+++ b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
@@ -1,11 +1,11 @@
import { GlToggle } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue';
+import RunnerMembershipToggle from '~/ci/runner/components/runner_membership_toggle.vue';
import {
I18N_SHOW_ONLY_INHERITED,
MEMBERSHIP_DESCENDANTS,
MEMBERSHIP_ALL_AVAILABLE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerMembershipToggle', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_pagination_spec.js b/spec/frontend/ci/runner/components/runner_pagination_spec.js
index 499cc59250d..f835ee4514d 100644
--- a/spec/frontend/runner/components/runner_pagination_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pagination_spec.js
@@ -1,6 +1,6 @@
import { GlKeysetPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
const mockStartCursor = 'START_CURSOR';
const mockEndCursor = 'END_CURSOR';
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
index 61476007571..12680e01b98 100644
--- a/spec/frontend/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -4,18 +4,18 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerToggleActiveMutation from '~/runner/graphql/shared/runner_toggle_active.mutation.graphql';
+import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import { createAlert } from '~/flash';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
I18N_RESUME,
I18N_RESUME_TOOLTIP,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import { allRunnersData } from '../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
@@ -23,7 +23,7 @@ const mockRunner = allRunnersData.data.runners.nodes[0];
Vue.use(VueApollo);
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
describe('RunnerPauseButton', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_paused_badge_spec.js b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
index c1c7351aab2..b051ebe99a7 100644
--- a/spec/frontend/runner/components/runner_paused_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
@@ -1,8 +1,8 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue';
+import RunnerStatePausedBadge from '~/ci/runner/components/runner_paused_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { I18N_PAUSED } from '~/runner/constants';
+import { I18N_PAUSED } from '~/ci/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/ci/runner/components/runner_projects_spec.js
index eca042cae86..17517c4db66 100644
--- a/spec/frontend/runner/components/runner_projects_spec.js
+++ b/spec/frontend/ci/runner/components/runner_projects_spec.js
@@ -12,18 +12,18 @@ import {
I18N_FILTER_PROJECTS,
I18N_NO_PROJECTS_FOUND,
RUNNER_DETAILS_PROJECTS_PAGE_SIZE,
-} from '~/runner/constants';
-import RunnerProjects from '~/runner/components/runner_projects.vue';
-import RunnerAssignedItem from '~/runner/components/runner_assigned_item.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
-import { captureException } from '~/runner/sentry_utils';
+} from '~/ci/runner/constants';
+import RunnerProjects from '~/ci/runner/components/runner_projects.vue';
+import RunnerAssignedItem from '~/ci/runner/components/runner_assigned_item.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
-import runnerProjectsQuery from '~/runner/graphql/show/runner_projects.query.graphql';
+import runnerProjectsQuery from '~/ci/runner/graphql/show/runner_projects.query.graphql';
import { runnerData, runnerProjectsData } from '../mock_data';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
const mockRunnerWithProjects = runnerProjectsData.data.runner;
diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
index 9ab6378304f..7d3064c2aef 100644
--- a/spec/frontend/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue';
+import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
I18N_STATUS_ONLINE,
@@ -13,7 +13,7 @@ import {
STATUS_OFFLINE,
STATUS_STALE,
STATUS_NEVER_CONTACTED,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_status_popover_spec.js b/spec/frontend/ci/runner/components/runner_status_popover_spec.js
index 789283d1245..89fb95f2da4 100644
--- a/spec/frontend/runner/components/runner_status_popover_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_popover_spec.js
@@ -1,6 +1,6 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue';
+import RunnerStatusPopover from '~/ci/runner/components/runner_status_popover.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data';
diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/ci/runner/components/runner_tag_spec.js
index 391c17f81cb..7bcb046ae43 100644
--- a/spec/frontend/runner/components/runner_tag_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tag_spec.js
@@ -2,8 +2,8 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { RUNNER_TAG_BADGE_VARIANT } from '~/runner/constants';
-import RunnerTag from '~/runner/components/runner_tag.vue';
+import { RUNNER_TAG_BADGE_VARIANT } from '~/ci/runner/constants';
+import RunnerTag from '~/ci/runner/components/runner_tag.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
const mockTag = 'tag1';
diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/ci/runner/components/runner_tags_spec.js
index c6bfabdb18a..96bec00302b 100644
--- a/spec/frontend/runner/components/runner_tags_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tags_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import RunnerTags from '~/runner/components/runner_tags.vue';
+import RunnerTags from '~/ci/runner/components/runner_tags.vue';
describe('RunnerTags', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
index fe922fb9d18..58f09362759 100644
--- a/spec/frontend/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
@@ -1,6 +1,6 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue';
+import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
INSTANCE_TYPE,
@@ -9,7 +9,7 @@ import {
I18N_INSTANCE_TYPE,
I18N_GROUP_TYPE,
I18N_PROJECT_TYPE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerTypeBadge', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
index dde35533bc3..3347c190083 100644
--- a/spec/frontend/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
@@ -1,14 +1,14 @@
import { GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import RunnerCount from '~/runner/components/stat/runner_count.vue';
+import RunnerTypeTabs from '~/ci/runner/components/runner_type_tabs.vue';
+import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
DEFAULT_MEMBERSHIP,
DEFAULT_SORT,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
const mockSearch = {
runnerType: null,
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index e12736216a0..a0e51ebf958 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -7,22 +7,22 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
-import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
+import RunnerUpdateForm from '~/ci/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/edit/runner_update.mutation.graphql';
-import { captureException } from '~/runner/sentry_utils';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+} from '~/ci/runner/constants';
+import runnerUpdateMutation from '~/ci/runner/graphql/edit/runner_update.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerFormData } from '../mock_data';
-jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerFormData.data.runner;
diff --git a/spec/frontend/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index a7363eb11cd..d3c7ea50f9d 100644
--- a/spec/frontend/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -6,7 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import TagToken, { TAG_SUGGESTIONS_PATH } from '~/runner/components/search_tokens/tag_token.vue';
+import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
diff --git a/spec/frontend/runner/components/stat/runner_count_spec.js b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
index 2a6a745099f..42d8c9a1080 100644
--- a/spec/frontend/runner/components/stat/runner_count_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
@@ -1,18 +1,18 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
-import RunnerCount from '~/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
-import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
-import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
import { runnersCountData, groupRunnersCountData } from '../../mock_data';
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
diff --git a/spec/frontend/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
index 964a6a6ff71..cad61f26012 100644
--- a/spec/frontend/runner/components/stat/runner_single_stat_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
@@ -1,8 +1,8 @@
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
-import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
-import RunnerCount from '~/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/runner/constants';
+import RunnerSingleStat from '~/ci/runner/components/stat/runner_single_stat.vue';
+import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
+import { INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
describe('RunnerStats', () => {
let wrapper;
diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
index 4afbe453903..daebf3df050 100644
--- a/spec/frontend/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
@@ -1,6 +1,6 @@
import { shallowMount, mount } from '@vue/test-utils';
-import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
+import RunnerStats from '~/ci/runner/components/stat/runner_stats.vue';
+import RunnerSingleStat from '~/ci/runner/components/stat/runner_single_stat.vue';
import {
I18N_STATUS_ONLINE,
I18N_STATUS_OFFLINE,
@@ -9,7 +9,7 @@ import {
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_STALE,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
describe('RunnerStats', () => {
let wrapper;
diff --git a/spec/frontend/runner/graphql/local_state_spec.js b/spec/frontend/ci/runner/graphql/local_state_spec.js
index 915170b53f9..ce07a6a618d 100644
--- a/spec/frontend/runner/graphql/local_state_spec.js
+++ b/spec/frontend/ci/runner/graphql/local_state_spec.js
@@ -1,8 +1,8 @@
import { gql } from '@apollo/client/core';
import createApolloClient from '~/lib/graphql';
-import { createLocalState } from '~/runner/graphql/list/local_state';
-import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
-import { RUNNER_TYPENAME } from '~/runner/constants';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+import getCheckedRunnerIdsQuery from '~/ci/runner/graphql/list/checked_runner_ids.query.graphql';
+import { RUNNER_TYPENAME } from '~/ci/runner/constants';
const makeRunner = (id, deleteRunner = true) => ({
id,
@@ -11,7 +11,7 @@ const makeRunner = (id, deleteRunner = true) => ({
},
});
-describe('~/runner/graphql/list/local_state', () => {
+describe('~/ci/runner/graphql/list/local_state', () => {
let localState;
let apolloClient;
diff --git a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index a3b67674c94..c6c3f3b7040 100644
--- a/spec/frontend/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -7,21 +7,21 @@ import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerDetails from '~/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
-import runnerQuery from '~/runner/graphql/show/runner.query.graphql';
-import GroupRunnerShowApp from '~/runner/group_runner_show/group_runner_show_app.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
+import GroupRunnerShowApp from '~/ci/runner/group_runner_show/group_runner_show_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import { runnerData } from '../mock_data';
-jest.mock('~/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
const mockRunner = runnerData.data.runner;
diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 7482926e151..c3493b3c9fd 100644
--- a/spec/frontend/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -13,19 +13,18 @@ import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
-import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
-import { createLocalState } from '~/runner/graphql/list/local_state';
-
-import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
-import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
-import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
-import RunnerList from '~/runner/components/runner_list.vue';
-import RunnerListEmptyState from '~/runner/components/runner_list_empty_state.vue';
-import RunnerStats from '~/runner/components/stat/runner_stats.vue';
-import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue';
-import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue';
-import RunnerPagination from '~/runner/components/runner_pagination.vue';
-import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue';
+import { upgradeStatusTokenConfig } from 'ee_else_ce/ci/runner/components/search_tokens/upgrade_status_token_config';
+import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+
+import RunnerTypeTabs from '~/ci/runner/components/runner_type_tabs.vue';
+import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
+import RunnerList from '~/ci/runner/components/runner_list.vue';
+import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
+import RunnerStats from '~/ci/runner/components/stat/runner_stats.vue';
+import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.vue';
+import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
+import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import RunnerMembershipToggle from '~/ci/runner/components/runner_membership_toggle.vue';
import {
CREATED_ASC,
@@ -46,11 +45,11 @@ import {
MEMBERSHIP_DESCENDANTS,
RUNNER_PAGE_SIZE,
I18N_EDIT,
-} from '~/runner/constants';
-import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
-import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
-import GroupRunnersApp from '~/runner/group_runners/group_runners_app.vue';
-import { captureException } from '~/runner/sentry_utils';
+} from '~/ci/runner/constants';
+import groupRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
+import GroupRunnersApp from '~/ci/runner/group_runners/group_runners_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
import {
groupRunnersData,
groupRunnersDataPaginated,
@@ -74,7 +73,7 @@ const mockGroupRunnersHandler = jest.fn();
const mockGroupRunnersCountHandler = jest.fn();
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),
@@ -83,7 +82,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GroupRunnersApp', () => {
let wrapper;
- const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner);
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
@@ -142,11 +140,6 @@ describe('GroupRunnersApp', () => {
wrapper.destroy();
});
- it('shows the feedback banner', () => {
- createComponent();
- expect(findRunnerStackedLayoutBanner().exists()).toBe(true);
- });
-
it('shows the runner tabs with a runner count for each type', async () => {
await createComponent({ mountFn: mountExtended });
@@ -391,9 +384,9 @@ describe('GroupRunnersApp', () => {
expect(findRunnerPagination().attributes('disabled')).toBe('true');
});
- it('runners cannot be deleted in bulk', () => {
+ it('runners can be deleted in bulk', () => {
createComponent();
- expect(findRunnerList().props('checkable')).toBe(false);
+ expect(findRunnerList().props('checkable')).toBe(true);
});
describe('when no runners are found', () => {
diff --git a/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/save_alert_to_local_storage_spec.js
index 69cda6d6022..b34ef01eeed 100644
--- a/spec/frontend/runner/local_storage_alert/save_alert_to_local_storage_spec.js
+++ b/spec/frontend/ci/runner/local_storage_alert/save_alert_to_local_storage_spec.js
@@ -1,6 +1,6 @@
import AccessorUtilities from '~/lib/utils/accessor';
-import { saveAlertToLocalStorage } from '~/runner/local_storage_alert/save_alert_to_local_storage';
-import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
const mockAlert = { message: 'Message!' };
diff --git a/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
index cabbe642dac..03908891cfd 100644
--- a/spec/frontend/runner/local_storage_alert/show_alert_from_local_storage_spec.js
+++ b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
@@ -1,6 +1,6 @@
import AccessorUtilities from '~/lib/utils/accessor';
-import { showAlertFromLocalStorage } from '~/runner/local_storage_alert/show_alert_from_local_storage';
-import { LOCAL_STORAGE_ALERT_KEY } from '~/runner/local_storage_alert/constants';
+import { showAlertFromLocalStorage } from '~/ci/runner/local_storage_alert/show_alert_from_local_storage';
+import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { createAlert } from '~/flash';
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index da0c0433b3e..eff5abc21b5 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -1,23 +1,23 @@
// Fixtures generated by: spec/frontend/fixtures/runner.rb
// Show runner queries
-import runnerData from 'test_fixtures/graphql/runner/show/runner.query.graphql.json';
-import runnerWithGroupData from 'test_fixtures/graphql/runner/show/runner.query.graphql.with_group.json';
-import runnerProjectsData from 'test_fixtures/graphql/runner/show/runner_projects.query.graphql.json';
-import runnerJobsData from 'test_fixtures/graphql/runner/show/runner_jobs.query.graphql.json';
+import runnerData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.json';
+import runnerWithGroupData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.with_group.json';
+import runnerProjectsData from 'test_fixtures/graphql/ci/runner/show/runner_projects.query.graphql.json';
+import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.query.graphql.json';
// Edit runner queries
-import runnerFormData from 'test_fixtures/graphql/runner/edit/runner_form.query.graphql.json';
+import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json';
// List queries
-import allRunnersData from 'test_fixtures/graphql/runner/list/all_runners.query.graphql.json';
-import allRunnersDataPaginated from 'test_fixtures/graphql/runner/list/all_runners.query.graphql.paginated.json';
-import runnersCountData from 'test_fixtures/graphql/runner/list/all_runners_count.query.graphql.json';
-import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.json';
-import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.paginated.json';
-import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runners_count.query.graphql.json';
+import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
+import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
+import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
+import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
+import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
+import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
-import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/runner/constants';
+import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/ci/runner/constants';
const emptyPageInfo = {
__typename: 'PageInfo',
diff --git a/spec/frontend/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
index fb118817d51..a9369a5e626 100644
--- a/spec/frontend/runner/runner_edit/runner_edit_app_spec.js
+++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
@@ -6,17 +6,17 @@ import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import RunnerHeader from '~/runner/components/runner_header.vue';
-import RunnerUpdateForm from '~/runner/components/runner_update_form.vue';
-import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql';
-import RunnerEditApp from '~//runner/runner_edit/runner_edit_app.vue';
-import { captureException } from '~/runner/sentry_utils';
-import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/runner/constants';
+import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
+import runnerFormQuery from '~/ci/runner/graphql/edit/runner_form.query.graphql';
+import RunnerEditApp from '~/ci/runner/runner_edit/runner_edit_app.vue';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/ci/runner/constants';
import { runnerFormData } from '../mock_data';
jest.mock('~/flash');
-jest.mock('~/runner/sentry_utils');
+jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerFormData.data.runner;
const mockRunnerGraphqlId = mockRunner.id;
diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/ci/runner/runner_search_utils_spec.js
index e1f90482b34..1db8fa1829b 100644
--- a/spec/frontend/runner/runner_search_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_search_utils_spec.js
@@ -5,7 +5,7 @@ import {
fromSearchToUrl,
fromSearchToVariables,
isSearchFiltered,
-} from 'ee_else_ce/runner/runner_search_utils';
+} from 'ee_else_ce/ci/runner/runner_search_utils';
import { mockSearchExamples } from './mock_data';
describe('search_params.js', () => {
diff --git a/spec/frontend/runner/runner_update_form_utils_spec.js b/spec/frontend/ci/runner/runner_update_form_utils_spec.js
index a633aee92f7..b2f7bbc49a9 100644
--- a/spec/frontend/runner/runner_update_form_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_update_form_utils_spec.js
@@ -1,5 +1,8 @@
-import { ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants';
-import { modelToUpdateMutationVariables, runnerToModel } from '~/runner/runner_update_form_utils';
+import { ACCESS_LEVEL_NOT_PROTECTED } from '~/ci/runner/constants';
+import {
+ modelToUpdateMutationVariables,
+ runnerToModel,
+} from '~/ci/runner/runner_update_form_utils';
const mockId = 'gid://gitlab/Ci::Runner/1';
const mockDescription = 'Runner Desc.';
@@ -20,7 +23,7 @@ const mockModel = {
tagList: 'tag-1, tag-2',
};
-describe('~/runner/runner_update_form_utils', () => {
+describe('~/ci/runner/runner_update_form_utils', () => {
describe('runnerToModel', () => {
it('collects all model data', () => {
expect(runnerToModel(mockRunner)).toEqual(mockModel);
diff --git a/spec/frontend/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index b61eb63961e..f7b689272ce 100644
--- a/spec/frontend/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/browser';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
jest.mock('@sentry/browser');
-describe('~/runner/sentry_utils', () => {
+describe('~/ci/runner/sentry_utils', () => {
let mockSetTag;
beforeEach(async () => {
diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/ci/runner/utils_spec.js
index 33de1345f85..56b758f00e4 100644
--- a/spec/frontend/runner/utils_spec.js
+++ b/spec/frontend/ci/runner/utils_spec.js
@@ -1,6 +1,11 @@
-import { formatJobCount, tableField, getPaginationVariables, parseInterval } from '~/runner/utils';
+import {
+ formatJobCount,
+ tableField,
+ getPaginationVariables,
+ parseInterval,
+} from '~/ci/runner/utils';
-describe('~/runner/utils', () => {
+describe('~/ci/runner/utils', () => {
describe('formatJobCount', () => {
it('formats a number', () => {
expect(formatJobCount(1)).toBe('1');
diff --git a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
index 864041141b8..c7375acd8e5 100644
--- a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
@@ -1,178 +1,35 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/settings';
import ciAdminVariables from '~/ci_variable_list/components/ci_admin_variables.vue';
-import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
-import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
-import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
-import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
-
-import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
-
-import { mockAdminVariables, newVariable } from '../mocks';
-
-jest.mock('~/flash');
-
-Vue.use(VueApollo);
-
-const mockProvide = {
- endpoint: '/variables',
-};
-
-describe('Ci Admin Variable list', () => {
+describe('Ci Project Variable wrapper', () => {
let wrapper;
- let mockApollo;
- let mockVariables;
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiTable = () => wrapper.findComponent(GlTable);
- const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
-
- // eslint-disable-next-line consistent-return
- const createComponentWithApollo = async ({ isLoading = false } = {}) => {
- const handlers = [[getAdminVariables, mockVariables]];
-
- mockApollo = createMockApollo(handlers, resolvers);
+ const findCiShared = () => wrapper.findComponent(ciVariableShared);
- wrapper = shallowMount(ciAdminVariables, {
- provide: mockProvide,
- apolloProvider: mockApollo,
- stubs: { ciVariableSettings, ciVariableTable },
- });
-
- if (!isLoading) {
- return waitForPromises();
- }
+ const createComponent = () => {
+ wrapper = shallowMount(ciAdminVariables);
};
beforeEach(() => {
- mockVariables = jest.fn();
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
});
- describe('while queries are being fetch', () => {
- beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
- });
-
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
+ it('Passes down the correct props to ci_variable_shared', () => {
+ expect(findCiShared().props()).toEqual({
+ areScopedVariablesAvailable: false,
+ componentName: 'InstanceVariables',
+ hideEnvironmentScope: true,
+ mutationData: wrapper.vm.$options.mutationData,
+ queryData: wrapper.vm.$options.queryData,
+ refetchAfterMutation: true,
+ fullPath: null,
+ id: null,
});
});
-
- describe('when queries are resolved', () => {
- describe('successfuly', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockAdminVariables);
-
- await createComponentWithApollo();
- });
-
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([]);
- });
-
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockAdminVariables.data.ciVariables.nodes,
- );
- });
-
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
-
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockVariables.mockRejectedValue();
-
- await createComponentWithApollo();
- });
-
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
- });
- });
- });
-
- describe('mutations', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockAdminVariables);
-
- await createComponentWithApollo();
- });
- it.each`
- actionName | mutation | event
- ${'add'} | ${addAdminVariable} | ${'add-variable'}
- ${'update'} | ${updateAdminVariable} | ${'update-variable'}
- ${'delete'} | ${deleteAdminVariable} | ${'delete-variable'}
- `(
- 'calls the right mutation when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event | mutationName
- ${'add'} | ${'add-variable'} | ${'addAdminVariable'}
- ${'update'} | ${'update-variable'} | ${'updateAdminVariable'}
- ${'delete'} | ${'delete-variable'} | ${'deleteAdminVariable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event, mutationName }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
- });
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
- });
});
diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
index 8a48e73eb9f..ef5a86ccb61 100644
--- a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
@@ -1,183 +1,72 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/settings';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue';
-import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
-import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
-import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
-import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
-
-import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
-
-import { mockGroupVariables, newVariable } from '../mocks';
-
-jest.mock('~/flash');
-
-Vue.use(VueApollo);
+import { GRAPHQL_GROUP_TYPE } from '~/ci_variable_list/constants';
const mockProvide = {
- endpoint: '/variables',
- groupPath: '/namespace/group',
- groupId: 1,
+ glFeatures: {
+ groupScopedCiVariables: false,
+ },
+ groupPath: '/group',
+ groupId: 12,
};
-describe('Ci Group Variable list', () => {
+describe('Ci Group Variable wrapper', () => {
let wrapper;
- let mockApollo;
- let mockVariables;
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiTable = () => wrapper.findComponent(GlTable);
- const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
-
- // eslint-disable-next-line consistent-return
- const createComponentWithApollo = async ({ isLoading = false } = {}) => {
- const handlers = [[getGroupVariables, mockVariables]];
-
- mockApollo = createMockApollo(handlers, resolvers);
+ const findCiShared = () => wrapper.findComponent(ciVariableShared);
+ const createComponent = ({ provide = {} } = {}) => {
wrapper = shallowMount(ciGroupVariables, {
- provide: mockProvide,
- apolloProvider: mockApollo,
- stubs: { ciVariableSettings, ciVariableTable },
+ provide: { ...mockProvide, ...provide },
});
-
- if (!isLoading) {
- return waitForPromises();
- }
};
- beforeEach(() => {
- mockVariables = jest.fn();
- });
-
afterEach(() => {
wrapper.destroy();
});
- describe('while queries are being fetch', () => {
+ describe('Props', () => {
beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
+ createComponent();
});
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
+ it('are passed down the correctly to ci_variable_shared', () => {
+ expect(findCiShared().props()).toEqual({
+ id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId),
+ areScopedVariablesAvailable: false,
+ componentName: 'GroupVariables',
+ fullPath: mockProvide.groupPath,
+ hideEnvironmentScope: false,
+ mutationData: wrapper.vm.$options.mutationData,
+ queryData: wrapper.vm.$options.queryData,
+ refetchAfterMutation: false,
+ });
});
});
- describe('when queries are resolved', () => {
- describe('successfuly', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
-
- await createComponentWithApollo();
- });
-
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([]);
- });
-
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockGroupVariables.data.group.ciVariables.nodes,
- );
+ describe('feature flag', () => {
+ describe('When enabled', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { groupScopedCiVariables: true } } });
});
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
+ it('Passes down `true` to variable shared component', () => {
+ expect(findCiShared().props('areScopedVariablesAvailable')).toBe(true);
});
});
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockVariables.mockRejectedValue();
-
- await createComponentWithApollo();
+ describe('When disabled', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { groupScopedCiVariables: false } } });
});
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('Passes down `false` to variable shared component', () => {
+ expect(findCiShared().props('areScopedVariablesAvailable')).toBe(false);
});
});
});
-
- describe('mutations', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
-
- await createComponentWithApollo();
- });
- it.each`
- actionName | mutation | event
- ${'add'} | ${addGroupVariable} | ${'add-variable'}
- ${'update'} | ${updateGroupVariable} | ${'update-variable'}
- ${'delete'} | ${deleteGroupVariable} | ${'delete-variable'}
- `(
- 'calls the right mutation when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- fullPath: mockProvide.groupPath,
- groupId: convertToGraphQLId('Group', mockProvide.groupId),
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event | mutationName
- ${'add'} | ${'add-variable'} | ${'addGroupVariable'}
- ${'update'} | ${'update-variable'} | ${'updateGroupVariable'}
- ${'delete'} | ${'delete-variable'} | ${'deleteGroupVariable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event, mutationName }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
- });
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
- });
});
diff --git a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
index c630278fbde..97051325f59 100644
--- a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
@@ -1,215 +1,45 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/settings';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciProjectVariables from '~/ci_variable_list/components/ci_project_variables.vue';
-import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
-import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
-import addProjectVariable from '~/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
-import deleteProjectVariable from '~/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
-import updateProjectVariable from '~/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
-
-import {
- environmentFetchErrorText,
- genericMutationErrorText,
- variableFetchErrorText,
-} from '~/ci_variable_list/constants';
-
-import {
- devName,
- mockProjectEnvironments,
- mockProjectVariables,
- newVariable,
- prodName,
-} from '../mocks';
-
-jest.mock('~/flash');
-
-Vue.use(VueApollo);
+import { GRAPHQL_PROJECT_TYPE } from '~/ci_variable_list/constants';
const mockProvide = {
- endpoint: '/variables',
projectFullPath: '/namespace/project',
projectId: 1,
};
-describe('Ci Project Variable list', () => {
+describe('Ci Project Variable wrapper', () => {
let wrapper;
- let mockApollo;
- let mockEnvironments;
- let mockVariables;
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCiTable = () => wrapper.findComponent(GlTable);
- const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
-
- // eslint-disable-next-line consistent-return
- const createComponentWithApollo = async ({ isLoading = false } = {}) => {
- const handlers = [
- [getProjectEnvironments, mockEnvironments],
- [getProjectVariables, mockVariables],
- ];
-
- mockApollo = createMockApollo(handlers, resolvers);
+ const findCiShared = () => wrapper.findComponent(ciVariableShared);
+ const createComponent = () => {
wrapper = shallowMount(ciProjectVariables, {
provide: mockProvide,
- apolloProvider: mockApollo,
- stubs: { ciVariableSettings, ciVariableTable },
});
-
- if (!isLoading) {
- return waitForPromises();
- }
};
beforeEach(() => {
- mockEnvironments = jest.fn();
- mockVariables = jest.fn();
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
});
- describe('while queries are being fetch', () => {
- beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
- });
-
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
- });
- });
-
- describe('when queries are resolved', () => {
- describe('successfuly', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
-
- await createComponentWithApollo();
- });
-
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
- });
-
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockProjectVariables.data.project.ciVariables.nodes,
- );
- });
-
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
-
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockRejectedValue();
-
- await createComponentWithApollo();
- });
-
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
- });
- });
-
- describe('with an error for environments', () => {
- beforeEach(async () => {
- mockEnvironments.mockRejectedValue();
- mockVariables.mockResolvedValue(mockProjectVariables);
-
- await createComponentWithApollo();
- });
-
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
- });
- });
- });
-
- describe('mutations', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
-
- await createComponentWithApollo();
+ it('Passes down the correct props to ci_variable_shared', () => {
+ expect(findCiShared().props()).toEqual({
+ id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId),
+ areScopedVariablesAvailable: true,
+ componentName: 'ProjectVariables',
+ fullPath: mockProvide.projectFullPath,
+ hideEnvironmentScope: false,
+ mutationData: wrapper.vm.$options.mutationData,
+ queryData: wrapper.vm.$options.queryData,
+ refetchAfterMutation: false,
});
- it.each`
- actionName | mutation | event
- ${'add'} | ${addProjectVariable} | ${'add-variable'}
- ${'update'} | ${updateProjectVariable} | ${'update-variable'}
- ${'delete'} | ${deleteProjectVariable} | ${'delete-variable'}
- `(
- 'calls the right mutation when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- fullPath: mockProvide.projectFullPath,
- projectId: convertToGraphQLId('Project', mockProvide.projectId),
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event | mutationName
- ${'add'} | ${'add-variable'} | ${'addProjectVariable'}
- ${'update'} | ${'update-variable'} | ${'updateProjectVariable'}
- ${'delete'} | ${'delete-variable'} | ${'deleteProjectVariable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event, mutationName }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
- });
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index 1ea4e4f833b..e4771f040d1 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -39,6 +39,7 @@ describe('Ci variable modal', () => {
const defaultProps = {
areScopedVariablesAvailable: true,
environments: [],
+ hideEnvironmentScope: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
variable: [],
@@ -75,6 +76,7 @@ describe('Ci variable modal', () => {
const findEnvScopeInput = () =>
wrapper.findByTestId('environment-scope').findComponent(GlFormInput);
const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
+ const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
afterEach(() => {
wrapper.destroy();
@@ -250,39 +252,83 @@ describe('Ci variable modal', () => {
describe('Environment scope', () => {
describe('when feature is available', () => {
- it('renders the environment dropdown', () => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: true,
- },
+ describe('and section is not hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: false,
+ },
+ });
});
- expect(findCiEnvironmentsDropdown().exists()).toBe(true);
- expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
- });
+ it('renders the environment dropdown and section title', () => {
+ expect(findCiEnvironmentsDropdown().exists()).toBe(true);
+ expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
+ expect(findEnvironmentScopeText().exists()).toBe(true);
+ });
- it('renders a link to documentation on scopes', () => {
- createComponent({ mountFn: mountExtended });
+ it('renders a link to documentation on scopes', () => {
+ const link = findEnvScopeLink();
+
+ expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
+ expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
+ });
+ });
- const link = findEnvScopeLink();
+ describe('and section is hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: true,
+ },
+ });
+ });
- expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
- expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
+ it('does not renders the environment dropdown and section title', () => {
+ expect(findCiEnvironmentsDropdown().exists()).toBe(false);
+ expect(findEnvironmentScopeText().exists()).toBe(false);
+ });
});
});
describe('when feature is not available', () => {
- it('disables the dropdown', () => {
- createComponent({
- mountFn: mountExtended,
- props: {
- areScopedVariablesAvailable: false,
- },
+ describe('and section is not hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: false,
+ hideEnvironmentScope: false,
+ },
+ });
});
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(findEnvScopeInput().attributes('readonly')).toBe('readonly');
+ it('disables the dropdown', () => {
+ expect(findCiEnvironmentsDropdown().exists()).toBe(false);
+ expect(findEnvironmentScopeText().exists()).toBe(true);
+ expect(findEnvScopeInput().attributes('readonly')).toBe('readonly');
+ });
+ });
+
+ describe('and section is hidden', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areScopedVariablesAvailable: false,
+ hideEnvironmentScope: true,
+ },
+ });
+ });
+
+ it('hides the dropdown', () => {
+ expect(findEnvironmentScopeText().exists()).toBe(false);
+ expect(findCiEnvironmentsDropdown().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
deleted file mode 100644
index 4d0c378d10e..00000000000
--- a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import CiVariablePopover from '~/ci_variable_list/components/ci_variable_popover.vue';
-import mockData from '../services/mock_data';
-
-describe('Ci Variable Popover', () => {
- let wrapper;
-
- const defaultProps = {
- target: 'ci-variable-value-22',
- value: mockData.mockPemCert,
- tooltipText: 'Copy value',
- };
-
- const createComponent = (props = defaultProps) => {
- wrapper = shallowMount(CiVariablePopover, {
- propsData: { ...props },
- });
- };
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('displays max count plus ... when character count is over 95', () => {
- expect(wrapper.text()).toHaveLength(98);
- });
-
- it('copies full value to clipboard', () => {
- expect(findButton().attributes('data-clipboard-text')).toEqual(mockData.mockPemCert);
- });
-
- it('displays full value when count is less than max count', () => {
- createComponent({
- target: 'ci-variable-value-22',
- value: 'test_variable_value',
- tooltipText: 'Copy value',
- });
- expect(wrapper.text()).toEqual('test_variable_value');
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
index 5c77ce71b41..8b5a0f7ae9d 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
@@ -18,6 +18,7 @@ describe('Ci variable table', () => {
const defaultProps = {
areScopedVariablesAvailable: true,
environments: mapEnvironmentNames(mockEnvs),
+ hideEnvironmentScope: false,
isLoading: false,
variables: mockVariablesWithScopes(projectString),
};
@@ -56,6 +57,7 @@ describe('Ci variable table', () => {
expect(findCiVariableModal().props()).toEqual({
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
new file mode 100644
index 00000000000..0cc0ee7a9c7
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
@@ -0,0 +1,428 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/settings';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+
+import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
+import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+ environmentFetchErrorText,
+ genericMutationErrorText,
+ variableFetchErrorText,
+} from '~/ci_variable_list/constants';
+
+import {
+ createGroupProps,
+ createInstanceProps,
+ createProjectProps,
+ devName,
+ mockProjectEnvironments,
+ mockProjectVariables,
+ newVariable,
+ prodName,
+ mockGroupVariables,
+ mockAdminVariables,
+} from '../mocks';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const mockProvide = {
+ endpoint: '/variables',
+};
+
+const defaultProps = {
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: false,
+ refetchAfterMutation: false,
+};
+
+describe('Ci Variable Shared Component', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockEnvironments;
+ let mockVariables;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCiTable = () => wrapper.findComponent(GlTable);
+ const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
+
+ // eslint-disable-next-line consistent-return
+ async function createComponentWithApollo({
+ customHandlers = null,
+ isLoading = false,
+ props = { ...createProjectProps() },
+ } = {}) {
+ const handlers = customHandlers || [
+ [getProjectEnvironments, mockEnvironments],
+ [getProjectVariables, mockVariables],
+ ];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = shallowMount(ciVariableShared, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: mockProvide,
+ apolloProvider: mockApollo,
+ stubs: { ciVariableSettings, ciVariableTable },
+ });
+
+ if (!isLoading) {
+ return waitForPromises();
+ }
+ }
+
+ beforeEach(() => {
+ mockEnvironments = jest.fn();
+ mockVariables = jest.fn();
+ });
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries are resolved', () => {
+ describe('successfuly', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
+ });
+
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockProjectVariables.data.project.ciVariables.nodes,
+ );
+ });
+
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockRejectedValue();
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
+ });
+
+ describe('with an error for environments', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockRejectedValue();
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ });
+ });
+ });
+
+ describe('environment query', () => {
+ describe('when there is an environment key in queryData', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
+
+ await createComponentWithApollo({ props: { ...createProjectProps() } });
+ });
+
+ it('is executed', () => {
+ expect(mockVariables).toHaveBeenCalled();
+ });
+ });
+
+ describe('when there isnt an environment key in queryData', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo({ props: { ...createGroupProps() } });
+ });
+
+ it('is skipped', () => {
+ expect(mockVariables).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ const groupProps = createGroupProps();
+
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: groupProps,
+ });
+ });
+ it.each`
+ actionName | mutation | event
+ ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'}
+ ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'}
+ ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'}
+ `(
+ 'calls the right mutation from propsData when user performs $actionName variable',
+ async ({ event, mutation }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ endpoint: mockProvide.endpoint,
+ fullPath: groupProps.fullPath,
+ id: convertToGraphQLId('Group', groupProps.id),
+ variable: newVariable,
+ },
+ });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error on failure with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+
+ describe('without fullpath and ID props', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ props: createInstanceProps(),
+ });
+ });
+
+ it('does not pass fullPath and ID to the mutation', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION],
+ variables: {
+ endpoint: mockProvide.endpoint,
+ variable: newVariable,
+ },
+ });
+ });
+ });
+ });
+
+ describe('Props', () => {
+ describe('in a specific context as', () => {
+ it.each`
+ name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | mutation
+ ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${null}
+ ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${getGroupVariables}
+ ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${getAdminVariables}
+ `(
+ 'passes down all the required props when its a $name component',
+ async ({
+ mutation,
+ mockVariablesValue,
+ mockEnvironmentsValue,
+ withEnvironments,
+ expectedEnvironments,
+ propsFn,
+ }) => {
+ const props = propsFn();
+
+ mockVariables.mockResolvedValue(mockVariablesValue);
+
+ if (withEnvironments) {
+ mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
+ }
+
+ let customHandlers = null;
+
+ if (mutation) {
+ customHandlers = [[mutation, mockVariables]];
+ }
+
+ await createComponentWithApollo({ customHandlers, props });
+
+ expect(findCiSettings().props()).toEqual({
+ areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ isLoading: false,
+ variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes,
+ environments: expectedEnvironments,
+ });
+ },
+ );
+ });
+
+ describe('refetchAfterMutation', () => {
+ it.each`
+ bool | text
+ ${true} | ${'refetches the variables'}
+ ${false} | ${'does not refetch the variables'}
+ `('when $bool it $text', async ({ bool }) => {
+ await createComponentWithApollo({
+ props: { ...createInstanceProps(), refetchAfterMutation: bool },
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
+ jest.spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch').mockImplementation(jest.fn());
+
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+
+ await nextTick();
+
+ if (bool) {
+ expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled();
+ } else {
+ expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled();
+ }
+ });
+ });
+
+ describe('Validators', () => {
+ describe('queryData', () => {
+ let error;
+
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps() },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('will not mount component with wrong data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps(), queryData: { wrongKey: {} } },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(false);
+ expect(error.toString()).toContain('custom validator check failed for prop');
+ }
+ });
+ });
+
+ describe('mutationData', () => {
+ let error;
+
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('will not mount component with wrong data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(false);
+ expect(error.toString()).toContain('custom validator check failed for prop');
+ }
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
deleted file mode 100644
index b3e23ba4201..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import LegacyCiEnvironmentsDropdown from '~/ci_variable_list/components/legacy_ci_environments_dropdown.vue';
-
-Vue.use(Vuex);
-
-describe('Ci environments dropdown', () => {
- let wrapper;
- let store;
-
- const enterSearchTerm = (value) =>
- wrapper.find('[data-testid="ci-environment-search"]').setValue(value);
-
- const createComponent = (term) => {
- store = new Vuex.Store({
- getters: {
- joinedEnvironments: () => ['dev', 'prod', 'staging'],
- },
- });
-
- wrapper = mount(LegacyCiEnvironmentsDropdown, {
- store,
- propsData: {
- value: term,
- },
- });
- enterSearchTerm(term);
- };
-
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('No environments found', () => {
- beforeEach(() => {
- createComponent('stable');
- });
-
- it('renders create button with search term if environments do not contain search term', () => {
- expect(findAllDropdownItems()).toHaveLength(2);
- expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
- });
-
- it('renders empty results message', () => {
- expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
- });
- });
-
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent('');
- });
-
- it('renders all environments when search term is empty', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe('dev');
- expect(findDropdownItemByIndex(1).text()).toBe('prod');
- expect(findDropdownItemByIndex(2).text()).toBe('staging');
- });
-
- it('should not display active checkmark on the inactive stage', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
- });
-
- describe('Environments found', () => {
- beforeEach(async () => {
- createComponent('prod');
- await nextTick();
- });
-
- it('renders only the environment searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('prod');
- });
-
- it('should not display create button', () => {
- const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create'));
- expect(environments).toHaveLength(0);
- expect(findAllDropdownItems()).toHaveLength(1);
- });
-
- it('should not display empty results message', () => {
- expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false);
- });
-
- it('should display active checkmark if active', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(false);
- });
-
- it('should clear the search term when showing the dropdown', () => {
- wrapper.findComponent(GlDropdown).trigger('click');
-
- expect(wrapper.find('[data-testid="ci-environment-search"]').text()).toBe('');
- });
-
- describe('Custom events', () => {
- it('should emit selectEnvironment if an environment is clicked', () => {
- findDropdownItemByIndex(0).vm.$emit('click');
- expect(wrapper.emitted('selectEnvironment')).toEqual([['prod']]);
- });
-
- it('should emit createClicked if an environment is clicked', async () => {
- createComponent('newscope');
-
- await nextTick();
- findDropdownItemByIndex(1).vm.$emit('click');
- expect(wrapper.emitted('createClicked')).toEqual([['newscope']]);
- });
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
deleted file mode 100644
index b607232907b..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
+++ /dev/null
@@ -1,323 +0,0 @@
-import { GlButton, GlFormInput } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mockTracking } from 'helpers/tracking_helper';
-import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
-import LegacyCiVariableModal from '~/ci_variable_list/components/legacy_ci_variable_modal.vue';
-import {
- AWS_ACCESS_KEY_ID,
- EVENT_LABEL,
- EVENT_ACTION,
- ENVIRONMENT_SCOPE_LINK_TITLE,
-} from '~/ci_variable_list/constants';
-import createStore from '~/ci_variable_list/store';
-import mockData from '../services/mock_data';
-import ModalStub from '../stubs';
-
-Vue.use(Vuex);
-
-describe('Ci variable modal', () => {
- let wrapper;
- let store;
- let trackingSpy;
-
- const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
-
- const createComponent = (method, options = {}) => {
- store = createStore({
- maskableRegex,
- isGroup: options.isGroup,
- environmentScopeLink: '/help/environments',
- });
- wrapper = method(LegacyCiVariableModal, {
- attachTo: document.body,
- stubs: {
- GlModal: ModalStub,
- },
- store,
- ...options,
- });
- };
-
- const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
- const findModal = () => wrapper.findComponent(ModalStub);
- const findAddorUpdateButton = () => findModal().find('[data-testid="ciUpdateOrAddVariableBtn"]');
- const deleteVariableButton = () =>
- findModal()
- .findAllComponents(GlButton)
- .wrappers.find((button) => button.props('variant') === 'danger');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Basic interactions', () => {
- beforeEach(() => {
- createComponent(shallowMount);
- });
-
- it('button is disabled when no key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
- });
- });
-
- describe('Adding a new variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- createComponent(shallowMount);
- jest.spyOn(store, 'dispatch').mockImplementation();
- store.state.variable = variable;
- });
-
- it('button is enabled when key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
-
- it('Add variable button dispatches addVariable action', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('addVariable');
- });
-
- it('Clears the modal state once modal is hidden', () => {
- findModal().vm.$emit('hidden');
- expect(store.dispatch).toHaveBeenCalledWith('clearModal');
- });
-
- it('should dispatch setVariableProtected when admin settings are configured to protect variables', () => {
- store.state.isProtectedByDefault = true;
- findModal().vm.$emit('shown');
-
- expect(store.dispatch).toHaveBeenCalledWith('setVariableProtected');
- });
- });
-
- describe('Adding a new non-AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value: 'value',
- secret_value: 'secret_value',
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- });
-
- it('does not show AWS guidance tip', () => {
- const tip = wrapper.find(`div[data-testid='aws-guidance-tip']`);
- expect(tip.exists()).toBe(true);
- expect(tip.isVisible()).toBe(false);
- });
- });
-
- describe('Adding a new AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- secret_value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- });
-
- it('shows AWS guidance tip', () => {
- const tip = wrapper.find(`[data-testid='aws-guidance-tip']`);
- expect(tip.exists()).toBe(true);
- expect(tip.isVisible()).toBe(true);
- });
- });
-
- describe.each`
- value | secret | rendered
- ${'value'} | ${'secret_value'} | ${false}
- ${'dollar$ign'} | ${'dollar$ign'} | ${true}
- `('Adding a new variable', ({ value, secret, rendered }) => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value,
- secret_value: secret,
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it(`${rendered ? 'renders' : 'does not render'} the variable reference warning`, () => {
- const warning = wrapper.find(`[data-testid='contains-variable-reference']`);
- expect(warning.exists()).toBe(rendered);
- });
- });
-
- describe('Editing a variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- createComponent(shallowMount);
- jest.spyOn(store, 'dispatch').mockImplementation();
- store.state.variableBeingEdited = variable;
- });
-
- it('button text is Update variable when updating', () => {
- expect(findAddorUpdateButton().text()).toBe('Update variable');
- });
-
- it('Update variable button dispatches updateVariable with correct variable', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('updateVariable');
- });
-
- it('Resets the editing state once modal is hidden', () => {
- findModal().vm.$emit('hidden');
- expect(store.dispatch).toHaveBeenCalledWith('resetEditing');
- });
-
- it('dispatches deleteVariable with correct variable to delete', () => {
- deleteVariableButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('deleteVariable');
- });
- });
-
- describe('Environment scope', () => {
- describe('group level variables', () => {
- it('renders the environment dropdown', () => {
- createComponent(shallowMount, {
- isGroup: true,
- provide: {
- glFeatures: {
- groupScopedCiVariables: true,
- },
- },
- });
-
- expect(findCiEnvironmentsDropdown().exists()).toBe(true);
- expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
- });
-
- describe('licensed feature is not available', () => {
- it('disables the dropdown', () => {
- createComponent(mount, {
- isGroup: true,
- provide: {
- glFeatures: {
- groupScopedCiVariables: false,
- },
- },
- });
-
- const environmentScopeInput = wrapper
- .find('[data-testid="environment-scope"]')
- .findComponent(GlFormInput);
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(environmentScopeInput.attributes('readonly')).toBe('readonly');
- });
- });
- });
-
- it('renders a link to documentation on scopes', () => {
- createComponent(mount);
-
- const link = wrapper.find('[data-testid="environment-scope-link"]');
-
- expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
- expect(link.attributes('href')).toBe('/help/environments');
- });
- });
-
- describe('Validations', () => {
- const maskError = 'This variable can not be masked.';
-
- describe('when the mask state is invalid', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidMaskVariable = {
- ...variable,
- key: 'qs',
- value: 'd:;',
- secret_value: 'd:;',
- masked: true,
- };
- createComponent(mount);
- store.state.variable = invalidMaskVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
- });
-
- it('shows the correct error text', () => {
- expect(findModal().text()).toContain(maskError);
- });
-
- it('sends the correct tracking event', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: ';',
- });
- });
- });
-
- describe.each`
- value | secret | masked | eventSent | trackingErrorProperty
- ${'value'} | ${'secretValue'} | ${false} | ${0} | ${null}
- ${'shortMasked'} | ${'short'} | ${true} | ${0} | ${null}
- ${'withDollar$Sign'} | ${'dollar$ign'} | ${false} | ${1} | ${'$'}
- ${'withDollar$Sign'} | ${'dollar$ign'} | ${true} | ${1} | ${'$'}
- ${'unsupported'} | ${'unsupported|char'} | ${true} | ${1} | ${'|'}
- ${'unsupportedMasked'} | ${'unsupported|char'} | ${false} | ${0} | ${null}
- `('Adding a new variable', ({ value, secret, masked, eventSent, trackingErrorProperty }) => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value,
- secret_value: secret,
- masked,
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it(`${
- eventSent > 0 ? 'sends the correct' : 'does not send the'
- } variable validation tracking event`, () => {
- expect(trackingSpy).toHaveBeenCalledTimes(eventSent);
-
- if (eventSent > 0) {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: trackingErrorProperty,
- });
- }
- });
- });
-
- describe('when both states are valid', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const validMaskandKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: '12345678',
- secret_value: '87654321',
- masked: true,
- };
- createComponent(mount);
- store.state.variable = validMaskandKeyVariable;
- });
-
- it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
deleted file mode 100644
index 7def4dd4f29..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import LegacyCiVariableSettings from '~/ci_variable_list/components/legacy_ci_variable_settings.vue';
-import createStore from '~/ci_variable_list/store';
-
-Vue.use(Vuex);
-
-describe('Ci variable table', () => {
- let wrapper;
- let store;
- let isProject;
-
- const createComponent = (projectState) => {
- store = createStore();
- store.state.isProject = projectState;
- jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = shallowMount(LegacyCiVariableSettings, {
- store,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches fetchEnvironments when mounted', () => {
- isProject = true;
- createComponent(isProject);
- expect(store.dispatch).toHaveBeenCalledWith('fetchEnvironments');
- });
-
- it('does not dispatch fetchenvironments when in group context', () => {
- isProject = false;
- createComponent(isProject);
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
deleted file mode 100644
index 310afc8003a..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import LegacyCiVariableTable from '~/ci_variable_list/components/legacy_ci_variable_table.vue';
-import createStore from '~/ci_variable_list/store';
-import mockData from '../services/mock_data';
-
-Vue.use(Vuex);
-
-describe('Ci variable table', () => {
- let wrapper;
- let store;
-
- const createComponent = () => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = mountExtended(LegacyCiVariableTable, {
- attachTo: document.body,
- store,
- });
- };
-
- const findRevealButton = () => wrapper.findByText('Reveal values');
- const findEditButton = () => wrapper.findByLabelText('Edit');
- const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches fetchVariables when mounted', () => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
- });
-
- describe('When table is empty', () => {
- beforeEach(() => {
- store.state.variables = [];
- });
-
- it('displays empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
- });
-
- it('hides the reveal button', () => {
- expect(findRevealButton().exists()).toBe(false);
- });
- });
-
- describe('When table has variables', () => {
- beforeEach(() => {
- store.state.variables = mockData.mockVariables;
- });
-
- it('does not display the empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
- });
-
- it('displays the reveal button', () => {
- expect(findRevealButton().exists()).toBe(true);
- });
-
- it('displays the correct amount of variables', async () => {
- expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(1);
- });
- });
-
- describe('Table click actions', () => {
- beforeEach(() => {
- store.state.variables = mockData.mockVariables;
- });
-
- it('reveals secret values when button is clicked', () => {
- findRevealButton().trigger('click');
- expect(store.dispatch).toHaveBeenCalledWith('toggleValues', false);
- });
-
- it('dispatches editVariable with correct variable to edit', () => {
- findEditButton().trigger('click');
- expect(store.dispatch).toHaveBeenCalledWith('editVariable', mockData.mockVariables[0]);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 6f3e73f8b83..03b77f80430 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -1,10 +1,28 @@
import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
variableTypes,
groupString,
instanceString,
projectString,
} from '~/ci_variable_list/constants';
+import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
+import addProjectVariable from '~/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
+import deleteProjectVariable from '~/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
+import updateProjectVariable from '~/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
+
+import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+
export const devName = 'dev';
export const prodName = 'prod';
@@ -118,3 +136,62 @@ export const newVariable = {
value: 'devops',
variableType: variableTypes.variableType,
};
+
+export const createProjectProps = () => {
+ return {
+ componentName: 'ProjectVariable',
+ fullPath: '/namespace/project/',
+ id: 'gid://gitlab/Project/20',
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addProjectVariable,
+ [UPDATE_MUTATION_ACTION]: updateProjectVariable,
+ [DELETE_MUTATION_ACTION]: deleteProjectVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.project?.ciVariables,
+ query: getProjectVariables,
+ },
+ environments: {
+ lookup: (data) => data?.project?.environments,
+ query: getProjectEnvironments,
+ },
+ },
+ };
+};
+
+export const createGroupProps = () => {
+ return {
+ componentName: 'GroupVariable',
+ fullPath: '/my-group',
+ id: 'gid://gitlab/Group/20',
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addGroupVariable,
+ [UPDATE_MUTATION_ACTION]: updateGroupVariable,
+ [DELETE_MUTATION_ACTION]: deleteGroupVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.group?.ciVariables,
+ query: getGroupVariables,
+ },
+ },
+ };
+};
+
+export const createInstanceProps = () => {
+ return {
+ componentName: 'InstanceVariable',
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addAdminVariable,
+ [UPDATE_MUTATION_ACTION]: updateAdminVariable,
+ [DELETE_MUTATION_ACTION]: deleteAdminVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.ciVariables,
+ query: getAdminVariables,
+ },
+ },
+ };
+};
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
deleted file mode 100644
index e8c81a53a55..00000000000
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ /dev/null
@@ -1,319 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import Api from '~/api';
-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 { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import mockData from '../services/mock_data';
-
-jest.mock('~/api.js');
-jest.mock('~/flash.js');
-
-describe('CI variable list store actions', () => {
- let mock;
- let state;
- const mockVariable = {
- environment_scope: '*',
- id: 63,
- key: 'test_var',
- masked: false,
- protected: false,
- value: 'test_val',
- variable_type: 'env_var',
- _destory: true,
- };
- const payloadError = new Error('Request failed with status code 500');
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- state = getInitialState();
- state.endpoint = '/variables';
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('toggleValues', () => {
- const valuesHidden = false;
- it('commits TOGGLE_VALUES mutation', () => {
- testAction(actions.toggleValues, valuesHidden, {}, [
- {
- type: types.TOGGLE_VALUES,
- payload: valuesHidden,
- },
- ]);
- });
- });
-
- describe('clearModal', () => {
- it('commits CLEAR_MODAL mutation', () => {
- testAction(actions.clearModal, {}, {}, [
- {
- type: types.CLEAR_MODAL,
- },
- ]);
- });
- });
-
- describe('resetEditing', () => {
- it('commits RESET_EDITING mutation', () => {
- testAction(
- actions.resetEditing,
- {},
- {},
- [
- {
- type: types.RESET_EDITING,
- },
- ],
- [{ type: 'fetchVariables' }],
- );
- });
- });
-
- describe('setVariableProtected', () => {
- it('commits SET_VARIABLE_PROTECTED mutation', () => {
- testAction(actions.setVariableProtected, {}, {}, [
- {
- type: types.SET_VARIABLE_PROTECTED,
- },
- ]);
- });
- });
-
- describe('deleteVariable', () => {
- it('dispatch correct actions on successful deleted variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.deleteVariable,
- {},
- state,
- [],
- [
- { type: 'requestDeleteVariable' },
- { type: 'receiveDeleteVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on delete failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.deleteVariable,
- {},
- state,
- [],
- [
- { type: 'requestDeleteVariable' },
- {
- type: 'receiveDeleteVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('updateVariable', () => {
- it('dispatch correct actions on successful updated variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.updateVariable,
- {},
- state,
- [],
- [
- { type: 'requestUpdateVariable' },
- { type: 'receiveUpdateVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on update failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.updateVariable,
- mockVariable,
- state,
- [],
- [
- { type: 'requestUpdateVariable' },
- {
- type: 'receiveUpdateVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('addVariable', () => {
- it('dispatch correct actions on successful added variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.addVariable,
- {},
- state,
- [],
- [
- { type: 'requestAddVariable' },
- { type: 'receiveAddVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on add failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.addVariable,
- {},
- state,
- [],
- [
- { type: 'requestAddVariable' },
- {
- type: 'receiveAddVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('fetchVariables', () => {
- it('dispatch correct actions on fetchVariables', () => {
- mock.onGet(state.endpoint).reply(200, { variables: mockData.mockVariables });
-
- return testAction(
- actions.fetchVariables,
- {},
- state,
- [],
- [
- { type: 'requestVariables' },
- {
- type: 'receiveVariablesSuccess',
- payload: prepareDataForDisplay(mockData.mockVariables),
- },
- ],
- );
- });
-
- it('should show flash error and set error in state on fetch variables failure', async () => {
- mock.onGet(state.endpoint).reply(500);
-
- await testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }]);
- expect(createAlert).toHaveBeenCalledWith({
- message: 'There was an error fetching the variables.',
- });
- });
- });
-
- describe('fetchEnvironments', () => {
- it('dispatch correct actions on fetchEnvironments', () => {
- Api.environments = jest.fn().mockResolvedValue({ data: mockData.mockEnvironments });
-
- return testAction(
- actions.fetchEnvironments,
- {},
- state,
- [],
- [
- { type: 'requestEnvironments' },
- {
- type: 'receiveEnvironmentsSuccess',
- payload: prepareEnvironments(mockData.mockEnvironments),
- },
- ],
- );
- });
-
- it('should show flash error and set error in state on fetch environments failure', async () => {
- Api.environments = jest.fn().mockRejectedValue();
-
- await testAction(actions.fetchEnvironments, {}, state, [], [{ type: 'requestEnvironments' }]);
-
- expect(createAlert).toHaveBeenCalledWith({
- message: 'There was an error fetching the environments information.',
- });
- });
- });
-
- describe('Update variable values', () => {
- it('updateVariableKey', () => {
- testAction(
- actions.updateVariableKey,
- { key: mockVariable.key },
- {},
- [
- {
- type: types.UPDATE_VARIABLE_KEY,
- payload: mockVariable.key,
- },
- ],
- [],
- );
- });
-
- it('updateVariableValue', () => {
- testAction(
- actions.updateVariableValue,
- { secret_value: mockVariable.value },
- {},
- [
- {
- type: types.UPDATE_VARIABLE_VALUE,
- payload: mockVariable.value,
- },
- ],
- [],
- );
- });
-
- it('updateVariableType', () => {
- testAction(
- actions.updateVariableType,
- { variable_type: mockVariable.variable_type },
- {},
- [{ type: types.UPDATE_VARIABLE_TYPE, payload: mockVariable.variable_type }],
- [],
- );
- });
-
- it('updateVariableProtected', () => {
- testAction(
- actions.updateVariableProtected,
- { protected_variable: mockVariable.protected },
- {},
- [{ type: types.UPDATE_VARIABLE_PROTECTED, payload: mockVariable.protected }],
- [],
- );
- });
-
- it('updateVariableMasked', () => {
- testAction(
- actions.updateVariableMasked,
- { masked: mockVariable.masked },
- {},
- [{ type: types.UPDATE_VARIABLE_MASKED, payload: mockVariable.masked }],
- [],
- );
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/getters_spec.js b/spec/frontend/ci_variable_list/store/getters_spec.js
deleted file mode 100644
index 92f22b18763..00000000000
--- a/spec/frontend/ci_variable_list/store/getters_spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as getters from '~/ci_variable_list/store/getters';
-import mockData from '../services/mock_data';
-
-describe('Ci variable getters', () => {
- describe('joinedEnvironments', () => {
- it('should join fetched environments with variable environment scopes', () => {
- const state = {
- environments: ['All (default)', 'staging', 'deployment', 'prod'],
- variables: mockData.mockVariableScopes,
- };
-
- expect(getters.joinedEnvironments(state)).toEqual([
- 'All (default)',
- 'deployment',
- 'prod',
- 'production',
- 'staging',
- ]);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
deleted file mode 100644
index c7d07ead09b..00000000000
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import * as types from '~/ci_variable_list/store/mutation_types';
-import mutations from '~/ci_variable_list/store/mutations';
-import state from '~/ci_variable_list/store/state';
-
-describe('CI variable list mutations', () => {
- let stateCopy;
-
- beforeEach(() => {
- stateCopy = state();
- });
-
- describe('TOGGLE_VALUES', () => {
- it('should toggle state', () => {
- const valuesHidden = false;
-
- mutations[types.TOGGLE_VALUES](stateCopy, valuesHidden);
-
- expect(stateCopy.valuesHidden).toEqual(valuesHidden);
- });
- });
-
- describe('VARIABLE_BEING_EDITED', () => {
- it('should set the variable that is being edited', () => {
- mutations[types.VARIABLE_BEING_EDITED](stateCopy);
-
- expect(stateCopy.variableBeingEdited).toBe(true);
- });
- });
-
- describe('RESET_EDITING', () => {
- it('should reset variableBeingEdited to false', () => {
- mutations[types.RESET_EDITING](stateCopy);
-
- expect(stateCopy.variableBeingEdited).toBe(false);
- });
- });
-
- describe('CLEAR_MODAL', () => {
- it('should clear modal state', () => {
- const modalState = {
- variable_type: 'Variable',
- key: '',
- secret_value: '',
- protected_variable: false,
- masked: false,
- environment_scope: 'All (default)',
- };
-
- mutations[types.CLEAR_MODAL](stateCopy);
-
- expect(stateCopy.variable).toEqual(modalState);
- });
- });
-
- describe('RECEIVE_ENVIRONMENTS_SUCCESS', () => {
- it('should set environments', () => {
- const environments = ['env1', 'env2'];
-
- mutations[types.RECEIVE_ENVIRONMENTS_SUCCESS](stateCopy, environments);
-
- expect(stateCopy.environments).toEqual(['All (default)', 'env1', 'env2']);
- });
- });
-
- describe('SET_ENVIRONMENT_SCOPE', () => {
- const environment = 'production';
-
- it('should set environment scope on variable', () => {
- mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
-
- expect(stateCopy.variable.environment_scope).toBe('production');
- });
- });
-
- describe('ADD_WILD_CARD_SCOPE', () => {
- it('should add wild card scope to environments array and sort', () => {
- stateCopy.environments = ['dev', 'staging'];
- mutations[types.ADD_WILD_CARD_SCOPE](stateCopy, 'production');
-
- expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']);
- });
- });
-
- describe('SET_VARIABLE_PROTECTED', () => {
- it('should set protected value to true', () => {
- mutations[types.SET_VARIABLE_PROTECTED](stateCopy);
-
- expect(stateCopy.variable.protected_variable).toBe(true);
- });
- });
-
- describe('UPDATE_VARIABLE_KEY', () => {
- it('should update variable key value', () => {
- const key = 'new_var';
- mutations[types.UPDATE_VARIABLE_KEY](stateCopy, key);
-
- expect(stateCopy.variable.key).toBe(key);
- });
- });
-
- describe('UPDATE_VARIABLE_VALUE', () => {
- it('should update variable value', () => {
- const value = 'variable_value';
- mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, value);
-
- expect(stateCopy.variable.secret_value).toBe(value);
- });
- });
-
- describe('UPDATE_VARIABLE_TYPE', () => {
- it('should update variable type value', () => {
- const type = 'File';
- mutations[types.UPDATE_VARIABLE_TYPE](stateCopy, type);
-
- expect(stateCopy.variable.variable_type).toBe(type);
- });
- });
-
- describe('UPDATE_VARIABLE_PROTECTED', () => {
- it('should update variable protected value', () => {
- const protectedValue = true;
- mutations[types.UPDATE_VARIABLE_PROTECTED](stateCopy, protectedValue);
-
- expect(stateCopy.variable.protected_variable).toBe(protectedValue);
- });
- });
-
- describe('UPDATE_VARIABLE_MASKED', () => {
- it('should update variable masked value', () => {
- const masked = true;
- mutations[types.UPDATE_VARIABLE_MASKED](stateCopy, masked);
-
- expect(stateCopy.variable.masked).toBe(masked);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/utils_spec.js b/spec/frontend/ci_variable_list/store/utils_spec.js
deleted file mode 100644
index 5b10370324a..00000000000
--- a/spec/frontend/ci_variable_list/store/utils_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- prepareDataForDisplay,
- prepareEnvironments,
- prepareDataForApi,
-} from '~/ci_variable_list/store/utils';
-import mockData from '../services/mock_data';
-
-describe('CI variables store utils', () => {
- it('prepares ci variables for display', () => {
- expect(prepareDataForDisplay(mockData.mockVariablesApi)).toStrictEqual(
- mockData.mockVariablesDisplay,
- );
- });
-
- it('prepares single ci variable for api', () => {
- expect(prepareDataForApi(mockData.mockVariablesDisplay[0])).toStrictEqual({
- environment_scope: '*',
- id: 113,
- key: 'test_var',
- masked: 'false',
- protected: 'false',
- secret_value: 'test_val',
- value: 'test_val',
- variable_type: 'env_var',
- });
-
- expect(prepareDataForApi(mockData.mockVariablesDisplay[1])).toStrictEqual({
- environment_scope: '*',
- id: 114,
- key: 'test_var_2',
- masked: 'false',
- protected: 'false',
- secret_value: 'test_val_2',
- value: 'test_val_2',
- variable_type: 'file',
- });
- });
-
- it('prepares single ci variable for delete', () => {
- expect(prepareDataForApi(mockData.mockVariablesDisplay[0], true)).toHaveProperty(
- '_destroy',
- true,
- );
- });
-
- it('prepares environments for display', () => {
- expect(prepareEnvironments(mockData.mockEnvironments)).toStrictEqual(['staging', 'production']);
- });
-});
diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
index cce17176129..98001858851 100644
--- a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
@@ -56,6 +56,7 @@ describe('content_editor/components/bubble_menus/formatting_bubble_menu', () =>
${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
${'superscript'} | ${{ contentType: 'superscript', iconName: 'superscript', label: 'Superscript', editorCommand: 'toggleSuperscript' }}
${'subscript'} | ${{ contentType: 'subscript', iconName: 'subscript', label: 'Subscript', editorCommand: 'toggleSubscript' }}
+ ${'highlight'} | ${{ contentType: 'highlight', iconName: 'highlight', label: 'Highlight', editorCommand: 'toggleHighlight' }}
${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link', editorCommand: 'toggleLink', editorCommandParams: { href: '' } }}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec.js b/spec/frontend/content_editor/markdown_snapshot_spec.js
index 63ca66172e6..146208bf8c7 100644
--- a/spec/frontend/content_editor/markdown_snapshot_spec.js
+++ b/spec/frontend/content_editor/markdown_snapshot_spec.js
@@ -1,10 +1,11 @@
-import path from 'path';
import { describeMarkdownSnapshots } from 'jest/content_editor/markdown_snapshot_spec_helper';
jest.mock('~/emoji');
-const glfmSpecificationDir = path.join(__dirname, '..', '..', '..', 'glfm_specification');
-
// See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
// for documentation on this spec.
-describeMarkdownSnapshots('CE markdown snapshots in ContentEditor', glfmSpecificationDir);
+//
+// NOTE: Unlike the backend markdown_snapshot_spec.rb which has a CE and EE version, there is only
+// one version of this spec. This is because the frontend markdown rendering does not require EE-only
+// backend features.
+describeMarkdownSnapshots('markdown example snapshots in ContentEditor');
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec_helper.js b/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
index 05fa8e6a6b2..64988c5b717 100644
--- a/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
+++ b/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
@@ -1,10 +1,12 @@
// See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
// for documentation on this spec.
-import fs from 'fs';
-import path from 'path';
import jsYaml from 'js-yaml';
import { pick } from 'lodash';
+import glfmExampleStatusYml from '../../../glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml';
+import markdownYml from '../../../glfm_specification/output_example_snapshots/markdown.yml';
+import htmlYml from '../../../glfm_specification/output_example_snapshots/html.yml';
+import prosemirrorJsonYml from '../../../glfm_specification/output_example_snapshots/prosemirror_json.yml';
import {
IMPLEMENTATION_ERROR_MSG,
renderHtmlAndJsonForAllExamples,
@@ -18,29 +20,21 @@ const filterExamples = (examples) => {
return pick(examples, focusedMarkdownExamples);
};
-const loadExamples = (dir, fileName) => {
- const yaml = fs.readFileSync(path.join(dir, fileName));
+const loadExamples = (yaml) => {
const examples = jsYaml.safeLoad(yaml, {});
return filterExamples(examples);
};
// eslint-disable-next-line jest/no-export
-export const describeMarkdownSnapshots = (description, glfmSpecificationDir) => {
+export const describeMarkdownSnapshots = (description) => {
let actualHtmlAndJsonExamples;
let skipRunningSnapshotWysiwygHtmlTests;
let skipRunningSnapshotProsemirrorJsonTests;
- const exampleStatuses = loadExamples(
- path.join(glfmSpecificationDir, 'input', 'gitlab_flavored_markdown'),
- 'glfm_example_status.yml',
- );
- const glfmExampleSnapshotsDir = path.join(glfmSpecificationDir, 'example_snapshots');
- const markdownExamples = loadExamples(glfmExampleSnapshotsDir, 'markdown.yml');
- const expectedHtmlExamples = loadExamples(glfmExampleSnapshotsDir, 'html.yml');
- const expectedProseMirrorJsonExamples = loadExamples(
- glfmExampleSnapshotsDir,
- 'prosemirror_json.yml',
- );
+ const exampleStatuses = loadExamples(glfmExampleStatusYml);
+ const markdownExamples = loadExamples(markdownYml);
+ const expectedHtmlExamples = loadExamples(htmlYml);
+ const expectedProseMirrorJsonExamples = loadExamples(prosemirrorJsonYml);
beforeAll(async () => {
return renderHtmlAndJsonForAllExamples(markdownExamples).then((examples) => {
diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
index bd48b7fdd23..5df901e0f15 100644
--- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
+++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
@@ -1,87 +1,8 @@
import { DOMSerializer } from 'prosemirror-model';
-// TODO: DRY up duplication with spec/frontend/content_editor/services/markdown_serializer_spec.js
-// See https://gitlab.com/groups/gitlab-org/-/epics/7719#plan
-import Audio from '~/content_editor/extensions/audio';
-import Blockquote from '~/content_editor/extensions/blockquote';
-import Bold from '~/content_editor/extensions/bold';
-import BulletList from '~/content_editor/extensions/bullet_list';
-import Code from '~/content_editor/extensions/code';
-import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import DescriptionItem from '~/content_editor/extensions/description_item';
-import DescriptionList from '~/content_editor/extensions/description_list';
-import Details from '~/content_editor/extensions/details';
-import DetailsContent from '~/content_editor/extensions/details_content';
-import Emoji from '~/content_editor/extensions/emoji';
-import Figure from '~/content_editor/extensions/figure';
-import FigureCaption from '~/content_editor/extensions/figure_caption';
-import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
-import FootnoteReference from '~/content_editor/extensions/footnote_reference';
-import FootnotesSection from '~/content_editor/extensions/footnotes_section';
-import Frontmatter from '~/content_editor/extensions/frontmatter';
-import HardBreak from '~/content_editor/extensions/hard_break';
-import Heading from '~/content_editor/extensions/heading';
-import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
-import HTMLNodes from '~/content_editor/extensions/html_nodes';
-import Image from '~/content_editor/extensions/image';
-import InlineDiff from '~/content_editor/extensions/inline_diff';
-import Italic from '~/content_editor/extensions/italic';
-import Link from '~/content_editor/extensions/link';
-import ListItem from '~/content_editor/extensions/list_item';
-import OrderedList from '~/content_editor/extensions/ordered_list';
-import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
-import Strike from '~/content_editor/extensions/strike';
-import Table from '~/content_editor/extensions/table';
-import TableCell from '~/content_editor/extensions/table_cell';
-import TableHeader from '~/content_editor/extensions/table_header';
-import TableRow from '~/content_editor/extensions/table_row';
-import TableOfContents from '~/content_editor/extensions/table_of_contents';
-import TaskItem from '~/content_editor/extensions/task_item';
-import TaskList from '~/content_editor/extensions/task_list';
-import Video from '~/content_editor/extensions/video';
import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
-import { createTestEditor } from 'jest/content_editor/test_utils';
+import { createTiptapEditor } from 'jest/content_editor/test_utils';
-const tiptapEditor = createTestEditor({
- extensions: [
- Audio,
- Blockquote,
- Bold,
- BulletList,
- Code,
- CodeBlockHighlight,
- DescriptionItem,
- DescriptionList,
- Details,
- DetailsContent,
- Emoji,
- FootnoteDefinition,
- FootnoteReference,
- FootnotesSection,
- Frontmatter,
- Figure,
- FigureCaption,
- HardBreak,
- Heading,
- HorizontalRule,
- ...HTMLNodes,
- Image,
- InlineDiff,
- Italic,
- Link,
- ListItem,
- OrderedList,
- ReferenceDefinition,
- Strike,
- Table,
- TableCell,
- TableHeader,
- TableRow,
- TableOfContents,
- TaskItem,
- TaskList,
- Video,
- ],
-});
+const tiptapEditor = createTiptapEditor();
export const IMPLEMENTATION_ERROR_MSG = 'Error - check implementation';
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 32193d97fd8..1bf23415052 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -1,4 +1,3 @@
-import Audio from '~/content_editor/extensions/audio';
import Blockquote from '~/content_editor/extensions/blockquote';
import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
@@ -16,7 +15,7 @@ import FootnoteReference from '~/content_editor/extensions/footnote_reference';
import HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
-import HTMLMarks from '~/content_editor/extensions/html_marks';
+import Highlight from '~/content_editor/extensions/highlight';
import HTMLNodes from '~/content_editor/extensions/html_nodes';
import Image from '~/content_editor/extensions/image';
import InlineDiff from '~/content_editor/extensions/inline_diff';
@@ -34,53 +33,13 @@ import TableHeader from '~/content_editor/extensions/table_header';
import TableRow from '~/content_editor/extensions/table_row';
import TaskItem from '~/content_editor/extensions/task_item';
import TaskList from '~/content_editor/extensions/task_list';
-import Video from '~/content_editor/extensions/video';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTiptapEditor, createDocBuilder } from '../test_utils';
jest.mock('~/emoji');
-const tiptapEditor = createTestEditor({
- extensions: [
- Audio,
- Blockquote,
- Bold,
- BulletList,
- Code,
- CodeBlockHighlight,
- DescriptionItem,
- DescriptionList,
- Details,
- DetailsContent,
- Emoji,
- FootnoteDefinition,
- FootnoteReference,
- Figure,
- FigureCaption,
- HardBreak,
- Heading,
- HorizontalRule,
- Image,
- InlineDiff,
- Italic,
- Link,
- ListItem,
- OrderedList,
- ReferenceDefinition,
- Sourcemap,
- Strike,
- Table,
- TableCell,
- TableHeader,
- TableRow,
- TaskItem,
- TaskList,
- Video,
- ...HTMLMarks,
- ...HTMLNodes,
- ],
-});
+const tiptapEditor = createTiptapEditor([Sourcemap]);
const {
builders: {
@@ -103,6 +62,7 @@ const {
figureCaption,
heading,
hardBreak,
+ highlight,
horizontalRule,
image,
inlineDiff,
@@ -141,6 +101,7 @@ const {
hardBreak: { nodeType: HardBreak.name },
heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name },
+ highlight: { markType: Highlight.name },
image: { nodeType: Image.name },
inlineDiff: { markType: InlineDiff.name },
italic: { nodeType: Italic.name },
@@ -202,6 +163,12 @@ describe('markdownSerializer', () => {
).toBe('{++30 lines+}{--10 lines-}');
});
+ it('correctly serializes highlight', () => {
+ expect(serialize(paragraph('this is some ', highlight('highlighted'), ' text'))).toBe(
+ 'this is some <mark>highlighted</mark> text',
+ );
+ });
+
it('correctly serializes a line break', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 4ed1ed97cbd..0768fa6e8df 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -5,6 +5,45 @@ import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { builders, eq } from 'prosemirror-test-builder';
import { nextTick } from 'vue';
+import Audio from '~/content_editor/extensions/audio';
+import Blockquote from '~/content_editor/extensions/blockquote';
+import Bold from '~/content_editor/extensions/bold';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import Code from '~/content_editor/extensions/code';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import DescriptionItem from '~/content_editor/extensions/description_item';
+import DescriptionList from '~/content_editor/extensions/description_list';
+import Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
+import Emoji from '~/content_editor/extensions/emoji';
+import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
+import FootnoteReference from '~/content_editor/extensions/footnote_reference';
+import FootnotesSection from '~/content_editor/extensions/footnotes_section';
+import Frontmatter from '~/content_editor/extensions/frontmatter';
+import Figure from '~/content_editor/extensions/figure';
+import FigureCaption from '~/content_editor/extensions/figure_caption';
+import HardBreak from '~/content_editor/extensions/hard_break';
+import Heading from '~/content_editor/extensions/heading';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import Highlight from '~/content_editor/extensions/highlight';
+import Image from '~/content_editor/extensions/image';
+import InlineDiff from '~/content_editor/extensions/inline_diff';
+import Italic from '~/content_editor/extensions/italic';
+import Link from '~/content_editor/extensions/link';
+import ListItem from '~/content_editor/extensions/list_item';
+import OrderedList from '~/content_editor/extensions/ordered_list';
+import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
+import Strike from '~/content_editor/extensions/strike';
+import Table from '~/content_editor/extensions/table';
+import TableCell from '~/content_editor/extensions/table_cell';
+import TableHeader from '~/content_editor/extensions/table_header';
+import TableRow from '~/content_editor/extensions/table_row';
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
+import TaskItem from '~/content_editor/extensions/task_item';
+import TaskList from '~/content_editor/extensions/task_list';
+import Video from '~/content_editor/extensions/video';
+import HTMLMarks from '~/content_editor/extensions/html_marks';
+import HTMLNodes from '~/content_editor/extensions/html_nodes';
export const createDocBuilder = ({ tiptapEditor, names = {} }) => {
const docBuilders = builders(tiptapEditor.schema, {
@@ -162,3 +201,49 @@ export const waitUntilNextDocTransaction = ({ tiptapEditor, action = () => {} })
action();
});
};
+
+export const createTiptapEditor = (extensions = []) =>
+ createTestEditor({
+ extensions: [
+ Audio,
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ DescriptionItem,
+ DescriptionList,
+ Details,
+ DetailsContent,
+ Emoji,
+ FootnoteDefinition,
+ FootnoteReference,
+ FootnotesSection,
+ Frontmatter,
+ Figure,
+ FigureCaption,
+ HardBreak,
+ Heading,
+ HorizontalRule,
+ ...HTMLMarks,
+ ...HTMLNodes,
+ Highlight,
+ Image,
+ InlineDiff,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ ReferenceDefinition,
+ Strike,
+ Table,
+ TableCell,
+ TableHeader,
+ TableRow,
+ TableOfContents,
+ TaskItem,
+ TaskList,
+ Video,
+ ...extensions,
+ ],
+ });
diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
index 19e9ba8b268..990f18d64c1 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -59,6 +59,19 @@ describe('New Deploy Token', () => {
expect(checkbox.text()).toBe('read_registry');
});
+ function submitTokenThenCheck() {
+ wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
+
+ return waitForPromises()
+ .then(() => nextTick())
+ .then(() => {
+ const [tokenUsername, tokenValue] = wrapper.findAllComponents(GlFormInputGroup).wrappers;
+
+ expect(tokenUsername.props('value')).toBe('test token username');
+ expect(tokenValue.props('value')).toBe('test token');
+ });
+ }
+
it('should make a request to create a token on submit', () => {
const mockAxios = new MockAdapter(axios);
@@ -72,9 +85,18 @@ describe('New Deploy Token', () => {
const datepicker = wrapper.findAllComponents(GlDatepicker).at(0);
datepicker.vm.$emit('input', date);
- const [readRepo, readRegistry] = wrapper.findAllComponents(GlFormCheckbox).wrappers;
+ const [
+ readRepo,
+ readRegistry,
+ writeRegistry,
+ readPackageRegistry,
+ writePackageRegistry,
+ ] = wrapper.findAllComponents(GlFormCheckbox).wrappers;
readRepo.vm.$emit('input', true);
readRegistry.vm.$emit('input', true);
+ writeRegistry.vm.$emit('input', true);
+ readPackageRegistry.vm.$emit('input', true);
+ writePackageRegistry.vm.$emit('input', true);
mockAxios
.onPost(createNewTokenPath, {
@@ -84,20 +106,47 @@ describe('New Deploy Token', () => {
username: 'test username',
read_repository: true,
read_registry: true,
+ write_registry: true,
+ read_package_registry: true,
+ write_package_registry: true,
},
})
.replyOnce(200, { username: 'test token username', token: 'test token' });
- wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
+ return submitTokenThenCheck();
+ });
- return waitForPromises()
- .then(() => nextTick())
- .then(() => {
- const [tokenUsername, tokenValue] = wrapper.findAllComponents(GlFormInputGroup).wrappers;
+ it('should request a token without an expiration date', () => {
+ const mockAxios = new MockAdapter(axios);
- expect(tokenUsername.props('value')).toBe('test token username');
- expect(tokenValue.props('value')).toBe('test token');
- });
+ const formInputs = wrapper.findAllComponents(GlFormInput);
+ const name = formInputs.at(0);
+ const username = formInputs.at(2);
+ name.vm.$emit('input', 'test never expire name');
+ username.vm.$emit('input', 'test never expire username');
+
+ const [, , , readPackageRegistry, writePackageRegistry] = wrapper.findAllComponents(
+ GlFormCheckbox,
+ ).wrappers;
+ readPackageRegistry.vm.$emit('input', true);
+ writePackageRegistry.vm.$emit('input', true);
+
+ mockAxios
+ .onPost(createNewTokenPath, {
+ deploy_token: {
+ name: 'test never expire name',
+ expires_at: null,
+ username: 'test never expire username',
+ read_repository: false,
+ read_registry: false,
+ write_registry: false,
+ read_package_registry: true,
+ write_package_registry: true,
+ },
+ })
+ .replyOnce(200, { username: 'test token username', token: 'test token' });
+
+ return submitTokenThenCheck();
});
});
});
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 4a070395eaf..439c20e0fb5 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -193,16 +193,18 @@ describe('deprecatedJQueryDropdown', () => {
});
it('should not focus search input while remote task is not complete', () => {
- expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).not.toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
it('should focus search input after remote task is complete', () => {
remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
it('should focus on input when opening for the second time after transition', () => {
@@ -215,7 +217,8 @@ describe('deprecatedJQueryDropdown', () => {
test.dropdownButtonElement.click();
test.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
});
@@ -225,7 +228,8 @@ describe('deprecatedJQueryDropdown', () => {
test.dropdownButtonElement.click();
test.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ expect(document.activeElement).toBeDefined();
+ expect(document.activeElement).toEqual(document.querySelector(SEARCH_INPUT_SELECTOR));
});
});
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index 8cfe11c9040..ef1ed9bee51 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Design management index page designs renders error 1`] = `
<div
- class="gl-mt-5"
+ class="gl-mt-4"
data-testid="designs-root"
>
<!---->
@@ -34,7 +34,7 @@ exports[`Design management index page designs renders error 1`] = `
exports[`Design management index page designs renders loading icon 1`] = `
<div
- class="gl-mt-5"
+ class="gl-mt-4"
data-testid="designs-root"
>
<!---->
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index 8b25691ce34..a6f508c73eb 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -9,6 +9,18 @@ import {
const LINE_CODE = 'abc123';
+function problemsClone({
+ brokenSymlink = false,
+ brokenLineCode = false,
+ fileOnlyMoved = false,
+} = {}) {
+ return {
+ brokenSymlink,
+ brokenLineCode,
+ fileOnlyMoved,
+ };
+}
+
describe('isHighlighted', () => {
it('should return true if line is highlighted', () => {
const line = { line_code: LINE_CODE };
@@ -137,9 +149,12 @@ describe('classNameMapCell', () => {
describe('addCommentTooltip', () => {
const brokenSymLinkTooltip =
- 'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
+ 'Commenting on symbolic links that replace or are replaced by files is not supported';
const brokenRealTooltip =
- 'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
+ 'Commenting on files that replace or are replaced by symbolic links is not supported';
+ const lineMovedOrRenamedFileTooltip =
+ 'Commenting on files that are only moved or renamed is not supported';
+ const lineWithNoLineCodeTooltip = 'Commenting on this line is not supported';
const dragTooltip = 'Add a comment to this line or drag for multiple lines';
it('should return default tooltip', () => {
@@ -147,24 +162,38 @@ describe('addCommentTooltip', () => {
});
it('should return drag comment tooltip when dragging is enabled', () => {
- expect(utils.addCommentTooltip({})).toEqual(dragTooltip);
+ expect(utils.addCommentTooltip({ problems: problemsClone() })).toEqual(dragTooltip);
});
it('should return broken symlink tooltip', () => {
- expect(utils.addCommentTooltip({ commentsDisabled: { wasSymbolic: true } })).toEqual(
- brokenSymLinkTooltip,
- );
- expect(utils.addCommentTooltip({ commentsDisabled: { isSymbolic: true } })).toEqual(
- brokenSymLinkTooltip,
- );
+ expect(
+ utils.addCommentTooltip({
+ problems: problemsClone({ brokenSymlink: { wasSymbolic: true } }),
+ }),
+ ).toEqual(brokenSymLinkTooltip);
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isSymbolic: true } }) }),
+ ).toEqual(brokenSymLinkTooltip);
});
it('should return broken real tooltip', () => {
- expect(utils.addCommentTooltip({ commentsDisabled: { wasReal: true } })).toEqual(
- brokenRealTooltip,
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { wasReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ });
+
+ it('reports a tooltip when the line is in a file that has only been moved or renamed', () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone({ fileOnlyMoved: true }) })).toEqual(
+ lineMovedOrRenamedFileTooltip,
);
- expect(utils.addCommentTooltip({ commentsDisabled: { isReal: true } })).toEqual(
- brokenRealTooltip,
+ });
+
+ it("reports a tooltip when the line doesn't have a line code to leave a comment on", () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone({ brokenLineCode: true }) })).toEqual(
+ lineWithNoLineCodeTooltip,
);
});
});
@@ -211,6 +240,7 @@ describe('mapParallel', () => {
discussions: [{}],
discussionsExpanded: true,
hasForm: true,
+ problems: problemsClone(),
};
const content = {
diffFile: {},
diff --git a/spec/frontend/diffs/mock_data/diff_file.js b/spec/frontend/diffs/mock_data/diff_file.js
index dd200b0248c..e0e5778e0d5 100644
--- a/spec/frontend/diffs/mock_data/diff_file.js
+++ b/spec/frontend/diffs/mock_data/diff_file.js
@@ -1,3 +1,11 @@
+function problemsClone() {
+ return {
+ brokenSymlink: false,
+ brokenLineCode: false,
+ fileOnlyMoved: false,
+ };
+}
+
export const getDiffFileMock = () => ({
submodule: false,
submodule_link: null,
@@ -61,6 +69,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
@@ -71,6 +80,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
@@ -81,6 +91,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
@@ -91,6 +102,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
@@ -101,6 +113,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_6',
@@ -111,6 +124,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_7',
@@ -121,6 +135,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_9',
@@ -131,6 +146,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
{
line_code: null,
@@ -144,6 +160,7 @@ export const getDiffFileMock = () => ({
old_pos: 3,
new_pos: 5,
},
+ problems: problemsClone(),
},
],
parallel_diff_lines: [
@@ -158,6 +175,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -171,6 +189,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -183,6 +202,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
@@ -193,6 +213,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -205,6 +226,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
@@ -215,6 +237,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -227,6 +250,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
@@ -237,6 +261,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -249,6 +274,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC6" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
right: {
line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_7',
@@ -259,6 +285,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -272,6 +299,7 @@ export const getDiffFileMock = () => ({
text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
rich_text: '<span id="LC7" class="line" lang="plaintext"></span>\n',
meta_data: null,
+ problems: problemsClone(),
},
},
{
@@ -287,6 +315,7 @@ export const getDiffFileMock = () => ({
old_pos: 3,
new_pos: 5,
},
+ problems: problemsClone(),
},
right: {
line_code: null,
@@ -300,6 +329,7 @@ export const getDiffFileMock = () => ({
old_pos: 3,
new_pos: 5,
},
+ problems: problemsClone(),
},
},
],
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index bf75f956d7f..87366cdbfc5 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -183,11 +183,11 @@ describe('DiffsStoreActions', () => {
beforeEach(() => {
delete noFilesData.diff_files;
-
- mock.onGet(endpointMetadata).reply(200, diffMetadata);
});
it('should fetch diff meta information', () => {
+ mock.onGet(endpointMetadata).reply(200, diffMetadata);
+
return testAction(
diffActions.fetchDiffFilesMeta,
{},
@@ -206,6 +206,40 @@ describe('DiffsStoreActions', () => {
[],
);
});
+
+ it('should show a warning on 404 reponse', async () => {
+ mock.onGet(endpointMetadata).reply(404);
+
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.stringMatching(
+ 'Building your merge request. Wait a few moments, then refresh this page.',
+ ),
+ variant: 'warning',
+ });
+ });
+
+ it('should show no warning on any other status code', async () => {
+ mock.onGet(endpointMetadata).reply(500);
+
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
describe('fetchCoverageFiles', () => {
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 3f870a98396..b5c44b084d8 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -311,9 +311,14 @@ describe('DiffsStoreUtils', () => {
describe('prepareLineForRenamedFile', () => {
const diffFile = {
file_hash: 'file-hash',
+ brokenSymlink: false,
+ renamed_file: false,
+ added_lines: 1,
+ removed_lines: 1,
};
const lineIndex = 4;
const sourceLine = {
+ line_code: 'abc',
foo: 'test',
rich_text: ' <p>rich</p>', // Note the leading space
};
@@ -328,6 +333,12 @@ describe('DiffsStoreUtils', () => {
hasForm: false,
text: undefined,
alreadyPrepared: true,
+ commentsDisabled: false,
+ problems: {
+ brokenLineCode: false,
+ brokenSymlink: false,
+ fileOnlyMoved: false,
+ },
};
let preppedLine;
@@ -360,24 +371,35 @@ describe('DiffsStoreUtils', () => {
});
it.each`
- brokenSymlink
- ${false}
- ${{}}
- ${'anything except `false`'}
+ brokenSymlink | renamed | added | removed | lineCode | commentsDisabled
+ ${false} | ${false} | ${0} | ${0} | ${'a'} | ${false}
+ ${{}} | ${false} | ${1} | ${1} | ${'a'} | ${true}
+ ${'truthy'} | ${false} | ${1} | ${1} | ${'a'} | ${true}
+ ${false} | ${true} | ${1} | ${1} | ${'a'} | ${false}
+ ${false} | ${true} | ${1} | ${0} | ${'a'} | ${false}
+ ${false} | ${true} | ${0} | ${1} | ${'a'} | ${false}
+ ${false} | ${true} | ${0} | ${0} | ${'a'} | ${true}
`(
- "properly assigns each line's `commentsDisabled` as the same value as the parent file's `brokenSymlink` value (`$brokenSymlink`)",
- ({ brokenSymlink }) => {
- preppedLine = utils.prepareLineForRenamedFile({
- diffViewType: INLINE_DIFF_VIEW_TYPE,
- line: sourceLine,
+ "properly sets a line's `commentsDisabled` to '$commentsDisabled' for file and line settings { brokenSymlink: $brokenSymlink, renamed: $renamed, added: $added, removed: $removed, line_code: $lineCode }",
+ ({ brokenSymlink, renamed, added, removed, lineCode, commentsDisabled }) => {
+ const line = {
+ ...sourceLine,
+ line_code: lineCode,
+ };
+ const file = {
+ ...diffFile,
+ brokenSymlink,
+ renamed_file: renamed,
+ added_lines: added,
+ removed_lines: removed,
+ };
+ const preparedLine = utils.prepareLineForRenamedFile({
index: lineIndex,
- diffFile: {
- ...diffFile,
- brokenSymlink,
- },
+ diffFile: file,
+ line,
});
- expect(preppedLine.commentsDisabled).toStrictEqual(brokenSymlink);
+ expect(preparedLine.commentsDisabled).toBe(commentsDisabled);
},
);
});
@@ -477,7 +499,7 @@ describe('DiffsStoreUtils', () => {
it('adds the `.brokenSymlink` property to each diff file', () => {
preparedDiff.diff_files.forEach((file) => {
- expect(file).toEqual(expect.objectContaining({ brokenSymlink: false }));
+ expect(file).toHaveProperty('brokenSymlink', false);
});
});
@@ -490,7 +512,7 @@ describe('DiffsStoreUtils', () => {
].flatMap((file) => [...file[INLINE_DIFF_LINES_KEY]]);
lines.forEach((line) => {
- expect(line.commentsDisabled).toBe(false);
+ expect(line.problems.brokenSymlink).toBe(false);
});
});
});
diff --git a/spec/frontend/diffs/utils/tree_worker_utils_spec.js b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
index 8113428f712..4df5fe75004 100644
--- a/spec/frontend/diffs/utils/tree_worker_utils_spec.js
+++ b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
@@ -35,6 +35,23 @@ describe('~/diffs/utils/tree_worker_utils', () => {
file_hash: 'test',
},
{
+ new_path: 'constructor/test/aFile.js',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 42,
+ file_hash: 'test',
+ },
+ {
+ new_path: 'submodule @ abcdef123',
+ deleted_file: false,
+ new_file: true,
+ removed_lines: 0,
+ added_lines: 1,
+ submodule: true,
+ file_hash: 'test',
+ },
+ {
new_path: 'package.json',
deleted_file: true,
new_file: false,
@@ -66,6 +83,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
path: 'app/index.js',
removedLines: 10,
tempFile: false,
+ submodule: undefined,
type: 'blob',
tree: [],
},
@@ -87,6 +105,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
path: 'app/test/index.js',
removedLines: 0,
tempFile: true,
+ submodule: undefined,
type: 'blob',
tree: [],
},
@@ -101,6 +120,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
path: 'app/test/filepathneedstruncating.js',
removedLines: 0,
tempFile: true,
+ submodule: undefined,
type: 'blob',
tree: [],
},
@@ -110,6 +130,45 @@ describe('~/diffs/utils/tree_worker_utils', () => {
opened: true,
},
{
+ key: 'constructor',
+ name: 'constructor/test',
+ opened: true,
+ path: 'constructor',
+ tree: [
+ {
+ addedLines: 42,
+ changed: true,
+ deleted: false,
+ fileHash: 'test',
+ key: 'constructor/test/aFile.js',
+ name: 'aFile.js',
+ parentPath: 'constructor/test/',
+ path: 'constructor/test/aFile.js',
+ removedLines: 0,
+ submodule: undefined,
+ tempFile: true,
+ tree: [],
+ type: 'blob',
+ },
+ ],
+ type: 'tree',
+ },
+ {
+ key: 'submodule @ abcdef123',
+ parentPath: '/',
+ path: 'submodule @ abcdef123',
+ name: 'submodule @ abcdef123',
+ type: 'blob',
+ changed: true,
+ tempFile: true,
+ submodule: true,
+ deleted: false,
+ fileHash: 'test',
+ addedLines: 1,
+ removedLines: 0,
+ tree: [],
+ },
+ {
key: 'package.json',
parentPath: '/',
path: 'package.json',
@@ -117,6 +176,7 @@ describe('~/diffs/utils/tree_worker_utils', () => {
type: 'blob',
changed: true,
tempFile: false,
+ submodule: undefined,
deleted: true,
fileHash: 'test',
addedLines: 0,
@@ -135,6 +195,10 @@ describe('~/diffs/utils/tree_worker_utils', () => {
'app/test',
'app/test/index.js',
'app/test/filepathneedstruncating.js',
+ 'constructor',
+ 'constructor/test',
+ 'constructor/test/aFile.js',
+ 'submodule @ abcdef123',
'package.json',
]);
});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index fc86907c144..32126a5fd9a 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -18,9 +18,7 @@ import VariablesJson from './json_tests/positive_tests/variables.json';
import DefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/default_no_additional_properties.json';
import InheritDefaultNoAdditionalPropertiesJson from './json_tests/negative_tests/inherit_default_no_additional_properties.json';
import JobVariablesMustNotContainObjectsJson from './json_tests/negative_tests/job_variables_must_not_contain_objects.json';
-import ReleaseAssetsLinksEmptyJson from './json_tests/negative_tests/release_assets_links_empty.json';
-import ReleaseAssetsLinksInvalidLinkTypeJson from './json_tests/negative_tests/release_assets_links_invalid_link_type.json';
-import ReleaseAssetsLinksMissingJson from './json_tests/negative_tests/release_assets_links_missing.json';
+import ReleaseAssetsLinksJson from './json_tests/negative_tests/release_assets_links.json';
import RetryUnknownWhenJson from './json_tests/negative_tests/retry_unknown_when.json';
// YAML POSITIVE TEST
@@ -31,34 +29,22 @@ import IncludeYaml from './yaml_tests/positive_tests/include.yml';
import RulesYaml from './yaml_tests/positive_tests/rules.yml';
import ProjectPathYaml from './yaml_tests/positive_tests/project_path.yml';
import VariablesYaml from './yaml_tests/positive_tests/variables.yml';
+import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
-import CacheNegativeYaml from './yaml_tests/negative_tests/cache.yml';
+import CacheKeyNeative from './yaml_tests/negative_tests/cache.yml';
import IncludeNegativeYaml from './yaml_tests/negative_tests/include.yml';
-import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
-import VariablesNegativeYaml from './yaml_tests/negative_tests/variables.yml';
-
+import JobWhenNegativeYaml from './yaml_tests/negative_tests/job_when.yml';
import ProjectPathIncludeEmptyYaml from './yaml_tests/negative_tests/project_path/include/empty.yml';
import ProjectPathIncludeInvalidVariableYaml from './yaml_tests/negative_tests/project_path/include/invalid_variable.yml';
import ProjectPathIncludeLeadSlashYaml from './yaml_tests/negative_tests/project_path/include/leading_slash.yml';
import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/include/no_slash.yml';
import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml';
-import ProjectPathTriggerIncludeEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/include/empty.yml';
-import ProjectPathTriggerIncludeInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml';
-import ProjectPathTriggerIncludeLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml';
-import ProjectPathTriggerIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml';
-import ProjectPathTriggerIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml';
-import ProjectPathTriggerMinimalEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml';
-import ProjectPathTriggerMinimalInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml';
-import ProjectPathTriggerMinimalLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml';
-import ProjectPathTriggerMinimalNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml';
-import ProjectPathTriggerMinimalTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml';
-import ProjectPathTriggerProjectEmptyYaml from './yaml_tests/negative_tests/project_path/trigger/project/empty.yml';
-import ProjectPathTriggerProjectInvalidVariableYaml from './yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml';
-import ProjectPathTriggerProjectLeadSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml';
-import ProjectPathTriggerProjectNoSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml';
-import ProjectPathTriggerProjectTailSlashYaml from './yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml';
+import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
+import TriggerNegative from './yaml_tests/negative_tests/trigger.yml';
+import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variables/invalid_syntax_desc.yml';
+import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml';
const ajv = new Ajv({
strictTypes: false,
@@ -68,7 +54,7 @@ const ajv = new Ajv({
ajv.addKeyword('markdownDescription');
AjvFormats(ajv);
-const schema = ajv.compile(CiSchema);
+const ajvSchema = ajv.compile(CiSchema);
describe('positive tests', () => {
it.each(
@@ -90,12 +76,17 @@ describe('positive tests', () => {
CacheYaml,
FilterYaml,
IncludeYaml,
+ JobWhenYaml,
RulesYaml,
VariablesYaml,
ProjectPathYaml,
}),
)('schema validates %s', (_, input) => {
- expect(input).toValidateJsonSchema(schema);
+ // We construct a new "JSON" from each main key that is inside a
+ // file which allow us to make sure each blob is valid.
+ Object.keys(input).forEach((key) => {
+ expect({ [key]: input[key] }).toValidateJsonSchema(ajvSchema);
+ });
});
});
@@ -106,39 +97,29 @@ describe('negative tests', () => {
DefaultNoAdditionalPropertiesJson,
JobVariablesMustNotContainObjectsJson,
InheritDefaultNoAdditionalPropertiesJson,
- ReleaseAssetsLinksEmptyJson,
- ReleaseAssetsLinksInvalidLinkTypeJson,
- ReleaseAssetsLinksMissingJson,
+ ReleaseAssetsLinksJson,
RetryUnknownWhenJson,
// YAML
ArtifactsNegativeYaml,
- CacheNegativeYaml,
+ CacheKeyNeative,
IncludeNegativeYaml,
+ JobWhenNegativeYaml,
RulesNegativeYaml,
- VariablesNegativeYaml,
+ VariablesInvalidSyntaxDescYaml,
+ VariablesWrongSyntaxUsageExpand,
ProjectPathIncludeEmptyYaml,
ProjectPathIncludeInvalidVariableYaml,
ProjectPathIncludeLeadSlashYaml,
ProjectPathIncludeNoSlashYaml,
ProjectPathIncludeTailSlashYaml,
- ProjectPathTriggerIncludeEmptyYaml,
- ProjectPathTriggerIncludeInvalidVariableYaml,
- ProjectPathTriggerIncludeLeadSlashYaml,
- ProjectPathTriggerIncludeNoSlashYaml,
- ProjectPathTriggerIncludeTailSlashYaml,
- ProjectPathTriggerMinimalEmptyYaml,
- ProjectPathTriggerMinimalInvalidVariableYaml,
- ProjectPathTriggerMinimalLeadSlashYaml,
- ProjectPathTriggerMinimalNoSlashYaml,
- ProjectPathTriggerMinimalTailSlashYaml,
- ProjectPathTriggerProjectEmptyYaml,
- ProjectPathTriggerProjectInvalidVariableYaml,
- ProjectPathTriggerProjectLeadSlashYaml,
- ProjectPathTriggerProjectNoSlashYaml,
- ProjectPathTriggerProjectTailSlashYaml,
+ TriggerNegative,
}),
)('schema validates %s', (_, input) => {
- expect(input).not.toValidateJsonSchema(schema);
+ // We construct a new "JSON" from each main key that is inside a
+ // file which allow us to make sure each blob is invalid.
+ Object.keys(input).forEach((key) => {
+ expect({ [key]: input[key] }).not.toValidateJsonSchema(ajvSchema);
+ });
});
});
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
index 955c19ef1ab..d30bc4649ab 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/default_no_additional_properties.json
@@ -9,4 +9,4 @@
"name": "test"
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
index 7411e4c2434..1a31467f9ae 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/inherit_default_no_additional_properties.json
@@ -1,8 +1,10 @@
{
"karma": {
"inherit": {
- "default": ["secrets"]
+ "default": [
+ "secrets"
+ ]
},
"script": "karma"
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
index bfdbf26ee70..68dd57824ab 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/job_variables_must_not_contain_objects.json
@@ -1,7 +1,9 @@
{
"gitlab-ci-variables-object": {
"stage": "test",
- "script": ["true"],
+ "script": [
+ "true"
+ ],
"variables": {
"DEPLOY_ENVIRONMENT": {
"value": "staging",
@@ -9,4 +11,4 @@
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links.json
index 048911aefa3..00b5b54c7e2 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_invalid_link_type.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links.json
@@ -1,4 +1,24 @@
{
+ "gitlab-ci-release-assets-links-missing": {
+ "script": "dostuff",
+ "stage": "deploy",
+ "release": {
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "assets": {}
+ }
+ },
+ "gitlab-ci-release-assets-links-empty": {
+ "script": "dostuff",
+ "stage": "deploy",
+ "release": {
+ "description": "Created using the release-cli $EXTRA_DESCRIPTION",
+ "tag_name": "$CI_COMMIT_TAG",
+ "assets": {
+ "links": []
+ }
+ }
+ },
"gitlab-ci-release-assets-links-invalid-link-type": {
"script": "dostuff",
"stage": "deploy",
@@ -21,4 +41,4 @@
}
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
deleted file mode 100644
index 84a1aa14698..00000000000
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_empty.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "gitlab-ci-release-assets-links-empty": {
- "script": "dostuff",
- "stage": "deploy",
- "release": {
- "description": "Created using the release-cli $EXTRA_DESCRIPTION",
- "tag_name": "$CI_COMMIT_TAG",
- "assets": {
- "links": []
- }
- }
- }
-}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
deleted file mode 100644
index 6f0b5a3bff8..00000000000
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/release_assets_links_missing.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "gitlab-ci-release-assets-links-missing": {
- "script": "dostuff",
- "stage": "deploy",
- "release": {
- "description": "Created using the release-cli $EXTRA_DESCRIPTION",
- "tag_name": "$CI_COMMIT_TAG",
- "assets": {}
- }
- }
-}
diff --git a/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
index 433504f52c6..2c53ce07109 100644
--- a/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
+++ b/spec/frontend/editor/schema/ci/json_tests/negative_tests/retry_unknown_when.json
@@ -6,4 +6,4 @@
"when": "gitlab-ci-retry-object-unknown-when"
}
}
-}
+} \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
index 04020c06753..3979c9ae2ac 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml
@@ -1,13 +1,62 @@
-stages:
- - prepare
+cache-key-files-not-an-array:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files: package.json
+ paths:
+ - vendor/ruby
+ - node_modules
+
+cache-key-prefix-array:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix:
+ - binaries-cache-$CI_JOB_NAME
+ paths:
+ - binaries/
+
+cache-key-with-.:
+ script: echo "This job uses a cache."
+ cache:
+ key: .
+ paths:
+ - binaries/
+
+cache-key-with-multiple-.:
+ stage: test
+ script: echo "This job uses a cache."
+ cache:
+ key: ..
+ paths:
+ - binaries/
+
+cache-key-with-/:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-ca/che
+ paths:
+ - binaries/
+
+cache-path-not-an-array:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-cache
+ paths: binaries/*.apk
+
+cache-untracked-string:
+ script: echo "This job uses a cache."
+ cache:
+ untracked: 'true'
-# invalid cache:when values
-when no integer:
- stage: prepare
+when_integer:
+ script: echo "This job uses a cache."
cache:
when: 0
-when must be a reserved word:
- stage: prepare
+when_not_reserved_keyword:
+ script: echo "This job uses a cache."
cache:
when: 'never'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
index 1e16bb55405..6afd8baa0e8 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/include.yml
@@ -1,6 +1,3 @@
-stages:
- - prepare
-
# invalid trigger:include
trigger missing file property:
stage: prepare
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml
new file mode 100644
index 00000000000..d4e3911ff60
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/job_when.yml
@@ -0,0 +1,11 @@
+job_with_wrong_when:
+ script: exit 0
+ when: on_xyz
+
+job_with_boolean_when:
+ script: exit 0
+ when: true
+
+job_with_array_when:
+ script: exit 0
+ when: [on_success]
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml
deleted file mode 100644
index ee2bb3e8ace..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/empty.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml
deleted file mode 100644
index 770305be0dc..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/invalid_variable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml
deleted file mode 100644
index 82fd77cf0d3..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/leading_slash.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml
deleted file mode 100644
index f4ea59c7945..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/no_slash.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml
deleted file mode 100644
index a0195c03352..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/include/tailing_slash.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-trigger-include:
- trigger:
- include:
- - file: '/path/to/child-pipeline.yml'
- project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml
deleted file mode 100644
index cad8dbbf430..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/empty.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml
deleted file mode 100644
index 6ca37666d09..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/invalid_variable.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml
deleted file mode 100644
index 9d7c6b44125..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/leading_slash.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml
deleted file mode 100644
index acd047477c8..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/no_slash.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml
deleted file mode 100644
index 0fdd00da3de..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/minimal/tailing_slash.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-trigger-minimal:
- trigger: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml
deleted file mode 100644
index 0aa2330cecb..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/empty.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: ''
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml
deleted file mode 100644
index 3c17ec62039..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/invalid_variable.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: 'slug#'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml
deleted file mode 100644
index f9884603171..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/leading_slash.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: '/slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml
deleted file mode 100644
index d89e09756eb..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/no_slash.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: 'slug'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml
deleted file mode 100644
index 3c39d6be4cb..00000000000
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/project_path/trigger/project/tailing_slash.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-trigger-project:
- trigger:
- project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml
new file mode 100644
index 00000000000..73cc82f2f1c
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/trigger.yml
@@ -0,0 +1,64 @@
+trigger-minimal-empty:
+ trigger: ''
+
+trigger-minimal-invalid-variable:
+ trigger: 'slug#'
+
+trigger-minimal-leading-slash:
+ trigger: '/slug'
+
+trigger-minimal-no-slash:
+ trigger: 'slug'
+
+trigger-minimal-trailing-slash:
+ trigger: 'slug/'
+
+trigger-project-empty:
+ trigger:
+ project: ''
+
+trigger-project-invalid-variable:
+ trigger:
+ project: 'slug#'
+
+trigger-project-leading-slash:
+ trigger:
+ project: '/slug'
+
+trigger-project-no-slash:
+ trigger:
+ project: 'slug'
+
+trigger-project-trailing-slash:
+ trigger:
+ project: 'slug/'
+
+trigger-include-empty:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: ''
+
+trigger-include-invalid:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug#'
+
+trigger-include-leading-slash:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: '/slug'
+
+trigger-include-no-slash:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug'
+
+trigger-include-trailing-slash:
+ trigger:
+ include:
+ - file: '/path/to/child-pipeline.yml'
+ project: 'slug/'
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_syntax_desc.yml
index a7f23cf0d73..4916a6b354e 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/invalid_syntax_desc.yml
@@ -1,4 +1,3 @@
-# invalid variable (unknown keyword is used)
variables:
FOO:
value: BAR
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml
new file mode 100644
index 00000000000..62bebfa57e7
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/variables/wrong_syntax_usage_expand.yml
@@ -0,0 +1,4 @@
+variables:
+ RAW_VAR:
+ value: Hello $FOO
+ expand: okay
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
index d83e14fdc6a..75918cd2a1b 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml
@@ -1,24 +1,124 @@
-stages:
- - prepare
-
# valid cache:when values
job1:
- stage: prepare
script:
- echo 'running job'
cache:
when: 'on_success'
job2:
- stage: prepare
script:
- echo 'running job'
cache:
when: 'on_failure'
job3:
- stage: prepare
script:
- echo 'running job'
cache:
when: 'always'
+
+# valid cache:paths
+cache-paths:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-cache
+ paths:
+ - binaries/*.apk
+ - .config
+
+# valid cache:key
+cache-key-string:
+ script: echo "This job uses a cache."
+ cache:
+ key: random-string
+ paths:
+ - binaries/
+
+cache-key-string-with-dots:
+ script: echo "This job uses a cache."
+ cache:
+ key: random-..string
+ paths:
+ - binaries/
+
+cache-key-string-beginning-with-dot:
+ script: echo "This job uses a cache."
+ cache:
+ key: .random-string
+ paths:
+ - binaries/
+
+cache-key-string-ending-with-dot:
+ script: echo "This job uses a cache."
+ cache:
+ key: random-string.
+ paths:
+ - binaries/
+
+cache-key-predefined-variable:
+ script: echo "This job uses a cache."
+ cache:
+ key: $CI_COMMIT_REF_SLUG
+ paths:
+ - binaries/
+
+cache-key-combination:
+ script: echo "This job uses a cache."
+ cache:
+ key: binaries-cache-$CI_COMMIT_REF_SLUG
+ paths:
+ - binaries/
+
+# valid cache:key:files
+cache-key-files:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ - package.json
+ paths:
+ - vendor/ruby
+ - node_modules
+
+# valide cache:key:prefix
+cache-key-prefix-string:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: random-string
+ paths:
+ - binaries/
+
+cache-key-prefix-predefined-variable:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: $CI_JOB_NAME
+ paths:
+ - binaries/
+
+cache-key-prefix-combination:
+ script: echo "This job uses a cache."
+ cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: binaries-cache-$CI_JOB_NAME
+ paths:
+ - binaries/
+
+# valid cache:untracked
+cache-untracked-true:
+ script: test
+ cache:
+ untracked: true
+
+cache-untracked-false:
+ script: test
+ cache:
+ untracked: false
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml
new file mode 100644
index 00000000000..2a684a78f4e
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/job_when.yml
@@ -0,0 +1,10 @@
+job_with_no_when:
+ script: exit 0
+
+job_with_when_always:
+ script: exit 0
+ when: always
+
+job_with_when_on_failure:
+ script: exit 0
+ when: on_failure
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
index ee71087a72e..53d020c432f 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/variables.yml
@@ -6,3 +6,13 @@ variables:
description: "A single value variable"
DEPLOY_ENVIRONMENT:
description: "A multi-value variable"
+ RAW_VAR:
+ value: "Hello $FOO"
+ expand: false
+
+rspec:
+ script: rspec
+ variables:
+ RAW_VAR2:
+ value: "Hello $DEPLOY_ENVIRONMENT"
+ expand: false \ No newline at end of file
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index 1ff351b6554..19ebe0e3cb7 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -81,9 +81,18 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
},
path: previewMarkdownPath,
actionShowPreviewCondition: expect.any(Object),
+ eventEmitter: expect.any(Object),
});
});
+ it('support external preview trigger via emitter event', () => {
+ expect(panelSpy).not.toHaveBeenCalled();
+
+ instance.markdownPreview.eventEmitter.fire();
+
+ expect(panelSpy).toHaveBeenCalled();
+ });
+
describe('onDidLayoutChange', () => {
const emitter = new Emitter();
let layoutSpy;
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 68895b194a1..48483152f7a 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -10,11 +10,7 @@ import actionMutation from '~/environments/graphql/mutations/action.mutation.gra
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import createMockApollo from 'helpers/mock_apollo_helper';
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
const scheduledJobAction = {
name: 'scheduled action',
diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index be61c6fcc90..5d36209f8a6 100644
--- a/spec/frontend/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -76,7 +76,7 @@ describe('Rollback Component', () => {
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: setEnvironmentToRollback,
- variables: { environment },
+ variables: { environment: { ...environment, isLastDeployment: true, retryUrl } },
});
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index d246641b94b..355b77b55c3 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -537,6 +537,7 @@ export const folder = {
export const resolvedEnvironment = {
id: 41,
+ retryUrl: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
globalId: 'gid://gitlab/Environment/41',
name: 'review/hello',
state: 'available',
diff --git a/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
index 212b9ffc8f9..c958f669f9a 100644
--- a/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
+++ b/spec/frontend/filtered_search/droplab/plugins/ajax_filter_spec.js
@@ -10,6 +10,9 @@ describe('AjaxFilter', () => {
dummyConfig = {
endpoint: 'dummy endpoint',
searchKey: 'dummy search key',
+ searchValueFunction() {
+ return 'test';
+ },
};
dummyList = {
data: [],
@@ -40,7 +43,7 @@ describe('AjaxFilter', () => {
it('calls onLoadingFinished after loading data', async () => {
ajaxSpy = (url) => {
- expect(url).toBe('dummy endpoint?dummy search key=');
+ expect(url).toBe('dummy endpoint?dummy%20search%20key=test');
return Promise.resolve(dummyData);
};
@@ -51,7 +54,7 @@ describe('AjaxFilter', () => {
it('does not call onLoadingFinished if Ajax call fails', async () => {
const dummyError = new Error('My dummy is sick! :-(');
ajaxSpy = (url) => {
- expect(url).toBe('dummy endpoint?dummy search key=');
+ expect(url).toBe('dummy endpoint?dummy%20search%20key=test');
return Promise.reject(dummyError);
};
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
index 7d95c506e6c..fae1f4056fb 100644
--- a/spec/frontend/fixtures/api_merge_requests.rb
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -7,7 +7,7 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let_it_be(:admin) { create(:admin, name: 'root') }
- let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' ) }
+ let_it_be(:namespace) { create(:namespace, name: 'gitlab-test') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let_it_be(:early_mrs) do
4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index 5acc1095d5c..b14f402a7b9 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -7,7 +7,7 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin, name: 'root') }
- let(:namespace) { create(:namespace, name: 'gitlab-test' ) }
+ let(:namespace) { create(:namespace, name: 'gitlab-test') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb
index b3ce23c8cd7..34e99ec647c 100644
--- a/spec/frontend/fixtures/application_settings.rb
+++ b/spec/frontend/fixtures/application_settings.rb
@@ -8,7 +8,7 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty
include AdminModeHelper
let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') }
before do
diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb
index 54c5b83da3e..b7b75247a59 100644
--- a/spec/frontend/fixtures/blob.rb
+++ b/spec/frontend/fixtures/blob.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index 6cda2f0f665..25626ed8c76 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Branches (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let_it_be(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb
index 426a76f29e0..ff15cfb62c3 100644
--- a/spec/frontend/fixtures/clusters.rb
+++ b/spec/frontend/fixtures/clusters.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace) }
let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb
index 24d602216d8..05fca368fd5 100644
--- a/spec/frontend/fixtures/deploy_keys.rb
+++ b/spec/frontend/fixtures/deploy_keys.rb
@@ -7,7 +7,7 @@ RSpec.describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :c
include AdminModeHelper
let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:project2) { create(:project, :internal) }
let(:project3) { create(:project, :internal) }
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
index dd16bd81b51..5aa466ef015 100644
--- a/spec/frontend/fixtures/freeze_period.rb
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
around do |example|
freeze_time do
# Mock time to sept 19 (intl. talk like a pirate day)
- Timecop.travel(2020, 9, 19)
+ travel_to(Time.utc(2020, 9, 19))
example.run
end
diff --git a/spec/frontend/fixtures/integrations.rb b/spec/frontend/fixtures/integrations.rb
index 45d1c400f5d..c26b9524324 100644
--- a/spec/frontend/fixtures/integrations.rb
+++ b/spec/frontend/fixtures/integrations.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') }
let!(:service) { create(:custom_issue_tracker_integration, project: project) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index e3d88098841..bc5ece20032 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
include JavaScriptFixturesHelpers
let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
render_views
diff --git a/spec/frontend/fixtures/job_artifacts.rb b/spec/frontend/fixtures/job_artifacts.rb
new file mode 100644
index 00000000000..e53cdbbaaa5
--- /dev/null
+++ b/spec/frontend/fixtures/job_artifacts.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Job Artifacts (GraphQL fixtures)' do
+ describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ job_artifacts_query_path = 'artifacts/graphql/queries/get_job_artifacts.query.graphql'
+
+ it "graphql/#{job_artifacts_query_path}.json" do
+ create(:ci_build, :failed, :artifacts, :trace_artifact, pipeline: pipeline)
+ create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline)
+
+ query = get_graphql_query_as_string(job_artifacts_query_path)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 3657a5405a4..ac58b99875b 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include GraphqlHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') }
let(:user) { project.first_owner }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb
index 2445c9376e2..9b8d073e74c 100644
--- a/spec/frontend/fixtures/labels.rb
+++ b/spec/frontend/fixtures/labels.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Labels (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
let(:user) { create(:user) }
- let(:group) { create(:group, name: 'frontend-fixtures-group' ) }
+ let(:group) { create(:group, name: 'frontend-fixtures-group') }
let(:project) { create(:project_empty_repo, namespace: group, path: 'labels-project') }
let!(:project_label_bug) { create(:label, project: project, title: 'bug', color: '#FF0000') }
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index cbf26a70e5f..18f89fbc5e5 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
let(:user) { project.first_owner }
@@ -147,6 +147,20 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
expect_graphql_errors_to_be_empty
end
end
+
+ context 'merge request in state getState query' do
+ base_input_path = 'vue_merge_request_widget/queries/'
+ base_output_path = 'graphql/merge_requests/'
+ query_name = 'get_state.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
private
diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb
index ff4b27844a6..cd22d110e38 100644
--- a/spec/frontend/fixtures/merge_requests_diffs.rb
+++ b/spec/frontend/fixtures/merge_requests_diffs.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
let(:user) { project.first_owner }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project, description: '- [ ] Task List Item') }
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
index 7f8b3d378d3..109b016d980 100644
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ b/spec/frontend/fixtures/metrics_dashboard.rb
@@ -7,7 +7,7 @@ RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
include MetricsDashboardHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:namespace) { create(:namespace, name: 'monitoring' ) }
+ let_it_be(:namespace) { create(:namespace, name: 'monitoring') }
let_it_be(:project) { project_with_dashboard_namespace('.gitlab/dashboards/test.yml', nil, namespace: namespace) }
let_it_be(:environment) { create(:environment, id: 1, project: project) }
let_it_be(:params) { { environment: environment } }
diff --git a/spec/frontend/fixtures/namespaces.rb b/spec/frontend/fixtures/namespaces.rb
index a3f295f4e66..9858e3241cb 100644
--- a/spec/frontend/fixtures/namespaces.rb
+++ b/spec/frontend/fixtures/namespaces.rb
@@ -32,6 +32,26 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
end
end
+ describe API::Groups, type: :request do
+ let_it_be(:user) { create(:user) }
+
+ describe 'transfer_locations' do
+ let_it_be(:groups) { create_list(:group, 4) }
+ let_it_be(:transfer_from_group) { create(:group) }
+
+ before_all do
+ groups.each { |group| group.add_owner(user) }
+ transfer_from_group.add_owner(user)
+ end
+
+ it 'api/groups/transfer_locations.json' do
+ get api("/groups/#{transfer_from_group.id}/transfer_locations", user)
+
+ expect(response).to be_successful
+ end
+ end
+ end
+
describe GraphQL::Query, type: :request do
let_it_be(:user) { create(:user) }
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 4de0bd762f8..3bfe9113e83 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include GraphqlHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
@@ -54,7 +54,7 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
get_pipeline_schedules_query = 'get_pipeline_schedules.query.graphql'
let_it_be(:query) do
- get_graphql_query_as_string("pipeline_schedules/graphql/queries/#{get_pipeline_schedules_query}")
+ get_graphql_query_as_string("ci/pipeline_schedules/graphql/queries/#{get_pipeline_schedules_query}")
end
it "#{fixtures_path}#{get_pipeline_schedules_query}.json" do
@@ -71,5 +71,14 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
+
+ it "#{fixtures_path}#{get_pipeline_schedules_query}.take_ownership.json" do
+ maintainer = create(:user)
+ project.add_maintainer(maintainer)
+
+ post_graphql(query, current_user: maintainer, variables: { projectPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
end
end
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 114db26d6a9..44b471a70d8 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') }
let_it_be(:commit_without_author) { RepoHelpers.another_sample_commit }
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index b9c427c7505..101ba203a57 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
runners_token = 'runnerstoken:intabulasreferre'
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) }
diff --git a/spec/frontend/fixtures/prometheus_integration.rb b/spec/frontend/fixtures/prometheus_integration.rb
index 250c50bc8bb..13130c00118 100644
--- a/spec/frontend/fixtures/prometheus_integration.rb
+++ b/spec/frontend/fixtures/prometheus_integration.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'integrations-project') }
let!(:integration) { create(:prometheus_integration, project: project) }
let(:user) { project.first_owner }
diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb
index 7bd5b8c5f6c..886f5525ac5 100644
--- a/spec/frontend/fixtures/raw.rb
+++ b/spec/frontend/fixtures/raw.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Raw files', '(JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') }
let(:response) { @response }
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index b523650dda5..de87114766e 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -20,8 +20,8 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:build) { create(:ci_build, runner: runner) }
- query_path = 'runner/graphql/'
- fixtures_path = 'graphql/runner/'
+ query_path = 'ci/runner/graphql/'
+ fixtures_path = 'graphql/ci/runner/'
after(:all) do
remove_repository(project)
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 58d4bc5c1f3..0510746a944 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:user) { project.first_owner }
let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) }
diff --git a/spec/frontend/fixtures/static/gl_field_errors.html b/spec/frontend/fixtures/static/gl_field_errors.html
index f8470e02b7c..a53366fc29f 100644
--- a/spec/frontend/fixtures/static/gl_field_errors.html
+++ b/spec/frontend/fixtures/static/gl_field_errors.html
@@ -17,6 +17,9 @@
<div class="form-group">
<input class="custom gl-field-error-ignore" type="text">Custom, do not validate</input>
</div>
+<div class="form-group">
+<textarea required title="Textarea is required">Textarea</textarea>
+</div>
<div class="form-group"></div>
<input class="submit" type="submit">Submit</input>
</form>
diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb
index d934396f803..58f230de546 100644
--- a/spec/frontend/fixtures/todos.rb
+++ b/spec/frontend/fixtures/todos.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Todos (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:user) { project.first_owner }
let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index a809bf248bf..a105b0b165c 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -193,6 +193,20 @@ describe('Flash', () => {
);
});
+ describe('with title', () => {
+ const mockTitle = 'my title';
+
+ it('shows title and message', () => {
+ createAlert({
+ title: mockTitle,
+ message: mockMessage,
+ });
+
+ const text = document.querySelector('.flash-container').textContent.trim();
+ expect(text).toBe(`${mockTitle} ${mockMessage}`);
+ });
+ });
+
describe('with buttons', () => {
const findAlertAction = () => document.querySelector('.flash-container .gl-alert-action');
diff --git a/spec/frontend/gfm_auto_complete/mock_data.js b/spec/frontend/gfm_auto_complete/mock_data.js
index 86795ffd0a5..9c5a9d7ef3d 100644
--- a/spec/frontend/gfm_auto_complete/mock_data.js
+++ b/spec/frontend/gfm_auto_complete/mock_data.js
@@ -32,3 +32,60 @@ export const eventlistenersMockDefaultMap = [
namespace: 'atwho',
},
];
+
+export const crmContactsMock = [
+ {
+ id: 1,
+ email: 'contact.1@email.com',
+ firstName: 'Contact',
+ lastName: 'One',
+ search: 'contact.1@email.com',
+ state: 'active',
+ set: false,
+ },
+ {
+ id: 2,
+ email: 'contact.2@email.com',
+ firstName: 'Contact',
+ lastName: 'Two',
+ search: 'contact.2@email.com',
+ state: 'active',
+ set: false,
+ },
+ {
+ id: 3,
+ email: 'contact.3@email.com',
+ firstName: 'Contact',
+ lastName: 'Three',
+ search: 'contact.3@email.com',
+ state: 'inactive',
+ set: false,
+ },
+ {
+ id: 4,
+ email: 'contact.4@email.com',
+ firstName: 'Contact',
+ lastName: 'Four',
+ search: 'contact.4@email.com',
+ state: 'inactive',
+ set: true,
+ },
+ {
+ id: 5,
+ email: 'contact.5@email.com',
+ firstName: 'Contact',
+ lastName: 'Five',
+ search: 'contact.5@email.com',
+ state: 'active',
+ set: true,
+ },
+ {
+ id: 5,
+ email: 'contact.6@email.com',
+ firstName: 'Contact',
+ lastName: 'Six',
+ search: 'contact.6@email.com',
+ state: 'active',
+ set: undefined, // On purpose
+ },
+];
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index c3dfc4570f9..68225f39c66 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -3,14 +3,23 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete';
+import GfmAutoComplete, {
+ membersBeforeSave,
+ highlighter,
+ CONTACT_STATE_ACTIVE,
+ CONTACTS_ADD_COMMAND,
+ CONTACTS_REMOVE_COMMAND,
+} from 'ee_else_ce/gfm_auto_complete';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
-import { eventlistenersMockDefaultMap } from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
+import {
+ eventlistenersMockDefaultMap,
+ crmContactsMock,
+} from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
describe('GfmAutoComplete', () => {
const fetchDataMock = { fetchData: jest.fn() };
@@ -871,7 +880,87 @@ describe('GfmAutoComplete', () => {
});
});
- describe('Contacts', () => {
+ describe('CRM Contacts', () => {
+ const dataSources = {
+ contacts: `${TEST_HOST}/autocomplete_sources/contacts`,
+ };
+
+ const allContacts = crmContactsMock;
+ const assignedContacts = allContacts.filter((contact) => contact.set);
+ const unassignedContacts = allContacts.filter(
+ (contact) => contact.state === CONTACT_STATE_ACTIVE && !contact.set,
+ );
+
+ let autocomplete;
+ let $textarea;
+
+ beforeEach(() => {
+ setHTMLFixture('<textarea></textarea>');
+ autocomplete = new GfmAutoComplete(dataSources);
+ $textarea = $('textarea');
+ autocomplete.setup($textarea, { contacts: true });
+ });
+
+ afterEach(() => {
+ autocomplete.destroy();
+ resetHTMLFixture();
+ });
+
+ const triggerDropdown = (text) => {
+ $textarea.trigger('focus').val(text).caret('pos', -1);
+ $textarea.trigger('keyup');
+
+ jest.runOnlyPendingTimers();
+ };
+
+ const getDropdownItems = () => {
+ const dropdown = document.getElementById('at-view-contacts');
+ const items = dropdown.getElementsByTagName('li');
+ return [].map.call(items, (item) => item.textContent.trim());
+ };
+
+ const expectContacts = ({ input, output }) => {
+ triggerDropdown(input);
+
+ expect(getDropdownItems()).toEqual(output.map((contact) => contact.email));
+ };
+
+ describe('with no contacts assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['[contact:'] = [...unassignedContacts];
+ });
+
+ it.each`
+ input | output
+ ${`${CONTACTS_ADD_COMMAND} [contact:`} | ${unassignedContacts}
+ ${`${CONTACTS_REMOVE_COMMAND} [contact:`} | ${[]}
+ `('$input shows $output.length contacts', expectContacts);
+ });
+
+ describe('with some contacts assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['[contact:'] = allContacts;
+ });
+
+ it.each`
+ input | output
+ ${`${CONTACTS_ADD_COMMAND} [contact:`} | ${unassignedContacts}
+ ${`${CONTACTS_REMOVE_COMMAND} [contact:`} | ${assignedContacts}
+ `('$input shows $output.length contacts', expectContacts);
+ });
+
+ describe('with all contacts assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['[contact:'] = [...assignedContacts];
+ });
+
+ it.each`
+ input | output
+ ${`${CONTACTS_ADD_COMMAND} [contact:`} | ${[]}
+ ${`${CONTACTS_REMOVE_COMMAND} [contact:`} | ${assignedContacts}
+ `('$input shows $output.length contacts', expectContacts);
+ });
+
it('escapes name and email correct', () => {
const xssPayload = '<script>alert(1)</script>';
const escapedPayload = '&lt;script&gt;alert(1)&lt;/script&gt;';
diff --git a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
new file mode 100644
index 00000000000..949bcf71ff5
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
@@ -0,0 +1,102 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import GitlabVersionCheckBadge from '~/gitlab_version_check/components/gitlab_version_check_badge.vue';
+import { STATUS_TYPES, UPGRADE_DOCS_URL } from '~/gitlab_version_check/constants';
+
+describe('GitlabVersionCheckBadge', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const defaultProps = {
+ status: STATUS_TYPES.SUCCESS,
+ };
+
+ const createComponent = (props = {}) => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+
+ wrapper = shallowMountExtended(GitlabVersionCheckBadge, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ unmockTracking();
+ wrapper.destroy();
+ });
+
+ const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+
+ describe('template', () => {
+ describe.each`
+ status | expectedUI
+ ${STATUS_TYPES.SUCCESS} | ${{ title: 'Up to date', variant: 'success' }}
+ ${STATUS_TYPES.WARNING} | ${{ title: 'Update available', variant: 'warning' }}
+ ${STATUS_TYPES.DANGER} | ${{ title: 'Update ASAP', variant: 'danger' }}
+ `('badge ui', ({ status, expectedUI }) => {
+ beforeEach(() => {
+ createComponent({ status, actionable: true });
+ });
+
+ describe(`when status is ${status}`, () => {
+ it(`title is ${expectedUI.title}`, () => {
+ expect(findGlBadge().text()).toBe(expectedUI.title);
+ });
+
+ it(`variant is ${expectedUI.variant}`, () => {
+ expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
+ });
+
+ it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
+ label: 'version_badge',
+ property: expectedUI.title,
+ });
+ });
+
+ it(`link is ${UPGRADE_DOCS_URL}`, () => {
+ expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
+ });
+
+ it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
+ await findGlBadgeClickWrapper().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'version_badge',
+ property: expectedUI.title,
+ });
+ });
+ });
+ });
+
+ describe('when actionable is false', () => {
+ beforeEach(() => {
+ createComponent({ actionable: false });
+ });
+
+ it('tracks rendered_version_badge correctly', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'render', {
+ label: 'version_badge',
+ property: 'Up to date',
+ });
+ });
+
+ it('does not provide a link to GlBadge', () => {
+ expect(findGlBadge().attributes('href')).toBe(undefined);
+ });
+
+ it('does not track click_version_badge', async () => {
+ await findGlBadgeClickWrapper().trigger('click');
+
+ expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'version_badge',
+ property: 'Up to date',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gitlab_version_check/index_spec.js b/spec/frontend/gitlab_version_check/index_spec.js
new file mode 100644
index 00000000000..8a11ff48bf2
--- /dev/null
+++ b/spec/frontend/gitlab_version_check/index_spec.js
@@ -0,0 +1,116 @@
+import Vue from 'vue';
+import * as Sentry from '@sentry/browser';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import initGitlabVersionCheck from '~/gitlab_version_check';
+
+describe('initGitlabVersionCheck', () => {
+ let originalGon;
+ let mock;
+ let vueApps;
+
+ const defaultResponse = {
+ code: 200,
+ res: { severity: 'success' },
+ };
+
+ const dummyGon = {
+ relative_url_root: '/',
+ };
+
+ const createApp = async (mockResponse, htmlClass) => {
+ originalGon = window.gon;
+
+ const response = {
+ ...defaultResponse,
+ ...mockResponse,
+ };
+
+ mock = new MockAdapter(axios);
+ mock.onGet().replyOnce(response.code, response.res);
+
+ setHTMLFixture(`<div class="${htmlClass}"></div>`);
+
+ vueApps = await initGitlabVersionCheck();
+ };
+
+ afterEach(() => {
+ mock.restore();
+ window.gon = originalGon;
+ resetHTMLFixture();
+ });
+
+ describe('with no .js-gitlab-version-check-badge elements', () => {
+ beforeEach(async () => {
+ await createApp();
+ });
+
+ it('does not make axios GET request', () => {
+ expect(mock.history.get.length).toBe(0);
+ });
+
+ it('does not render the Version Check Badge', () => {
+ expect(vueApps).toBeNull();
+ });
+ });
+
+ describe('with .js-gitlab-version-check-badge element but API errors', () => {
+ beforeEach(async () => {
+ jest.spyOn(Sentry, 'captureException');
+ await createApp({ code: 500, res: null }, 'js-gitlab-version-check-badge');
+ });
+
+ it('does make axios GET request', () => {
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].url).toContain('/admin/version_check.json');
+ });
+
+ it('logs error to Sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+
+ it('does not render the Version Check Badge', () => {
+ expect(vueApps).toBeNull();
+ });
+ });
+
+ describe('with .js-gitlab-version-check-badge element and successful API call', () => {
+ beforeEach(async () => {
+ await createApp({}, 'js-gitlab-version-check-badge');
+ });
+
+ it('does make axios GET request', () => {
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].url).toContain('/admin/version_check.json');
+ });
+
+ it('does render the Version Check Badge', () => {
+ expect(vueApps).toHaveLength(1);
+ expect(vueApps[0]).toBeInstanceOf(Vue);
+ });
+ });
+
+ describe.each`
+ root | description
+ ${'/'} | ${'not used (uses its own (sub)domain)'}
+ ${'/gitlab'} | ${'custom path'}
+ ${'/service/gitlab'} | ${'custom path with 2 depth'}
+ `('path for version_check.json', ({ root, description }) => {
+ describe(`when relative url is ${description}: ${root}`, () => {
+ beforeEach(async () => {
+ originalGon = window.gon;
+ window.gon = { ...dummyGon };
+ window.gon.relative_url_root = root;
+ await createApp({}, 'js-gitlab-version-check-badge');
+ });
+
+ it('reflects the relative url setting', () => {
+ expect(mock.history.get.length).toBe(1);
+
+ const pathRegex = new RegExp(`^${root}`);
+ expect(mock.history.get[0].url).toMatch(pathRegex);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/gl_field_errors_spec.js b/spec/frontend/gl_field_errors_spec.js
index 92d04927ee5..1f6929baa75 100644
--- a/spec/frontend/gl_field_errors_spec.js
+++ b/spec/frontend/gl_field_errors_spec.js
@@ -27,7 +27,7 @@ describe('GL Style Field Errors', () => {
expect(testContext.fieldErrors).toBeDefined();
const { inputs } = testContext.fieldErrors.state;
- expect(inputs.length).toBe(4);
+ expect(inputs.length).toBe(5);
});
it('should ignore elements with custom error handling', () => {
diff --git a/spec/frontend/google_cloud/service_accounts/list_spec.js b/spec/frontend/google_cloud/service_accounts/list_spec.js
index 7a76a893757..c2bd2005b5d 100644
--- a/spec/frontend/google_cloud/service_accounts/list_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/list_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlAlert, GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
+import { GlAlert, GlButton, GlEmptyState, GlLink, GlTable } from '@gitlab/ui';
import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue';
describe('google_cloud/service_accounts/list', () => {
@@ -45,7 +45,26 @@ describe('google_cloud/service_accounts/list', () => {
beforeEach(() => {
const propsData = {
- list: [{}, {}, {}],
+ list: [
+ {
+ ref: '*',
+ gcp_project: 'gcp-project-123',
+ service_account_exists: true,
+ service_account_key_exists: true,
+ },
+ {
+ ref: 'prod',
+ gcp_project: 'gcp-project-456',
+ service_account_exists: true,
+ service_account_key_exists: true,
+ },
+ {
+ ref: 'stag',
+ gcp_project: 'gcp-project-789',
+ service_account_exists: true,
+ service_account_key_exists: true,
+ },
+ ],
createUrl: '#create-url',
emptyIllustrationUrl: '#empty-illustration-url',
};
@@ -68,6 +87,12 @@ describe('google_cloud/service_accounts/list', () => {
expect(findRows().length).toBe(4);
});
+ it('table row must contain link to the google cloud console', () => {
+ expect(findRows().at(1).findComponent(GlLink).attributes('href')).toBe(
+ `${ServiceAccountsList.GOOGLE_CONSOLE_URL}?project=gcp-project-123`,
+ );
+ });
+
it('shows the link to create new service accounts', () => {
const button = findButton();
expect(button.exists()).toBe(true);
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index 93e087e10f2..b615679dcc5 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -67,6 +67,7 @@ describe('OverviewTabs', () => {
const findTabPanels = () => wrapper.findAllComponents(GlTab);
const findTab = (name) => wrapper.findByRole('tab', { name });
const findSelectedTab = () => wrapper.findByRole('tab', { selected: true });
+ const findSearchInput = () => wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder);
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
@@ -244,18 +245,39 @@ describe('OverviewTabs', () => {
};
describe('when search is typed in', () => {
- const search = 'Foo bar';
+ describe('when search is greater than or equal to 3 characters', () => {
+ const search = 'Foo bar';
- beforeEach(async () => {
- await setup();
- await wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).setValue(search);
- });
+ beforeEach(async () => {
+ await setup();
+ await findSearchInput().setValue(search);
+ });
- it('updates query string with `filter` key', () => {
- expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } });
+ it('updates query string with `filter` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } });
+ });
+
+ sharedAssertions({ search, sort: defaultProvide.initialSort });
});
- sharedAssertions({ search, sort: defaultProvide.initialSort });
+ describe('when search is less than 3 characters', () => {
+ const search = 'Fo';
+
+ beforeEach(async () => {
+ await setup();
+ await findSearchInput().setValue(search);
+ });
+
+ it('does not emit `fetchFilteredAndSortedGroups` event from `eventHub`', () => {
+ expect(eventHub.$emit).not.toHaveBeenCalledWith(
+ `${ACTIVE_TAB_SUBGROUPS_AND_PROJECTS}fetchFilteredAndSortedGroups`,
+ {
+ filterGroupsBy: search,
+ sortBy: defaultProvide.initialSort,
+ },
+ );
+ });
+ });
});
describe('when sort is changed', () => {
@@ -308,6 +330,16 @@ describe('OverviewTabs', () => {
).toBe('Foo bar');
});
+ describe('when search is cleared', () => {
+ it('removes `filter` key from query string', async () => {
+ await findSearchInput().setValue('');
+
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { sort: SORTING_ITEM_UPDATED.desc },
+ });
+ });
+ });
+
it('sets sort dropdown', () => {
expect(wrapper.findComponent(GlSorting).props()).toMatchObject({
text: SORTING_ITEM_UPDATED.label,
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
index 7cbe6e5bbab..0065820f78f 100644
--- a/spec/frontend/groups/components/transfer_group_form_spec.js
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -1,8 +1,13 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import Component from '~/groups/components/transfer_group_form.vue';
+import TransferLocationsForm, { i18n } from '~/groups/components/transfer_group_form.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import { getGroupTransferLocations } from '~/api/groups_api';
+
+jest.mock('~/api/groups_api', () => ({
+ getGroupTransferLocations: jest.fn(),
+}));
describe('Transfer group form', () => {
let wrapper;
@@ -22,25 +27,25 @@ describe('Transfer group form', () => {
];
const defaultProps = {
- groupNamespaces,
paidGroupHelpLink,
isPaidGroup: false,
confirmationPhrase,
confirmButtonText,
};
- const createComponent = (propsData = {}) =>
- shallowMountExtended(Component, {
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMountExtended(TransferLocationsForm, {
propsData: {
...defaultProps,
...propsData,
},
stubs: { GlSprintf },
});
+ };
const findAlert = () => wrapper.findComponent(GlAlert);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
+ const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findHiddenInput = () => wrapper.find('[name="new_parent_group_id"]');
afterEach(() => {
@@ -49,21 +54,17 @@ describe('Transfer group form', () => {
describe('default', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
- it('renders the namespace select component', () => {
- expect(findNamespaceSelect().exists()).toBe(true);
- });
+ it('renders the transfer locations dropdown and passes correct props', () => {
+ findTransferLocations().props('groupTransferLocationsApiMethod')();
- it('sets the namespace select properties', () => {
- expect(findNamespaceSelect().props()).toMatchObject({
- defaultText: 'Select parent group',
- fullWidth: false,
- includeHeaders: false,
- emptyNamespaceTitle: 'No parent group',
- includeEmptyNamespace: true,
- groupNamespaces,
+ expect(getGroupTransferLocations).toHaveBeenCalled();
+ expect(findTransferLocations().props()).toMatchObject({
+ value: null,
+ label: i18n.dropdownLabel,
+ additionalDropdownItems: TransferLocationsForm.additionalDropdownItems,
});
});
@@ -90,10 +91,15 @@ describe('Transfer group form', () => {
});
describe('with a selected project', () => {
- const [firstGroup] = groupNamespaces;
+ const [selectedItem] = groupNamespaces;
+
beforeEach(() => {
- wrapper = createComponent();
- findNamespaceSelect().vm.$emit('select', firstGroup);
+ createComponent();
+ findTransferLocations().vm.$emit('input', selectedItem);
+ });
+
+ it('sets `value` prop on `TransferLocations` component', () => {
+ expect(findTransferLocations().props('value')).toEqual(selectedItem);
});
it('sets the confirm danger disabled property to false', () => {
@@ -102,7 +108,7 @@ describe('Transfer group form', () => {
it('sets the hidden input field', () => {
expect(findHiddenInput().exists()).toBe(true);
- expect(parseInt(findHiddenInput().attributes('value'), 10)).toBe(firstGroup.id);
+ expect(findHiddenInput().attributes('value')).toBe(String(selectedItem.id));
});
it('emits "confirm" event when the danger modal is confirmed', () => {
@@ -116,15 +122,15 @@ describe('Transfer group form', () => {
describe('isPaidGroup = true', () => {
beforeEach(() => {
- wrapper = createComponent({ isPaidGroup: true });
+ createComponent({ isPaidGroup: true });
});
it('disables the transfer button', () => {
expect(findConfirmDanger().props()).toMatchObject({ disabled: true });
});
- it('hides the namespace selector button', () => {
- expect(findNamespaceSelect().exists()).toBe(false);
+ it('hides the transfer locations dropdown', () => {
+ expect(findTransferLocations().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js
new file mode 100644
index 00000000000..74424ee3230
--- /dev/null
+++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js
@@ -0,0 +1,377 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlAlert,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
+import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
+import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { __ } from '~/locale';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import { getTransferLocations } from '~/api/projects_api';
+import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
+
+jest.mock('~/api/projects_api', () => ({
+ getTransferLocations: jest.fn(),
+}));
+
+describe('TransferLocations', () => {
+ let wrapper;
+
+ // Default data
+ const resourceId = '1';
+ const defaultPropsData = {
+ groupTransferLocationsApiMethod: getTransferLocations,
+ value: null,
+ };
+ const additionalDropdownItem = {
+ id: -1,
+ humanName: __('No parent group'),
+ };
+
+ // Mock requests
+ const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
+ const mockResolvedGetTransferLocations = ({
+ data = transferLocationsResponsePage1,
+ page = '1',
+ nextPage = '2',
+ total = '4',
+ totalPages = '2',
+ prevPage = null,
+ } = {}) => {
+ getTransferLocations.mockResolvedValueOnce({
+ data,
+ headers: {
+ 'x-per-page': '2',
+ 'x-page': page,
+ 'x-total': total,
+ 'x-total-pages': totalPages,
+ 'x-next-page': nextPage,
+ 'x-prev-page': prevPage,
+ },
+ });
+ };
+ const mockRejectedGetTransferLocations = () => {
+ const error = new Error();
+
+ getTransferLocations.mockRejectedValueOnce(error);
+ };
+
+ // VTU wrapper helpers
+ Vue.use(VueApollo);
+ const createComponent = ({
+ propsData = {},
+ requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
+ } = {}) => {
+ wrapper = mountExtended(TransferLocations, {
+ provide: {
+ resourceId,
+ },
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ apolloProvider: createMockApollo(requestHandlers),
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('show');
+ await waitForPromises();
+ };
+ const findUserTransferLocations = () =>
+ wrapper
+ .findByTestId('user-transfer-locations')
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((dropdownItem) => dropdownItem.text());
+ const findGroupTransferLocations = () =>
+ wrapper
+ .findByTestId('group-transfer-locations')
+ .findAllComponents(GlDropdownItem)
+ .wrappers.map((dropdownItem) => dropdownItem.text());
+ const findDropdownItemByText = (text) =>
+ wrapper
+ .findAllComponents(GlDropdownItem)
+ .wrappers.find((dropdownItem) => dropdownItem.text() === text);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
+ const searchEmitInput = (searchTerm = 'foo') => findSearch().vm.$emit('input', searchTerm);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when `GlDropdown` is opened', () => {
+ it('shows loading icon', async () => {
+ getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
+ createComponent();
+ findDropdown().vm.$emit('show');
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('fetches and renders user and group transfer locations', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
+
+ expect(findUserTransferLocations()).toEqual([namespace.fullName]);
+ expect(findGroupTransferLocations()).toEqual(
+ transferLocationsResponsePage1.map((transferLocation) => transferLocation.full_name),
+ );
+ });
+
+ describe('when `showUserTransferLocations` prop is `false`', () => {
+ it('does not fetch user transfer locations', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent({
+ propsData: {
+ showUserTransferLocations: false,
+ },
+ });
+ await showDropdown();
+
+ expect(wrapper.findByTestId('user-transfer-locations').exists()).toBe(false);
+ });
+ });
+
+ describe('when `additionalDropdownItems` prop is passed', () => {
+ it('displays additional dropdown items', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ });
+ await showDropdown();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true);
+ });
+
+ describe('when loading', () => {
+ it('does not display additional dropdown items', async () => {
+ getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
+ createComponent({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ });
+ findDropdown().vm.$emit('show');
+ await nextTick();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined();
+ });
+ });
+ });
+
+ describe('when transfer locations have already been fetched', () => {
+ beforeEach(async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+ });
+
+ it('does not fetch transfer locations', async () => {
+ getTransferLocations.mockClear();
+ defaultQueryHandler.mockClear();
+
+ await showDropdown();
+
+ expect(getTransferLocations).not.toHaveBeenCalled();
+ expect(defaultQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays dismissible error alert', async () => {
+ mockRejectedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+
+ alert.vm.$emit('dismiss');
+ await nextTick();
+
+ expect(alert.exists()).toBe(false);
+ });
+ });
+
+ describe('when `currentUser` GraphQL query fails', () => {
+ it('displays error alert', async () => {
+ mockResolvedGetTransferLocations();
+ const error = new Error();
+ createComponent({
+ requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
+ });
+ await showDropdown();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when transfer location is selected', () => {
+ it('displays transfer location as selected', () => {
+ const [{ id, full_name: humanName }] = transferLocationsResponsePage1;
+
+ createComponent({
+ propsData: {
+ value: {
+ id,
+ humanName,
+ },
+ },
+ });
+
+ expect(findDropdown().props('text')).toBe(humanName);
+ });
+ });
+
+ describe('when search is typed in', () => {
+ const transferLocationsResponseSearch = [transferLocationsResponsePage1[0]];
+
+ const arrange = async ({ propsData, searchTerm } = {}) => {
+ mockResolvedGetTransferLocations();
+ createComponent({ propsData });
+ await showDropdown();
+ mockResolvedGetTransferLocations({ data: transferLocationsResponseSearch });
+ searchEmitInput(searchTerm);
+ await nextTick();
+ };
+
+ it('sets `isSearchLoading` prop to `true`', async () => {
+ await arrange();
+
+ expect(findSearch().props('isLoading')).toBe(true);
+ });
+
+ it('passes `search` param to API call and updates group transfer locations', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ resourceId,
+ expect.objectContaining({ search: 'foo' }),
+ );
+ expect(findGroupTransferLocations()).toEqual(
+ transferLocationsResponseSearch.map((transferLocation) => transferLocation.full_name),
+ );
+ });
+
+ it('does not display additional dropdown items if they do not match the search', async () => {
+ await arrange({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ });
+ await waitForPromises();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined();
+ });
+
+ it('displays additional dropdown items if they match the search', async () => {
+ await arrange({
+ propsData: {
+ additionalDropdownItems: [additionalDropdownItem],
+ },
+ searchTerm: 'No par',
+ });
+ await waitForPromises();
+
+ expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true);
+ });
+ });
+
+ describe('when there are no more pages', () => {
+ it('does not show intersection observer', async () => {
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage1,
+ nextPage: null,
+ total: '2',
+ totalPages: '1',
+ prevPage: null,
+ });
+ createComponent();
+ await showDropdown();
+
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+ });
+
+ describe('when intersection observer appears', () => {
+ const arrange = async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showDropdown();
+
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage2,
+ page: '2',
+ nextPage: null,
+ prevPage: '1',
+ totalPages: '2',
+ });
+
+ intersectionObserverEmitAppear();
+ await nextTick();
+ };
+
+ it('shows loading icon', async () => {
+ await arrange();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('passes `page` param to API call', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ resourceId,
+ expect.objectContaining({ page: 2 }),
+ );
+ });
+
+ it('updates dropdown with new group transfer locations', async () => {
+ await arrange();
+
+ await waitForPromises();
+
+ expect(findGroupTransferLocations()).toEqual(
+ [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
+ ({ full_name: fullName }) => fullName,
+ ),
+ );
+ });
+ });
+
+ describe('when `label` prop is passed', () => {
+ it('renders label', () => {
+ const label = 'Foo bar';
+
+ createComponent({ propsData: { label } });
+
+ expect(wrapper.findByRole('group', { name: label }).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 48c670757a2..a575f428a69 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -3,9 +3,11 @@ import Vue from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { stubPerformanceWebAPI } from 'helpers/performance';
+import { __ } from '~/locale';
import CannotPushCodeAlert from '~/ide/components/cannot_push_code_alert.vue';
import ErrorMessage from '~/ide/components/error_message.vue';
import Ide from '~/ide/components/ide.vue';
+import eventHub from '~/ide/eventhub';
import { MSG_CANNOT_PUSH_CODE_GO_TO_FORK, MSG_GO_TO_FORK } from '~/ide/messages';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
@@ -14,6 +16,7 @@ import { projectData } from '../mock_data';
Vue.use(Vuex);
const TEST_FORK_IDE_PATH = '/test/ide/path';
+const MSG_ARE_YOU_SURE = __('Are you sure you want to lose unsaved changes?');
describe('WebIDE', () => {
const emptyProjData = { ...projectData, empty_repo: true, branches: {} };
@@ -40,6 +43,8 @@ describe('WebIDE', () => {
const findAlert = () => wrapper.findComponent(CannotPushCodeAlert);
+ const callOnBeforeUnload = (e = {}) => window.onbeforeunload(e);
+
beforeEach(() => {
stubPerformanceWebAPI();
@@ -49,6 +54,7 @@ describe('WebIDE', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ window.onbeforeunload = null;
});
describe('ide component, empty repo', () => {
@@ -90,7 +96,8 @@ describe('WebIDE', () => {
describe('onBeforeUnload', () => {
it('returns undefined when no staged files or changed files', () => {
createComponent();
- expect(wrapper.vm.onBeforeUnload()).toBe(undefined);
+
+ expect(callOnBeforeUnload()).toBe(undefined);
});
it('returns warning text when their are changed files', () => {
@@ -100,7 +107,10 @@ describe('WebIDE', () => {
},
});
- expect(wrapper.vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
+ const e = {};
+
+ expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE);
+ expect(e.returnValue).toBe(MSG_ARE_YOU_SURE);
});
it('returns warning text when their are staged files', () => {
@@ -110,20 +120,27 @@ describe('WebIDE', () => {
},
});
- expect(wrapper.vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
+ const e = {};
+
+ expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE);
+ expect(e.returnValue).toBe(MSG_ARE_YOU_SURE);
});
- it('updates event object', () => {
- const event = {};
+ it('returns undefined once after "skip-beforeunload" was emitted', () => {
createComponent({
state: {
stagedFiles: [file()],
},
});
- wrapper.vm.onBeforeUnload(event);
+ eventHub.$emit('skip-beforeunload');
+ const e = {};
+
+ expect(callOnBeforeUnload()).toBe(undefined);
+ expect(e.returnValue).toBe(undefined);
- expect(event.returnValue).toBe('Are you sure you want to lose unsaved changes?');
+ expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE);
+ expect(e.returnValue).toBe(MSG_ARE_YOU_SURE);
});
});
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index 1d38231a767..e92f843ae6e 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
@@ -127,5 +127,29 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
});
});
});
+
+ describe('with initOpenView that does not exist', () => {
+ beforeEach(async () => {
+ createComponent({ extensionTabs, initOpenView: 'does-not-exist' });
+
+ await nextTick();
+ });
+
+ it('nothing is dispatched', () => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with initOpenView that does exist', () => {
+ beforeEach(async () => {
+ createComponent({ extensionTabs, initOpenView: fakeView.name });
+
+ await nextTick();
+ });
+
+ it('dispatches open with view on create', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('rightPane/open', fakeView);
+ });
+ });
});
});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 4555f519bc2..b7349b8fed1 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -3,12 +3,16 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import RightPane from '~/ide/components/panes/right.vue';
+import SwitchEditorsView from '~/ide/components/switch_editors/switch_editors_view.vue';
import { rightSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
import extendStore from '~/ide/stores/extend';
+import { __ } from '~/locale';
Vue.use(Vuex);
+const SWITCH_EDITORS_VIEW_NAME = 'switch-editors';
+
describe('ide/components/panes/right.vue', () => {
let wrapper;
let store;
@@ -33,6 +37,19 @@ describe('ide/components/panes/right.vue', () => {
wrapper = null;
});
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders collapsible-sidebar', () => {
+ expect(wrapper.findComponent(CollapsibleSidebar).props()).toMatchObject({
+ side: 'right',
+ initOpenView: SWITCH_EDITORS_VIEW_NAME,
+ });
+ });
+ });
+
describe('pipelines tab', () => {
it('is always shown', () => {
createComponent();
@@ -113,4 +130,32 @@ describe('ide/components/panes/right.vue', () => {
);
});
});
+
+ describe('switch editors tab', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ desc | canUseNewWebIde | expectedShow
+ ${'is shown'} | ${true} | ${true}
+ ${'is not shown'} | ${false} | ${false}
+ `('with canUseNewWebIde=$canUseNewWebIde, $desc', async ({ canUseNewWebIde, expectedShow }) => {
+ Object.assign(store.state, { canUseNewWebIde });
+
+ await nextTick();
+
+ expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ show: expectedShow,
+ title: __('Switch editors'),
+ views: [
+ { component: SwitchEditorsView, name: SWITCH_EDITORS_VIEW_NAME, keepAlive: true },
+ ],
+ }),
+ ]),
+ );
+ });
+ });
});
diff --git a/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js b/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
new file mode 100644
index 00000000000..7a958391fea
--- /dev/null
+++ b/spec/frontend/ide/components/switch_editors/switch_editors_view_spec.js
@@ -0,0 +1,214 @@
+import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { createAlert } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { logError } from '~/lib/logger';
+import { __ } from '~/locale';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import SwitchEditorsView, {
+ MSG_ERROR_ALERT,
+ MSG_CONFIRM,
+ MSG_TITLE,
+ MSG_LEARN_MORE,
+ MSG_DESCRIPTION,
+} from '~/ide/components/switch_editors/switch_editors_view.vue';
+import eventHub from '~/ide/eventhub';
+import { createStore } from '~/ide/stores';
+
+jest.mock('~/flash');
+jest.mock('~/lib/logger');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+const TEST_USER_PREFERENCES_PATH = '/test/user-pref/path';
+const TEST_SWITCH_EDITOR_SVG_PATH = '/test/switch/editor/path.svg';
+const TEST_HREF = '/test/new/web/ide/href';
+
+describe('~/ide/components/switch_editors/switch_editors_view.vue', () => {
+ useMockLocationHelper();
+
+ let store;
+ let wrapper;
+ let confirmResolve;
+ let requestSpy;
+ let skipBeforeunloadSpy;
+ let axiosMock;
+
+ // region: finders ------------------
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ // region: actions ------------------
+ const triggerSwitchPreference = () => findButton().vm.$emit('click');
+ const submitConfirm = async (val) => {
+ confirmResolve(val);
+
+ // why: We need to wait for promises for the immediate next lines to be executed
+ await waitForPromises();
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(SwitchEditorsView, {
+ store,
+ stubs: {
+ GlEmptyState,
+ },
+ });
+ };
+
+ // region: test setup ------------------
+ beforeEach(() => {
+ // Setup skip-beforeunload side-effect
+ skipBeforeunloadSpy = jest.fn();
+ eventHub.$on('skip-beforeunload', skipBeforeunloadSpy);
+
+ // Setup request side-effect
+ requestSpy = jest.fn().mockImplementation(() => new Promise(() => {}));
+ axiosMock = new MockAdapter(axios);
+ axiosMock.onPut(TEST_USER_PREFERENCES_PATH).reply(({ data }) => requestSpy(data));
+
+ // Setup store
+ store = createStore();
+ store.state.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
+ store.state.switchEditorSvgPath = TEST_SWITCH_EDITOR_SVG_PATH;
+ store.state.links = {
+ newWebIDEHelpPagePath: TEST_HREF,
+ };
+
+ // Setup user confirm side-effect
+ confirmAction.mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ confirmResolve = resolve;
+ }),
+ );
+ });
+
+ afterEach(() => {
+ eventHub.$off('skip-beforeunload', skipBeforeunloadSpy);
+
+ axiosMock.restore();
+ });
+
+ // region: tests ------------------
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('render empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: TEST_SWITCH_EDITOR_SVG_PATH,
+ svgHeight: 150,
+ title: MSG_TITLE,
+ });
+ });
+
+ it('render link', () => {
+ expect(wrapper.findComponent(GlLink).attributes('href')).toBe(TEST_HREF);
+ expect(wrapper.findComponent(GlLink).text()).toBe(MSG_LEARN_MORE);
+ });
+
+ it('renders description', () => {
+ expect(findEmptyState().text()).toContain(MSG_DESCRIPTION);
+ });
+
+ it('is not loading', () => {
+ expect(findButton().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when user triggers switch preference', () => {
+ beforeEach(() => {
+ createComponent();
+
+ triggerSwitchPreference();
+ });
+
+ it('creates a single confirm', () => {
+ // Call again to ensure that we only show 1 confirm action
+ triggerSwitchPreference();
+
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+ expect(confirmAction).toHaveBeenCalledWith(MSG_CONFIRM, {
+ primaryBtnText: __('Switch editors'),
+ cancelBtnText: __('Cancel'),
+ });
+ });
+
+ it('starts loading', () => {
+ expect(findButton().props('loading')).toBe(true);
+ });
+
+ describe('when user cancels confirm', () => {
+ beforeEach(async () => {
+ await submitConfirm(false);
+ });
+
+ it('does not make request', () => {
+ expect(requestSpy).not.toHaveBeenCalled();
+ });
+
+ it('can be triggered again', () => {
+ triggerSwitchPreference();
+
+ expect(confirmAction).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('when user accepts confirm and response success', () => {
+ beforeEach(async () => {
+ requestSpy.mockReturnValue([200, {}]);
+ await submitConfirm(true);
+ });
+
+ it('does not handle error', () => {
+ expect(logError).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
+ it('emits "skip-beforeunload" and reloads', () => {
+ expect(skipBeforeunloadSpy).toHaveBeenCalledTimes(1);
+ expect(window.location.reload).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls request', () => {
+ expect(requestSpy).toHaveBeenCalledTimes(1);
+ expect(requestSpy).toHaveBeenCalledWith(
+ JSON.stringify({ user: { use_legacy_web_ide: false } }),
+ );
+ });
+
+ it('is not loading', () => {
+ expect(findButton().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when user accepts confirm and response fails', () => {
+ beforeEach(async () => {
+ requestSpy.mockReturnValue([400, {}]);
+ await submitConfirm(true);
+ });
+
+ it('handles error', () => {
+ expect(logError).toHaveBeenCalledTimes(1);
+ expect(logError).toHaveBeenCalledWith(
+ 'Error while updating user preferences',
+ expect.any(Error),
+ );
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: MSG_ERROR_ALERT,
+ });
+ });
+
+ it('does not reload', () => {
+ expect(skipBeforeunloadSpy).not.toHaveBeenCalled();
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 4117f2648bd..ae21d257bb2 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -87,11 +87,13 @@ describe('Multi-file store mutations', () => {
emptyStateSvgPath: 'emptyState',
noChangesStateSvgPath: 'noChanges',
committedStateSvgPath: 'committed',
+ switchEditorSvgPath: 'switchEditorSvg',
});
expect(localState.emptyStateSvgPath).toBe('emptyState');
expect(localState.noChangesStateSvgPath).toBe('noChanges');
expect(localState.committedStateSvgPath).toBe('committed');
+ expect(localState.switchEditorSvgPath).toBe('switchEditorSvg');
});
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index a0115cb9349..61f860688dc 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -1,10 +1,11 @@
-import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createAlert } from '~/flash';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
@@ -50,6 +51,7 @@ describe('import table', () => {
rowWrapper.find('[data-testid="target-namespace-selector"]');
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
+ const findNewPathCol = () => wrapper.find('[data-test-id="new-path-col"]');
const triggerSelectAllCheckbox = (checked = true) =>
wrapper.find('thead input[type=checkbox]').setChecked(checked);
@@ -76,6 +78,9 @@ describe('import table', () => {
historyPath: '/fake_history_path',
defaultTargetNamespace,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
apolloProvider,
});
};
@@ -540,6 +545,26 @@ describe('import table', () => {
);
});
+ it('displays info icon with a tooltip', async () => {
+ const NEW_GROUPS = [generateFakeEntry({ id: 1, status: STATUSES.NONE })];
+
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: NEW_GROUPS,
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ await waitForPromises();
+
+ const icon = findNewPathCol().findComponent(GlIcon);
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe('Path of the new group.');
+ });
+
describe('unavailable features warning', () => {
it('renders alert when there are unavailable features', async () => {
createComponent({
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 0a3beee0507..7e67379f5ab 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlForm } from '@gitlab/ui';
+import { GlAlert, GlBadge, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser';
@@ -80,6 +80,7 @@ describe('IntegrationForm', () => {
const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group');
const findTestButton = () => wrapper.findByTestId('test-button');
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findGlBadge = () => wrapper.findComponent(GlBadge);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
@@ -715,45 +716,72 @@ describe('IntegrationForm', () => {
});
});
- describe('Help and sections rendering', () => {
- const dummyHelp = 'Foo Help';
+ describe('Slack integration', () => {
+ describe('Help and sections rendering', () => {
+ const dummyHelp = 'Foo Help';
+
+ it.each`
+ integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
+ ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ `(
+ '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
+ ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ createComponent({
+ provide: {
+ helpHtml,
+ glFeatures: { integrationSlackAppNotifications: flagIsOn },
+ },
+ customStateProps: {
+ sections,
+ type: integration,
+ },
+ });
+ expect(findAllSections().length > 0).toEqual(shouldShowSections);
+ expect(findHelpHtml().exists()).toBe(shouldShowHelp);
+ if (shouldShowHelp) {
+ expect(findHelpHtml().html()).toContain(helpHtml);
+ }
+ },
+ );
+ });
it.each`
- integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
- ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert
+ ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true}
+ ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${true} | ${false}
+ ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${false} | ${false}
+ ${'does not'} | ${'foo'} | ${true} | ${true} | ${false}
+ ${'does not'} | ${'foo'} | ${false} | ${true} | ${false}
+ ${'does not'} | ${'foo'} | ${true} | ${false} | ${false}
`(
- '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
- ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ '$prefix render the upgrade warnning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack"',
+ ({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => {
createComponent({
provide: {
- helpHtml,
glFeatures: { integrationSlackAppNotifications: flagIsOn },
},
customStateProps: {
- sections,
+ shouldUpgradeSlack,
type: integration,
+ sections: [mockSectionConnection],
},
});
- expect(findAllSections().length > 0).toEqual(shouldShowSections);
- expect(findHelpHtml().exists()).toBe(shouldShowHelp);
- if (shouldShowHelp) {
- expect(findHelpHtml().html()).toContain(helpHtml);
- }
+ expect(findAlert().exists()).toBe(shouldShowAlert);
},
);
});
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index c329ca8522f..082eeea30f1 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 { mountExtended } from 'helpers/vue_test_utils_helper';
+import { placeholderForType } from 'jh_else_ce/integrations/constants';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
@@ -28,6 +29,50 @@ describe('TriggerFields', () => {
const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
const findAllGlFormInputs = () => wrapper.findAllComponents(GlFormInput);
+ describe('placeholder text on the event fields and default values', () => {
+ const dummyFieldPlaceholder = '#foo';
+ const integrationTypes = {
+ INTEGRATION_TYPE_SLACK: 'slack',
+ INTEGRATION_TYPE_SLACK_APPLICATION: 'gitlab_slack_application',
+ INTEGRATION_TYPE_MATTERMOST: 'mattermost',
+ INTEGRATION_TYPE_NON_EXISTING: 'non_existing',
+ };
+ it.each`
+ integrationType | fieldPlaceholder | expectedPlaceholder
+ ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION]}
+ ${integrationTypes.INTEGRATION_TYPE_SLACK_APPLICATION} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${undefined} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_MATTERMOST]}
+ ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${''} | ${placeholderForType[integrationTypes.INTEGRATION_TYPE_MATTERMOST]}
+ ${integrationTypes.INTEGRATION_TYPE_MATTERMOST} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${undefined} | ${undefined}
+ ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${''} | ${undefined}
+ ${integrationTypes.INTEGRATION_TYPE_NON_EXISTING} | ${dummyFieldPlaceholder} | ${dummyFieldPlaceholder}
+ `(
+ 'passed down correct placeholder for "$integrationType" type and "$fieldPlaceholder" placeholder on the field',
+ ({ integrationType, fieldPlaceholder, expectedPlaceholder }) => {
+ createComponent({
+ type: integrationType,
+ events: [
+ {
+ field: {
+ name: 'foo',
+ value: '',
+ placeholder: fieldPlaceholder,
+ },
+ },
+ ],
+ });
+ const field = wrapper.findComponent(GlFormInput);
+
+ expect(field.attributes('placeholder')).toBe(expectedPlaceholder);
+ },
+ );
+ });
+
describe.each([true, false])('template, isInheriting = `%p`', (isInheriting) => {
it('renders a label with text "Trigger"', () => {
createComponent();
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 e9e1fbad07b..47be1933ed7 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -10,12 +10,12 @@ import InviteMembersModal from '~/invite_members/components/invite_members_modal
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
+import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
import {
INVITE_MEMBERS_FOR_TASK,
MEMBERS_MODAL_CELEBRATE_INTRO,
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_PLACEHOLDER,
- MEMBERS_PLACEHOLDER_DISABLED,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
EXPANDED_ERRORS,
@@ -31,8 +31,6 @@ import {
propsData,
inviteSource,
newProjectPath,
- freeUsersLimit,
- membersCount,
user1,
user2,
user3,
@@ -99,6 +97,7 @@ describe('InviteMembersModal', () => {
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
+ const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
const findAccordion = () => wrapper.findComponent(GlCollapse);
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
const findMemberErrorMessage = (element) =>
@@ -112,7 +111,7 @@ describe('InviteMembersModal', () => {
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
- const membersFormGroupText = () => findMembersFormGroup().text();
+ const membersFormGroupDescription = () => findMembersFormGroup().attributes('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
@@ -299,19 +298,8 @@ describe('InviteMembersModal', () => {
describe('members form group description', () => {
it('renders correct description', () => {
- createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
- });
-
- describe('when reached user limit', () => {
- it('renders correct description', () => {
- createInviteMembersToProjectWrapper(
- { freeUsersLimit, membersCount: 5 },
- { GlFormGroup },
- );
-
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
- });
+ createInviteMembersToProjectWrapper({ GlFormGroup });
+ expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
});
@@ -339,23 +327,10 @@ describe('InviteMembersModal', () => {
describe('members form group description', () => {
it('renders correct description', async () => {
- createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
+ createInviteMembersToProjectWrapper({ GlFormGroup });
await triggerOpenModal({ mode: 'celebrate' });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
- });
-
- describe('when reached user limit', () => {
- it('renders correct description', async () => {
- createInviteMembersToProjectWrapper(
- { freeUsersLimit, membersCount: 5 },
- { GlFormGroup },
- );
-
- await triggerOpenModal({ mode: 'celebrate' });
-
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
- });
+ expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
});
@@ -370,20 +345,39 @@ describe('InviteMembersModal', () => {
describe('members form group description', () => {
it('renders correct description', () => {
- createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
- });
-
- describe('when reached user limit', () => {
- it('renders correct description', () => {
- createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount: 5 }, { GlFormGroup });
- expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
- });
+ createInviteMembersToGroupWrapper({ GlFormGroup });
+ expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
});
});
+ describe('rendering the user limit notification', () => {
+ it('shows the user limit notification alert when reached limit', () => {
+ const usersLimitDataset = { reachedLimit: true };
+
+ createInviteMembersToProjectWrapper(usersLimitDataset);
+
+ expect(findUserLimitAlert().exists()).toBe(true);
+ });
+
+ it('shows the user limit notification alert when close to dashboard limit', () => {
+ const usersLimitDataset = { closeToDashboardLimit: true };
+
+ createInviteMembersToProjectWrapper(usersLimitDataset);
+
+ expect(findUserLimitAlert().exists()).toBe(true);
+ });
+
+ it('does not show the user limit notification alert', () => {
+ const usersLimitDataset = {};
+
+ createInviteMembersToProjectWrapper(usersLimitDataset);
+
+ expect(findUserLimitAlert().exists()).toBe(false);
+ });
+ });
+
describe('submitting the invite form', () => {
const mockInvitationsApi = (code, data) => {
mock.onPost(GROUPS_INVITATIONS_PATH).reply(code, data);
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index b55eeb72471..aeead8809fd 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -18,10 +18,7 @@ import {
CANCEL_BUTTON_TEXT,
INVITE_BUTTON_TEXT_DISABLED,
INVITE_BUTTON_TEXT,
- CANCEL_BUTTON_TEXT_DISABLED,
ON_SHOW_TRACK_LABEL,
- ON_CLOSE_TRACK_LABEL,
- ON_SUBMIT_TRACK_LABEL,
} from '~/invite_members/constants';
import { propsData, membersPath, purchasePath } from '../mock_data/modal_base';
@@ -131,7 +128,9 @@ describe('InviteModalBase', () => {
it('renders description', () => {
createComponent({}, { GlFormGroup });
- expect(findMembersFormGroup().text()).toContain(propsData.formGroupDescription);
+ expect(findMembersFormGroup().attributes('description')).toContain(
+ propsData.formGroupDescription,
+ );
});
describe('when users limit is reached', () => {
@@ -145,30 +144,13 @@ describe('InviteModalBase', () => {
beforeEach(() => {
createComponent(
- { usersLimitDataset: { membersPath, purchasePath }, reachedLimit: true },
+ { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } },
{ GlModal, GlFormGroup },
);
});
- it('renders correct blocks', () => {
- expect(findIcon().exists()).toBe(true);
- expect(findDisabledInput().exists()).toBe(true);
- expect(findDropdown().exists()).toBe(false);
- expect(findDatepicker().exists()).toBe(false);
- });
-
- it('renders correct buttons', () => {
- const cancelButton = findCancelButton();
- const actionButton = findActionButton();
-
- expect(cancelButton.attributes('href')).toBe(purchasePath);
- expect(cancelButton.text()).toBe(CANCEL_BUTTON_TEXT_DISABLED);
- expect(actionButton.attributes('href')).toBe(membersPath);
- expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED);
- });
-
it('tracks actions', () => {
- createComponent({ reachedLimit: true }, { GlFormGroup, GlModal });
+ createComponent({ usersLimitDataset: { reachedLimit: true } }, { GlFormGroup, GlModal });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
const modal = wrapper.findComponent(GlModal);
@@ -176,37 +158,20 @@ describe('InviteModalBase', () => {
modal.vm.$emit('shown');
expectTracking('render', ON_SHOW_TRACK_LABEL);
- modal.vm.$emit('cancel', { preventDefault: jest.fn() });
- expectTracking('click_button', ON_CLOSE_TRACK_LABEL);
-
- modal.vm.$emit('primary', { preventDefault: jest.fn() });
- expectTracking('click_button', ON_SUBMIT_TRACK_LABEL);
-
unmockTracking();
});
-
- describe('when free user namespace', () => {
- it('hides cancel button', () => {
- createComponent(
- {
- usersLimitDataset: { membersPath, purchasePath, userNamespace: true },
- reachedLimit: true,
- },
- { GlModal, GlFormGroup },
- );
-
- expect(findCancelButton().exists()).toBe(false);
- });
- });
});
describe('when user limit is close on a personal namespace', () => {
beforeEach(() => {
createComponent(
{
- closeToLimit: true,
- reachedLimit: false,
- usersLimitDataset: { membersPath, userNamespace: true },
+ usersLimitDataset: {
+ membersPath,
+ userNamespace: true,
+ closeToDashboardLimit: true,
+ reachedLimit: false,
+ },
},
{ GlModal, GlFormGroup },
);
diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js
index 1ff2e86412f..2a780490468 100644
--- a/spec/frontend/invite_members/components/user_limit_notification_spec.js
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -1,8 +1,8 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
-import { REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE } from '~/invite_members/constants';
-import { freeUsersLimit, membersCount } from '../mock_data/member_modal';
+import { REACHED_LIMIT_VARIANT, CLOSE_TO_LIMIT_VARIANT } from '~/invite_members/constants';
+import { freeUsersLimit, remainingSeats } from '../mock_data/member_modal';
const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name';
@@ -10,20 +10,16 @@ describe('UserLimitNotification', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findTrialLink = () => wrapper.findByTestId('trial-link');
+ const findUpgradeLink = () => wrapper.findByTestId('upgrade-link');
- const createComponent = (
- closeToLimit = false,
- reachedLimit = false,
- usersLimitDataset = {},
- props = {},
- ) => {
+ const createComponent = (limitVariant, usersLimitDataset = {}, props = {}) => {
wrapper = shallowMountExtended(UserLimitNotification, {
propsData: {
- closeToLimit,
- reachedLimit,
+ limitVariant,
usersLimitDataset: {
+ remainingSeats,
freeUsersLimit,
- membersCount,
newTrialRegistrationPath: 'newTrialRegistrationPath',
purchasePath: 'purchasePath',
...usersLimitDataset,
@@ -35,40 +31,46 @@ describe('UserLimitNotification', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when limit is not reached', () => {
- it('renders empty block', () => {
- createComponent();
-
- expect(findAlert().exists()).toBe(false);
- });
- });
-
describe('when close to limit within a group', () => {
it("renders user's limit notification", () => {
- createComponent(true, false, { membersCount: 3 });
+ createComponent(CLOSE_TO_LIMIT_VARIANT);
const alert = findAlert();
expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE);
- expect(alert.text()).toEqual(
- 'To get more members an owner of the group can start a trial or upgrade to a paid tier.',
- );
+ expect(alert.text()).toContain('To get more members an owner of the group can');
});
});
describe('when limit is reached', () => {
it("renders user's limit notification", () => {
- createComponent(true, true);
+ createComponent(REACHED_LIMIT_VARIANT);
const alert = findAlert();
expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name");
- expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE);
+ expect(alert.text()).toContain(
+ 'To invite new users to this namespace, you must remove existing users.',
+ );
});
});
+
+ describe('tracking', () => {
+ it.each([CLOSE_TO_LIMIT_VARIANT, REACHED_LIMIT_VARIANT])(
+ `has tracking attributes for %j variant`,
+ (variant) => {
+ createComponent(variant);
+
+ expect(findTrialLink().attributes('data-track-action')).toBe('click_link');
+ expect(findTrialLink().attributes('data-track-label')).toBe(
+ `start_trial_user_limit_notification_${variant}`,
+ );
+ expect(findUpgradeLink().attributes('data-track-action')).toBe('click_link');
+ expect(findUpgradeLink().attributes('data-track-label')).toBe(
+ `upgrade_user_limit_notification_${variant}`,
+ );
+ },
+ );
+ });
});
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 4f4e9345e46..59d58f21bb0 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -19,7 +19,7 @@ export const propsData = {
export const inviteSource = 'unknown';
export const newProjectPath = 'projects/new';
export const freeUsersLimit = 5;
-export const membersCount = 1;
+export const remainingSeats = 2;
export const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
export const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };
diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
new file mode 100644
index 00000000000..c432d722637
--- /dev/null
+++ b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js
@@ -0,0 +1,554 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { cloneDeep } from 'lodash';
+import VueApollo from 'vue-apollo';
+import { GlAlert } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import createFlash from '~/flash';
+import { logError } from '~/lib/logger';
+import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+import MoveIssuesButton from '~/issuable/bulk_update_sidebar/components/move_issues_button.vue';
+import issuableEventHub from '~/issues/list/eventhub';
+import moveIssueMutation from '~/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import { getIssuesCountsQueryResponse, getIssuesQueryResponse } from 'jest/issues/list/mock_data';
+import {
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+} from '~/work_items/constants';
+
+jest.mock('~/flash');
+jest.mock('~/lib/logger');
+useMockLocationHelper();
+
+const mockDefaultProps = {
+ projectFullPath: 'flight/FlightJS',
+ projectsFetchPath: '/-/autocomplete/projects?project_id=1',
+};
+
+const mockDestinationProject = {
+ full_path: 'gitlab-org/GitLabTest',
+};
+
+const mockMutationErrorMessage = 'Example error message';
+
+const mockIssue = {
+ iid: '15',
+ type: WORK_ITEM_TYPE_ENUM_ISSUE,
+};
+
+const mockIncident = {
+ iid: '32',
+ type: WORK_ITEM_TYPE_ENUM_INCIDENT,
+};
+
+const mockTask = {
+ iid: '40',
+ type: WORK_ITEM_TYPE_ENUM_TASK,
+};
+
+const mockTestCase = {
+ iid: '51',
+ type: WORK_ITEM_TYPE_ENUM_TEST_CASE,
+};
+
+const selectedIssuesMocks = {
+ tasksOnly: [mockTask],
+ testCasesOnly: [mockTestCase],
+ issuesOnly: [mockIssue, mockIncident],
+ tasksAndTestCases: [mockTask, mockTestCase],
+ issuesAndTasks: [mockIssue, mockIncident, mockTask],
+ issuesAndTestCases: [mockIssue, mockIncident, mockTestCase],
+ issuesTasksAndTestCases: [mockIssue, mockIncident, mockTask, mockTestCase],
+};
+
+let getIssuesQueryCompleteResponse = getIssuesQueryResponse;
+if (IS_EE) {
+ getIssuesQueryCompleteResponse = cloneDeep(getIssuesQueryResponse);
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].blockingCount = 1;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].healthStatus = null;
+ getIssuesQueryCompleteResponse.data.project.issues.nodes[0].weight = 5;
+}
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [],
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [{ message: mockMutationErrorMessage }],
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue({});
+
+const mockIssuesQueryResponse = jest.fn().mockResolvedValue(getIssuesQueryCompleteResponse);
+const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
+
+describe('MoveIssuesButton', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDropdown = () => wrapper.findComponent(IssuableMoveDropdown);
+ const emitMoveIssuablesEvent = () => {
+ findDropdown().vm.$emit('move-issuable', mockDestinationProject);
+ };
+
+ const createComponent = (data = {}, mutationResolverMock = rejectedMutationMock) => {
+ fakeApollo = createMockApollo([
+ [moveIssueMutation, mutationResolverMock],
+ [getIssuesQuery, mockIssuesQueryResponse],
+ [getIssuesCountsQuery, mockIssuesCountsQueryResponse],
+ ]);
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesQuery,
+ variables: {
+ isProject: true,
+ fullPath: mockDefaultProps.projectFullPath,
+ },
+ data: getIssuesQueryCompleteResponse.data,
+ });
+
+ fakeApollo.defaultClient.cache.writeQuery({
+ query: getIssuesCountsQuery,
+ variables: {
+ isProject: true,
+ },
+ data: getIssuesCountsQueryResponse.data,
+ });
+
+ wrapper = shallowMount(MoveIssuesButton, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ ...mockDefaultProps,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ beforeEach(() => {
+ // Needed due to a bug in Apollo: https://github.com/apollographql/apollo-client/issues/8900
+ // eslint-disable-next-line no-console
+ console.warn = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ fakeApollo = null;
+ });
+
+ describe('`Move selected` dropdown', () => {
+ it('renders disabled by default', () => {
+ createComponent();
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ });
+
+ it.each`
+ selectedIssuablesMock | disabled | status | testMessage
+ ${[]} | ${true} | ${'disabled'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'disabled'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'disabled'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'enabled'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'disabled'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${false} | ${'enabled'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${false} | ${'enabled'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${false} | ${'enabled'} | ${'issues and tasks and test cases are selected'}
+ `('renders $status if $testMessage', async ({ selectedIssuablesMock, disabled }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ if (disabled) {
+ expect(findDropdown().attributes('disabled')).toBe('true');
+ } else {
+ expect(findDropdown().attributes('disabled')).toBeUndefined();
+ }
+ });
+ });
+
+ describe('warning message', () => {
+ it.each`
+ selectedIssuablesMock | warningExists | visibility | message | testMessage
+ ${[]} | ${false} | ${'not visible'} | ${'empty'} | ${'nothing is selected'}
+ ${selectedIssuesMocks.tasksOnly} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'only tasks are selected'}
+ ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'only test cases are selected'}
+ ${selectedIssuesMocks.issuesOnly} | ${false} | ${'not visible'} | ${'empty'} | ${'only issues are selected'}
+ ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'tasks and test cases are selected'}
+ ${selectedIssuesMocks.issuesAndTasks} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'issues and tasks are selected'}
+ ${selectedIssuesMocks.issuesAndTestCases} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'issues and test cases are selected'}
+ ${selectedIssuesMocks.issuesTasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'issues and tasks and test cases are selected'}
+ `(
+ 'is $visibility with `$message` message if $testMessage',
+ async ({ selectedIssuablesMock, warningExists, message }) => {
+ createComponent({ selectedIssuables: selectedIssuablesMock });
+
+ await nextTick();
+
+ const alert = findAlert();
+ expect(alert.exists()).toBe(warningExists);
+
+ if (warningExists) {
+ expect(alert.text()).toBe(message);
+ expect(alert.attributes('variant')).toBe('warning');
+ }
+ },
+ );
+ });
+
+ describe('moveIssues method', () => {
+ describe('changes the `Move selected` dropdown loading state', () => {
+ it('keeps loading state to false when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('keeps loading state to false when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to true when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+
+ expect(findDropdown().props('moveInProgress')).toBe(true);
+ });
+
+ it('sets loading state to false when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+
+ it('sets loading state to false when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findDropdown().props('moveInProgress')).toBe(false);
+ });
+ });
+
+ describe('handles events', () => {
+ beforeEach(() => {
+ jest.spyOn(issuableEventHub, '$emit');
+ });
+
+ it('does not emit any event when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not emit any event when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('emits `issuables:bulkMoveStarted` when issues are moving', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveStarted');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when all mutations succeed', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+
+ it('emits `issuables:bulkMoveEnded` when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded');
+ });
+ });
+
+ describe('shows errors', () => {
+ it('does not create flashes or logs errors when no issue is selected', async () => {
+ createComponent();
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('does not create flashes or logs errors when issues are moved without errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('creates a flash and logs errors when a mutation returns errors', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We're mocking two issues so it will log two errors
+ expect(logError).toHaveBeenCalledTimes(2);
+ expect(logError).toHaveBeenNthCalledWith(
+ 1,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+ expect(logError).toHaveBeenNthCalledWith(
+ 2,
+ `Error moving issue. Error message: ${mockMutationErrorMessage}`,
+ );
+
+ // Only one flash is created even if multiple errors are reported
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+
+ it('creates a flash but not logs errors when a mutation is rejected', async () => {
+ createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(logError).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was an error while moving the issues.',
+ });
+ });
+ });
+
+ describe('calls mutations', () => {
+ it('does not call any mutation when no issue is selected', async () => {
+ createComponent({}, resolvedMutationWithoutErrorsMock);
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.testCasesOnly },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('does not call any mutation when only tasks and test cases are selected', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.tasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled();
+ });
+
+ it('calls a mutation for every selected issue skipping tasks', async () => {
+ createComponent(
+ { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
+ resolvedMutationWithoutErrorsMock,
+ );
+ emitMoveIssuablesEvent();
+
+ await waitForPromises();
+
+ // We mock three elements but only two are valid issues since the task is skipped
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenCalledTimes(2);
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(1, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[0].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(2, {
+ moveIssueInput: {
+ projectPath: mockDefaultProps.projectFullPath,
+ iid: selectedIssuesMocks.issuesTasksAndTestCases[1].iid.toString(),
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 1b2935ce5d1..996b2406240 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -40,12 +40,12 @@ describe('RelatedIssuesBlock', () => {
});
it.each`
- issuableType | pathIdSeparator | titleText | helpLinkText | addButtonText
- ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked items'} | ${'Read more about related issues'} | ${'Add a related issue'}
- ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Read more about related epics'} | ${'Add a related epic'}
+ issuableType | pathIdSeparator | titleText | addButtonText
+ ${'issue'} | ${PathIdSeparator.Issue} | ${'Linked items'} | ${'Add a related issue'}
+ ${'epic'} | ${PathIdSeparator.Epic} | ${'Linked epics'} | ${'Add a related epic'}
`(
- 'displays "$titleText" in the header, "$helpLinkText" aria-label for help link, and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
- ({ issuableType, pathIdSeparator, titleText, helpLinkText, addButtonText }) => {
+ 'displays "$titleText" in the header and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
+ ({ issuableType, pathIdSeparator, titleText, addButtonText }) => {
wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator,
@@ -56,9 +56,6 @@ describe('RelatedIssuesBlock', () => {
});
expect(wrapper.find('.card-title').text()).toContain(titleText);
- expect(wrapper.find('[data-testid="help-link"]').attributes('aria-label')).toBe(
- helpLinkText,
- );
expect(findIssueCountBadgeAddButton().attributes('aria-label')).toBe(addButtonText);
},
);
@@ -100,7 +97,7 @@ describe('RelatedIssuesBlock', () => {
slots: { 'header-actions': headerActions },
});
- expect(wrapper.find('[data-testid="custom-button"]').html()).toBe(headerActions);
+ expect(wrapper.findByTestId('custom-button').html()).toBe(headerActions);
});
});
@@ -260,15 +257,30 @@ describe('RelatedIssuesBlock', () => {
});
});
- it('toggle button is disabled when issue has no related items', () => {
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [],
- issuableType: 'issue',
- },
- });
+ describe('empty state', () => {
+ it.each`
+ issuableType | pathIdSeparator | showCategorizedIssues | emptyText | helpLinkText
+ ${'issue'} | ${PathIdSeparator.Issue} | ${false} | ${"Link issues together to show that they're related."} | ${'Learn more about linking issues'}
+ ${'issue'} | ${PathIdSeparator.Issue} | ${true} | ${"Link issues together to show that they're related or that one is blocking others."} | ${'Learn more about linking issues'}
+ ${'incident'} | ${PathIdSeparator.Issue} | ${false} | ${"Link incidents together to show that they're related."} | ${'Learn more about linking issues and incidents'}
+ ${'incident'} | ${PathIdSeparator.Issue} | ${true} | ${"Link incidents together to show that they're related or that one is blocking others."} | ${'Learn more about linking issues and incidents'}
+ ${'epic'} | ${PathIdSeparator.Epic} | ${true} | ${"Link epics together to show that they're related or that one is blocking others."} | ${'Learn more about linking epics'}
+ `(
+ 'displays "$emptyText" in the body and "$helpLinkText" aria-label for help link',
+ ({ issuableType, pathIdSeparator, showCategorizedIssues, emptyText, helpLinkText }) => {
+ wrapper = mountExtended(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator,
+ issuableType,
+ canAdmin: true,
+ helpPath: '/help/user/project/issues/related_issues',
+ showCategorizedIssues,
+ },
+ });
- expect(findToggleButton().props('disabled')).toBe(true);
+ expect(wrapper.findByTestId('related-issues-body').text()).toContain(emptyText);
+ expect(wrapper.findByTestId('help-link').attributes('aria-label')).toBe(helpLinkText);
+ },
+ );
});
});
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 680dbd68493..bedf8bcaf34 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
@@ -201,18 +201,20 @@ describe('RelatedIssuesRoot', () => {
]);
});
- it('displays a message from the backend upon error', async () => {
+ it('passes an error message from the backend upon error', async () => {
const input = '#123';
const message = 'error';
mock.onPost(defaultProps.endpoint).reply(409, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
- expect(createAlert).not.toHaveBeenCalled();
+ expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
+ expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(null);
findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
await waitForPromises();
- expect(createAlert).toHaveBeenCalledWith({ message });
+ expect(findRelatedIssuesBlock().props('hasError')).toBe(true);
+ expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(message);
});
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
new file mode 100644
index 00000000000..3f72396cce6
--- /dev/null
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -0,0 +1,58 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { IssuableStates } from '~/vue_shared/issuable/list/constants';
+
+describe('IssuesDashboardApp component', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ calendarPath: 'calendar/path',
+ emptyStateSvgPath: 'empty-state.svg',
+ isSignedIn: true,
+ rssPath: 'rss/path',
+ };
+
+ const findCalendarButton = () =>
+ wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.calendarButtonText });
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findRssButton = () =>
+ wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText });
+
+ const mountComponent = () => {
+ wrapper = mountExtended(IssuesDashboardApp, { provide: defaultProvide });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders IssuableList component', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ currentTab: IssuableStates.Opened,
+ namespace: 'dashboard',
+ recentSearchesStorageKey: 'issues',
+ searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ tabs: IssuesDashboardApp.IssuableListTabs,
+ });
+ });
+
+ it('renders RSS button link', () => {
+ expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
+ expect(findRssButton().props('icon')).toBe('rss');
+ });
+
+ it('renders calendar button link', () => {
+ expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ expect(findCalendarButton().props('icon')).toBe('calendar');
+ });
+
+ it('renders empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultProvide.emptyStateSvgPath,
+ title: IssuesDashboardApp.i18n.emptyStateTitle,
+ });
+ });
+});
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 5133c02b190..d0c93c896b3 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -33,16 +33,6 @@ import {
CREATED_DESC,
RELATIVE_POSITION,
RELATIVE_POSITION_ASC,
- TOKEN_TYPE_ASSIGNEE,
- TOKEN_TYPE_AUTHOR,
- TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_CONTACT,
- TOKEN_TYPE_LABEL,
- TOKEN_TYPE_MILESTONE,
- TOKEN_TYPE_MY_REACTION,
- TOKEN_TYPE_ORGANIZATION,
- TOKEN_TYPE_RELEASE,
- TOKEN_TYPE_TYPE,
urlSortParams,
} from '~/issues/list/constants';
import eventHub from '~/issues/list/eventhub';
@@ -57,7 +47,19 @@ import {
WORK_ITEM_TYPE_ENUM_TASK,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
} from '~/work_items/constants';
-import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import('~/issuable/bulk_update_sidebar');
import('~/users_select');
@@ -89,7 +91,6 @@ describe('CE IssuesListApp component', () => {
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
- hasMultipleIssueAssigneesFeature: true,
hasScopedLabelsFeature: true,
initialEmail: 'email@example.com',
initialSort: CREATED_DESC,
@@ -131,7 +132,6 @@ describe('CE IssuesListApp component', () => {
const mountComponent = ({
provide = {},
data = {},
- workItems = false,
issuesQueryResponse = mockIssuesQueryResponse,
issuesCountsQueryResponse = mockIssuesCountsQueryResponse,
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
@@ -150,9 +150,6 @@ describe('CE IssuesListApp component', () => {
apolloProvider: createMockApollo(requestHandlers),
router,
provide: {
- glFeatures: {
- workItems,
- },
...defaultProvide,
...provide,
},
@@ -605,17 +602,20 @@ describe('CE IssuesListApp component', () => {
beforeEach(() => {
wrapper = mountComponent({
provide: { hasAnyIssues: false, isSignedIn: false },
+ mountFn: mount,
});
});
it('shows empty state', () => {
expect(findGlEmptyState().props()).toMatchObject({
- description: IssuesListApp.i18n.noIssuesSignedOutDescription,
title: IssuesListApp.i18n.noIssuesSignedOutTitle,
svgPath: defaultProvide.emptyStateSvgPath,
primaryButtonText: IssuesListApp.i18n.noIssuesSignedOutButtonText,
primaryButtonLink: defaultProvide.signInPath,
});
+ expect(findGlEmptyState().text()).toContain(
+ IssuesListApp.i18n.noIssuesSignedOutDescription,
+ );
});
});
});
@@ -1060,45 +1060,23 @@ describe('CE IssuesListApp component', () => {
});
describe('fetching issues', () => {
- describe('when work_items feature flag is disabled', () => {
- beforeEach(() => {
- wrapper = mountComponent({ workItems: false });
- jest.runOnlyPendingTimers();
- });
-
- it('fetches issue, incident, and test case types', () => {
- const types = [
- WORK_ITEM_TYPE_ENUM_ISSUE,
- WORK_ITEM_TYPE_ENUM_INCIDENT,
- WORK_ITEM_TYPE_ENUM_TEST_CASE,
- ];
-
- expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
- expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({ types }),
- );
- });
+ beforeEach(() => {
+ wrapper = mountComponent();
+ jest.runOnlyPendingTimers();
});
- describe('when work_items feature flag is enabled', () => {
- beforeEach(() => {
- wrapper = mountComponent({ workItems: true });
- jest.runOnlyPendingTimers();
- });
-
- it('fetches issue, incident, test case, and task types', () => {
- const types = [
- WORK_ITEM_TYPE_ENUM_ISSUE,
- WORK_ITEM_TYPE_ENUM_INCIDENT,
- WORK_ITEM_TYPE_ENUM_TEST_CASE,
- WORK_ITEM_TYPE_ENUM_TASK,
- ];
-
- expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
- expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
- expect.objectContaining({ types }),
- );
- });
+ it('fetches issue, incident, test case, and task types', () => {
+ const types = [
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ ];
+
+ expect(mockIssuesQueryResponse).toHaveBeenCalledWith(expect.objectContaining({ types }));
+ expect(mockIssuesCountsQueryResponse).toHaveBeenCalledWith(
+ expect.objectContaining({ types }),
+ );
});
});
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 42e9d348b16..62fcbf7aad0 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -1,6 +1,21 @@
import {
+ FILTERED_SEARCH_TERM,
OPERATOR_IS,
OPERATOR_IS_NOT,
+ OPERATOR_OR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_EPIC,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getIssuesQueryResponse = {
@@ -122,6 +137,8 @@ export const locationSearch = [
'assignee_username[]=5',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
+ 'or[assignee_username][]=carl',
+ 'or[assignee_username][]=lenny',
'milestone_title=season+3',
'milestone_title=season+4',
'not[milestone_title]=season+20',
@@ -166,56 +183,58 @@ export const locationSearchWithSpecialValues = [
].join('&');
export const filteredTokens = [
- { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } },
- { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
- { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: '5', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
- { type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
- { type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } },
- { type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } },
- { type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
- { type: 'milestone', value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
- { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } },
- { type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } },
- { type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } },
- { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
- { type: 'release', value: { data: 'v3', operator: OPERATOR_IS } },
- { type: 'release', value: { data: 'v4', operator: OPERATOR_IS } },
- { type: 'release', value: { data: 'v20', operator: OPERATOR_IS_NOT } },
- { type: 'release', value: { data: 'v30', operator: OPERATOR_IS_NOT } },
- { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'feature', operator: OPERATOR_IS } },
- { type: 'type', value: { data: 'bug', operator: OPERATOR_IS_NOT } },
- { type: 'type', value: { data: 'incident', operator: OPERATOR_IS_NOT } },
- { type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
- { type: 'my_reaction_emoji', value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
- { type: 'confidential', value: { data: 'yes', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: '4', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: '12', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } },
- { type: 'iteration', value: { data: '42', operator: OPERATOR_IS_NOT } },
- { type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } },
- { type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
- { type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
- { type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
- { type: 'crm_contact', value: { data: '123', operator: OPERATOR_IS } },
- { type: 'crm_organization', value: { data: '456', operator: OPERATOR_IS } },
- { type: 'filtered-search-term', value: { data: 'find' } },
- { type: 'filtered-search-term', value: { data: 'issues' } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'homer', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lisa', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: '5', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'patty', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'selma', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'carl', operator: OPERATOR_OR } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'lenny', operator: OPERATOR_OR } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 3', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 4', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'season 30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'cartoon', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'issue', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'feature', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '20', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: '42', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_EPIC, value: { data: '12', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_EPIC, value: { data: '34', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: '1', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_IS_NOT } },
+ { type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
+ { type: FILTERED_SEARCH_TERM, value: { data: 'issues' } },
];
export const filteredTokensWithSpecialValues = [
- { type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } },
- { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
- { type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } },
- { type: 'labels', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'release', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'milestone', value: { data: 'Upcoming', operator: OPERATOR_IS } },
- { type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } },
- { type: 'weight', value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: '123', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MY_REACTION, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_ITERATION, value: { data: 'Current', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_RELEASE, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_MILESTONE, value: { data: 'Upcoming', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_EPIC, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_WEIGHT, value: { data: 'None', operator: OPERATOR_IS } },
];
export const apiParams = {
@@ -244,6 +263,9 @@ export const apiParams = {
epicId: '34',
weight: '3',
},
+ or: {
+ assigneeUsernames: ['carl', 'lenny'],
+ },
};
export const apiParamsWithSpecialValues = {
@@ -263,6 +285,7 @@ export const urlParams = {
'not[author_username]': 'marge',
'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
+ 'or[assignee_username][]': ['carl', 'lenny'],
milestone_title: ['season 3', 'season 4'],
'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index cd4d422583b..273ddfdd5d4 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -86,7 +86,7 @@ describe('Description field component', () => {
renderMarkdownPath: '/',
markdownDocsPath: '/',
quickActionsDocsPath: expect.any(String),
- initOnAutofocus: true,
+ autofocus: true,
supportsQuickActions: true,
enableAutocomplete: true,
}),
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index 2e7449974e5..0ce3f75f576 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -52,6 +52,9 @@ describe('Timeline events form', () => {
findMinuteInput().setValue(45);
};
const findTextarea = () => wrapper.findByTestId('input-note');
+ const findCountNumeric = (count) => wrapper.findByText(count);
+ const findCountVerbose = (count) => wrapper.findByText(`${count} characters remaining`);
+ const findCountHint = () => wrapper.findByText(timelineFormI18n.hint);
const submitForm = async () => {
findSubmitButton().vm.$emit('click');
@@ -135,4 +138,31 @@ describe('Timeline events form', () => {
expect(findSubmitAndAddButton().props('disabled')).toBe(false);
});
});
+
+ describe('form character limit', () => {
+ beforeEach(() => {
+ mountComponent({ mountMethod: mountExtended });
+ });
+
+ it('sets a character limit hint', () => {
+ expect(findCountHint().exists()).toBe(true);
+ });
+
+ it('sets a character limit when text is entered', async () => {
+ await findTextarea().setValue('hello');
+
+ expect(findCountNumeric('275').text()).toBe('275');
+ expect(findCountVerbose('275').text()).toBe('275 characters remaining');
+ });
+
+ it('prevents form submission when text is beyond maximum length', async () => {
+ // 281 characters long
+ const longText =
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in volupte';
+ await findTextarea().setValue(longText);
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ expect(findSubmitAndAddButton().props('disabled')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
index cb32ca9d3dc..95eb10118ee 100644
--- a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js
@@ -3,7 +3,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue';
import createStore from '~/jobs/store';
-import job from '../../mock_data';
+import job, { failedJobStatus } from '../../mock_data';
describe('Legacy Sidebar Header', () => {
let store;
@@ -67,6 +67,12 @@ describe('Legacy Sidebar Header', () => {
it('should render the retry button', () => {
expect(findRetryButton().props('href')).toBe(job.retry_path);
});
+
+ it('should have a different label when the job status is passed', () => {
+ expect(findRetryButton().attributes('title')).toBe(
+ LegacySidebarHeader.i18n.runAgainJobButtonLabel,
+ );
+ });
});
describe('when there is no retry path', () => {
@@ -88,4 +94,16 @@ describe('Legacy Sidebar Header', () => {
expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
});
});
+
+ describe('when the job is failed', () => {
+ describe('retry button', () => {
+ it('should have a different label when the job status is failed', () => {
+ createWrapper({ job: { ...job, status: failedJobStatus } });
+
+ expect(findRetryButton().attributes('title')).toBe(
+ LegacySidebarHeader.i18n.retryJobButtonLabel,
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js
index dc1aa67489d..27911eb76eb 100644
--- a/spec/frontend/jobs/components/job/sidebar_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_spec.js
@@ -1,6 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import httpStatus from '~/lib/utils/http_status';
import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue';
import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue';
import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue';
@@ -10,6 +13,7 @@ import createStore from '~/jobs/store';
import job, { jobsInStage } from '../../mock_data';
describe('Sidebar details block', () => {
+ let mock;
let store;
let wrapper;
@@ -18,6 +22,8 @@ describe('Sidebar details block', () => {
const findArtifactsBlock = () => wrapper.findComponent(ArtifactsBlock);
const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
const findTerminalLink = () => wrapper.findByTestId('terminal-link');
+ const findJobStagesDropdown = () => wrapper.findComponent(StagesDropdown);
+ const findJobsContainer = () => wrapper.findComponent(JobsContainer);
const createWrapper = (props) => {
store = createStore();
@@ -35,6 +41,13 @@ describe('Sidebar details block', () => {
);
};
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet().reply(httpStatus.OK, {
+ name: job.stage,
+ });
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -110,31 +123,72 @@ describe('Sidebar details block', () => {
describe('stages dropdown', () => {
beforeEach(() => {
createWrapper();
- return store.dispatch('receiveJobSuccess', { ...job, stage: 'aStage' });
+ return store.dispatch('receiveJobSuccess', job);
});
describe('with stages', () => {
it('renders value provided as selectedStage as selected', () => {
- expect(wrapper.findComponent(StagesDropdown).props('selectedStage')).toBe('aStage');
+ expect(findJobStagesDropdown().props('selectedStage')).toBe(job.stage);
});
});
describe('without jobs for stages', () => {
- beforeEach(() => store.dispatch('receiveJobSuccess', job));
-
it('does not render jobs container', () => {
- expect(wrapper.findComponent(JobsContainer).exists()).toBe(false);
+ expect(findJobsContainer().exists()).toBe(false);
});
});
describe('with jobs for stages', () => {
+ beforeEach(() => {
+ return store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
+ });
+
+ it('renders list of jobs', async () => {
+ expect(findJobsContainer().exists()).toBe(true);
+ });
+ });
+
+ describe('when job data changes', () => {
+ const stageArg = job.pipeline.details.stages.find((stage) => stage.name === job.stage);
+
beforeEach(async () => {
- await store.dispatch('receiveJobSuccess', job);
- await store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
+ jest.spyOn(store, 'dispatch');
});
- it('renders list of jobs', () => {
- expect(wrapper.findComponent(JobsContainer).exists()).toBe(true);
+ describe('and the job stage is currently selected', () => {
+ describe('when the status changed', () => {
+ it('refetch the jobs list for the stage', async () => {
+ await store.dispatch('receiveJobSuccess', { ...job, status: 'new' });
+
+ expect(store.dispatch).toHaveBeenNthCalledWith(2, 'fetchJobsForStage', { ...stageArg });
+ });
+ });
+
+ describe('when the status did not change', () => {
+ it('does not refetch the jobs list for the stage', async () => {
+ await store.dispatch('receiveJobSuccess', { ...job });
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenNthCalledWith(1, 'receiveJobSuccess', {
+ ...job,
+ });
+ });
+ });
+ });
+
+ describe('and the job stage is not currently selected', () => {
+ it('does not refetch the jobs list for the stage', async () => {
+ // Setting stage to `random` on the job means that we are looking
+ // at `build` stage currently, but the job we are seeing in the logs
+ // belong to `random`, so we shouldn't have to refetch
+ await store.dispatch('receiveJobSuccess', { ...job, stage: 'random' });
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenNthCalledWith(1, 'receiveJobSuccess', {
+ ...job,
+ stage: 'random',
+ });
+ });
});
});
});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index bf238b2e39a..a7fe6d5a626 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -925,6 +925,7 @@ export default {
locked: false,
},
name: 'test',
+ stage: 'build',
build_path: '/root/ci-mock/-/jobs/4757',
retry_path: '/root/ci-mock/-/jobs/4757/retry',
cancel_path: '/root/ci-mock/-/jobs/4757/cancel',
@@ -1085,6 +1086,29 @@ export default {
has_trace: true,
};
+export const failedJobStatus = {
+ icon: 'status_warning',
+ text: 'failed',
+ label: 'failed (allowed to fail)',
+ group: 'failed-with-warnings',
+ tooltip: 'failed - (unknown failure) (allowed to fail)',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab-shell/-/jobs/454',
+ illustration: {
+ image: 'illustrations/skipped-job_empty.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/gitlab-org/gitlab-shell/-/jobs/454/retry',
+ method: 'post',
+ },
+};
+
export const jobsInStage = {
name: 'build',
title: 'build: running',
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index a0140d1d8a8..947c38c8ae8 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1016,45 +1016,6 @@ describe('common_utils', () => {
});
});
- describe('searchBy', () => {
- const searchSpace = {
- iid: 1,
- reference: '&1',
- title: 'Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate.',
- url: '/groups/gitlab-org/-/epics/1',
- };
-
- it('returns null when `query` or `searchSpace` params are empty/undefined', () => {
- expect(commonUtils.searchBy('omnis', null)).toBeNull();
- expect(commonUtils.searchBy('', searchSpace)).toBeNull();
- expect(commonUtils.searchBy()).toBeNull();
- });
-
- it('returns object with matching props based on `query` & `searchSpace` params', () => {
- // String `omnis` is found only in `title` prop so return just that
- expect(commonUtils.searchBy('omnis', searchSpace)).toEqual(
- expect.objectContaining({
- title: searchSpace.title,
- }),
- );
-
- // String `1` is found in both `iid` and `reference` props so return both
- expect(commonUtils.searchBy('1', searchSpace)).toEqual(
- expect.objectContaining({
- iid: searchSpace.iid,
- reference: searchSpace.reference,
- }),
- );
-
- // String `/epics/1` is found in `url` prop so return just that
- expect(commonUtils.searchBy('/epics/1', searchSpace)).toEqual(
- expect.objectContaining({
- url: searchSpace.url,
- }),
- );
- });
- });
-
describe('isScopedLabel', () => {
it('returns true when `::` is present in title', () => {
expect(commonUtils.isScopedLabel({ title: 'foo::bar' })).toBe(true);
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
new file mode 100644
index 00000000000..142c76f7bc0
--- /dev/null
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
@@ -0,0 +1,103 @@
+import Vue, { nextTick } from 'vue';
+import { createWrapper } from '@vue/test-utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
+
+const originalMount = Vue.prototype.$mount;
+
+describe('confirmAction', () => {
+ let modalWrapper;
+ let confirActionPromise;
+ let modal;
+
+ const findConfirmModal = () => modalWrapper.findComponent(ConfirmModal);
+ const renderRootComponent = async (message, opts) => {
+ confirActionPromise = confirmAction(message, opts);
+ // We have to wait for two ticks here.
+ // The first one is to wait for rendering of the root component
+ // The second one to wait for rendering of the dynamically
+ // loaded confirm-modal component
+ await nextTick();
+ await nextTick();
+ modal = findConfirmModal();
+ };
+ const mockMount = (vm, el) => {
+ originalMount.call(vm, el);
+ modalWrapper = createWrapper(vm);
+ return vm;
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="component"></div>');
+ const el = document.getElementById('component');
+ // We mock the implementation only once to make sure that we mock
+ // it only for the root component in confirm_action.
+ // Mounting other components (like confirm-modal) should not be affected with
+ // this mock
+ jest.spyOn(Vue.prototype, '$mount').mockImplementationOnce(function mock() {
+ return mockMount(this, el);
+ });
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ Vue.prototype.$mount.mockRestore();
+ modalWrapper?.destroy();
+ modalWrapper = null;
+ modal?.destroy();
+ modal = null;
+ });
+
+ it('creats a ConfirmModal with message as slot', async () => {
+ const message = 'Bonjour le monde!';
+ await renderRootComponent(message);
+
+ expect(modal.vm.$slots.default[0].text).toBe(message);
+ });
+
+ it('creats a ConfirmModal with props', async () => {
+ const options = {
+ primaryBtnText: 'primaryBtnText',
+ primaryBtnVariant: 'info',
+ secondaryBtnText: 'secondaryBtnText',
+ secondaryBtnVariant: 'success',
+ cancelBtnText: 'cancelBtnText',
+ cancelBtnVariant: 'danger',
+ modalHtmlMessage: '<strong>Hello</strong>',
+ title: 'title',
+ hideCancel: true,
+ };
+ await renderRootComponent('', options);
+ expect(modal.props()).toEqual(
+ expect.objectContaining({
+ primaryText: options.primaryBtnText,
+ primaryVariant: options.primaryBtnVariant,
+ secondaryText: options.secondaryBtnText,
+ secondaryVariant: options.secondaryBtnVariant,
+ cancelText: options.cancelBtnText,
+ cancelVariant: options.cancelBtnVariant,
+ modalHtmlMessage: options.modalHtmlMessage,
+ title: options.title,
+ hideCancel: options.hideCancel,
+ }),
+ );
+ });
+
+ it('resolves promise when modal emit `closed`', async () => {
+ await renderRootComponent('');
+
+ modal.vm.$emit('closed');
+
+ await expect(confirActionPromise).resolves.toBe(false);
+ });
+
+ it('confirms when modal emit `confirmed` before `closed`', async () => {
+ await renderRootComponent('');
+
+ modal.vm.$emit('confirmed');
+ modal.vm.$emit('closed');
+
+ await expect(confirActionPromise).resolves.toBe(true);
+ });
+});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js
new file mode 100644
index 00000000000..6966c79b232
--- /dev/null
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal_spec.js
@@ -0,0 +1,80 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { confirmViaGlModal } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_action');
+
+describe('confirmViaGlModal', () => {
+ let el;
+
+ afterEach(() => {
+ el = undefined;
+ resetHTMLFixture();
+ jest.resetAllMocks();
+ });
+
+ const createElement = (html) => {
+ setHTMLFixture(html);
+ return document.body.firstChild;
+ };
+
+ it('returns confirmAction result', async () => {
+ confirmAction.mockReturnValue(Promise.resolve(true));
+ el = createElement(`<div/>`);
+
+ await expect(confirmViaGlModal('', el)).resolves.toBe(true);
+ });
+
+ it('calls confirmAction with message', () => {
+ el = createElement(`<div/>`);
+
+ confirmViaGlModal('message', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('message', {});
+ });
+
+ it.each(['gl-sr-only', 'sr-only'])(
+ `uses slot.%s contentText as primaryBtnText`,
+ (srOnlyClass) => {
+ el = createElement(
+ `<a href="#"><span class="${srOnlyClass}">Delete merge request</span></a>`,
+ );
+
+ confirmViaGlModal('', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('', {
+ primaryBtnText: 'Delete merge request',
+ });
+ },
+ );
+
+ it('uses `aria-label` value as `primaryBtnText`', () => {
+ el = createElement(`<a aria-label="Delete merge request" href="#"></a>`);
+
+ confirmViaGlModal('', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('', {
+ primaryBtnText: 'Delete merge request',
+ });
+ });
+
+ it.each([
+ ['title', 'title', 'Delete?'],
+ ['confirm-btn-variant', `primaryBtnVariant`, 'danger'],
+ ])('uses data-%s value as confirmAction config', (dataKey, configKey, value) => {
+ el = createElement(`<a data-${dataKey}="${value}" href="#"></a>`);
+
+ confirmViaGlModal('message', el);
+
+ expect(confirmAction).toHaveBeenCalledWith('message', { [configKey]: value });
+ });
+
+ it('uses message as modalHtmlMessage value when data-is-html-message is true', () => {
+ el = createElement(`<a data-is-html-message="true" href="#"></a>`);
+ const message = 'Hola mundo!';
+
+ confirmViaGlModal(message, el);
+
+ expect(confirmAction).toHaveBeenCalledWith(message, { modalHtmlMessage: message });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
index 59b3b4c02df..055d57d6ada 100644
--- a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
@@ -1,4 +1,9 @@
-import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
+import {
+ getDateWithUTC,
+ newDateAsLocaleTime,
+ nSecondsAfter,
+ nSecondsBefore,
+} from '~/lib/utils/datetime/date_calculation_utility';
describe('newDateAsLocaleTime', () => {
it.each`
@@ -31,3 +36,33 @@ describe('getDateWithUTC', () => {
expect(getDateWithUTC(date)).toEqual(expected);
});
});
+
+describe('nSecondsAfter', () => {
+ const start = new Date('2022-03-22T01:23:45.678Z');
+ it.each`
+ date | seconds | expected
+ ${start} | ${0} | ${start}
+ ${start} | ${1} | ${new Date('2022-03-22T01:23:46.678Z')}
+ ${start} | ${5} | ${new Date('2022-03-22T01:23:50.678Z')}
+ ${start} | ${60} | ${new Date('2022-03-22T01:24:45.678Z')}
+ ${start} | ${3600} | ${new Date('2022-03-22T02:23:45.678Z')}
+ ${start} | ${86400} | ${new Date('2022-03-23T01:23:45.678Z')}
+ `('returns $expected given $string', ({ date, seconds, expected }) => {
+ expect(nSecondsAfter(date, seconds)).toEqual(expected);
+ });
+});
+
+describe('nSecondsBefore', () => {
+ const start = new Date('2022-03-22T01:23:45.678Z');
+ it.each`
+ date | seconds | expected
+ ${start} | ${0} | ${start}
+ ${start} | ${1} | ${new Date('2022-03-22T01:23:44.678Z')}
+ ${start} | ${5} | ${new Date('2022-03-22T01:23:40.678Z')}
+ ${start} | ${60} | ${new Date('2022-03-22T01:22:45.678Z')}
+ ${start} | ${3600} | ${new Date('2022-03-22T00:23:45.678Z')}
+ ${start} | ${86400} | ${new Date('2022-03-21T01:23:45.678Z')}
+ `('returns $expected given $string', ({ date, seconds, expected }) => {
+ expect(nSecondsBefore(date, seconds)).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js
index b537e6b2bf8..d6bac935970 100644
--- a/spec/frontend/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/lib/utils/dom_utils_spec.js
@@ -1,8 +1,10 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
import {
addClassIfElementExists,
canScrollUp,
canScrollDown,
+ getContentWrapperHeight,
parseBooleanDataAttributes,
isElementVisible,
getParents,
@@ -235,4 +237,30 @@ describe('DOM Utils', () => {
expect(div.getAttribute('title')).toBe('another test');
});
});
+
+ describe('getContentWrapperHeight', () => {
+ const fixture = `
+ <div>
+ <div class="content-wrapper">
+ <div class="content"></div>
+ </div>
+ </div>
+ `;
+
+ beforeEach(() => {
+ setHTMLFixture(fixture);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('returns the height of an element that exists', () => {
+ expect(getContentWrapperHeight('.content-wrapper')).toBe('0px');
+ });
+
+ it('returns an empty string for a class that does not exist', () => {
+ expect(getContentWrapperHeight('.does-not-exist')).toBe('');
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/unit_format/index_spec.js b/spec/frontend/lib/utils/unit_format/index_spec.js
index dc9d6ece48e..057d7aded02 100644
--- a/spec/frontend/lib/utils/unit_format/index_spec.js
+++ b/spec/frontend/lib/utils/unit_format/index_spec.js
@@ -2,6 +2,7 @@ import {
number,
percent,
percentHundred,
+ days,
seconds,
milliseconds,
decimalBytes,
@@ -72,6 +73,11 @@ describe('unit_format', () => {
expect(percentHundred(1000)).toBe('1,000%');
});
+ it('days', () => {
+ expect(days(1)).toBe('1d');
+ expect(days(1, undefined, { unitSeparator: '/' })).toBe('1/d');
+ });
+
it('seconds', () => {
expect(seconds(1)).toBe('1s');
expect(seconds(1, undefined, { unitSeparator: ' ' })).toBe('1 s');
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index 3dac47974e7..df5c884f42e 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -24,86 +24,52 @@ describe('AccessRequestActionButtons', () => {
wrapper.destroy();
});
- describe('when user has `canRemove` permissions', () => {
- beforeEach(() => {
- createComponent({
- permissions: {
- canRemove: true,
- },
- });
- });
+ it('renders remove member button', () => {
+ createComponent();
- it('renders remove member button', () => {
- expect(findRemoveMemberButton().exists()).toBe(true);
- });
-
- it('sets props correctly', () => {
- expect(findRemoveMemberButton().props()).toMatchObject({
- memberId: member.id,
- title: 'Deny access',
- isAccessRequest: true,
- isInvite: false,
- icon: 'close',
- });
- });
-
- describe('when member is the current user', () => {
- it('sets `message` prop correctly', () => {
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
- );
- });
- });
+ expect(findRemoveMemberButton().exists()).toBe(true);
+ });
- describe('when member is not the current user', () => {
- it('sets `message` prop correctly', () => {
- createComponent({
- isCurrentUser: false,
- permissions: {
- canRemove: true,
- },
- });
+ it('sets props correctly on remove member button', () => {
+ createComponent();
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
- );
- });
+ expect(findRemoveMemberButton().props()).toMatchObject({
+ memberId: member.id,
+ title: 'Deny access',
+ isAccessRequest: true,
+ isInvite: false,
+ icon: 'close',
});
});
- describe('when user does not have `canRemove` permissions', () => {
- it('does not render remove member button', () => {
- createComponent({
- permissions: {
- canRemove: false,
- },
- });
+ describe('when member is the current user', () => {
+ it('sets `message` prop correctly', () => {
+ createComponent();
- expect(findRemoveMemberButton().exists()).toBe(false);
+ expect(findRemoveMemberButton().props('message')).toBe(
+ `Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
+ );
});
});
- describe('when user has `canUpdate` permissions', () => {
- it('renders the approve button', () => {
+ describe('when member is not the current user', () => {
+ it('sets `message` prop correctly', () => {
createComponent({
+ isCurrentUser: false,
permissions: {
- canUpdate: true,
+ canRemove: true,
},
});
- expect(findApproveButton().exists()).toBe(true);
+ expect(findRemoveMemberButton().props('message')).toBe(
+ `Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
+ );
});
});
- describe('when user does not have `canUpdate` permissions', () => {
- it('does not render the approve button', () => {
- createComponent({
- permissions: {
- canUpdate: false,
- },
- });
+ it('renders the approve button', () => {
+ createComponent();
- expect(findApproveButton().exists()).toBe(false);
- });
+ expect(findApproveButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 1354b938d77..77af5e7293e 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -81,6 +81,7 @@ describe('MembersTabs', () => {
stubs: ['members-app'],
provide: {
canManageMembers: true,
+ canManageAccessRequests: true,
canExportMembers: true,
exportCsvPath: '',
...provide,
@@ -181,7 +182,9 @@ describe('MembersTabs', () => {
describe('when `canManageMembers` is `false`', () => {
it('shows all tabs except `Invited` and `Access requests`', async () => {
- await createComponent({ provide: { canManageMembers: false } });
+ await createComponent({
+ provide: { canManageMembers: false, canManageAccessRequests: false },
+ });
expect(findTabByText('Members')).not.toBeUndefined();
expect(findTabByText('Groups')).not.toBeUndefined();
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap
new file mode 100644
index 00000000000..2eba8869535
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/experiment_spec.js.snap
@@ -0,0 +1,223 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShowExperiment with candidates renders correctly 1`] = `
+<div>
+ <div
+ class="gl-alert gl-alert-warning"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16 gl-alert-icon"
+ data-testid="warning-icon"
+ role="img"
+ >
+ <use
+ href="#warning"
+ />
+ </svg>
+
+ <div
+ aria-live="assertive"
+ class="gl-alert-content"
+ role="alert"
+ >
+ <h2
+ class="gl-alert-title"
+ >
+ Machine Learning Experiment Tracking is in Incubating Phase
+ </h2>
+
+ <div
+ class="gl-alert-body"
+ >
+
+ GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited
+
+ <a
+ class="gl-link"
+ href="https://about.gitlab.com/handbook/engineering/incubation/"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ Learn More
+ </a>
+ </div>
+
+ <div
+ class="gl-alert-actions"
+ >
+ <a
+ class="btn gl-alert-action btn-confirm btn-md gl-button"
+ href="https://gitlab.com/groups/gitlab-org/-/epics/8560"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Feedback and Updates
+
+ </span>
+ </a>
+ </div>
+ </div>
+
+ <button
+ aria-label="Dismiss"
+ class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="close-icon"
+ role="img"
+ >
+ <use
+ href="#close"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+
+ <h3>
+
+ Experiment Candidates
+
+ </h3>
+
+ <table
+ aria-busy="false"
+ aria-colcount="4"
+ class="table b-table gl-table gl-mt-0!"
+ role="table"
+ >
+ <!---->
+ <!---->
+ <thead
+ class=""
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <th
+ aria-colindex="1"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ L1 Ratio
+ </div>
+ </th>
+ <th
+ aria-colindex="2"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Rmse
+ </div>
+ </th>
+ <th
+ aria-colindex="3"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Auc
+ </div>
+ </th>
+ <th
+ aria-colindex="4"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Mae
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ 0.4
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+ 1
+ </td>
+ <td
+ aria-colindex="3"
+ class=""
+ role="cell"
+ />
+ <td
+ aria-colindex="4"
+ class=""
+ role="cell"
+ />
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ 0.5
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ />
+ <td
+ aria-colindex="3"
+ class=""
+ role="cell"
+ >
+ 0.3
+ </td>
+ <td
+ aria-colindex="4"
+ class=""
+ role="cell"
+ />
+ </tr>
+ <!---->
+ <!---->
+ </tbody>
+ <!---->
+ </table>
+</div>
+`;
diff --git a/spec/frontend/ml/experiment_tracking/components/experiment_spec.js b/spec/frontend/ml/experiment_tracking/components/experiment_spec.js
new file mode 100644
index 00000000000..af722d77532
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/experiment_spec.js
@@ -0,0 +1,44 @@
+import { GlAlert } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
+
+describe('ShowExperiment', () => {
+ let wrapper;
+
+ const createWrapper = (candidates = [], metricNames = [], paramNames = []) => {
+ return mountExtended(ShowExperiment, { provide: { candidates, metricNames, paramNames } });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const findEmptyState = () => wrapper.findByText('This Experiment has no logged Candidates');
+
+ it('shows incubation warning', () => {
+ wrapper = createWrapper();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('no candidates', () => {
+ it('shows empty state', () => {
+ wrapper = createWrapper();
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+
+ describe('with candidates', () => {
+ it('renders correctly', () => {
+ wrapper = createWrapper(
+ [
+ { rmse: 1, l1_ratio: 0.4 },
+ { auc: 0.3, l1_ratio: 0.5 },
+ ],
+ ['rmse', 'auc', 'mae'],
+ ['l1_ratio'],
+ );
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
new file mode 100644
index 00000000000..e07a4ed816b
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
@@ -0,0 +1,27 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import IncubationAlert from '~/ml/experiment_tracking/components/incubation_alert.vue';
+
+describe('IncubationAlert', () => {
+ let wrapper;
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ wrapper = mount(IncubationAlert);
+ });
+
+ it('displays link to issue', () => {
+ expect(findButton().attributes().href).toBe(
+ 'https://gitlab.com/groups/gitlab-org/-/epics/8560',
+ );
+ });
+
+ it('is removed if dismissed', async () => {
+ await wrapper.find('[aria-label="Dismiss"]').trigger('click');
+
+ expect(findAlert().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index c757b55faf4..a7776bd5b69 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -5,20 +5,22 @@ import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json';
import basicJson from 'test_fixtures/blob/notebook/basic.json';
import mathJson from 'test_fixtures/blob/notebook/math.json';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
const Component = Vue.extend(MarkdownComponent);
window.katex = katex;
-function buildCellComponent(cell, relativePath = '') {
+function buildCellComponent(cell, relativePath = '', hidePrompt) {
return mount(Component, {
propsData: {
cell,
+ hidePrompt,
},
provide: {
relativeRawPath: relativePath,
},
- }).vm;
+ });
}
function buildMarkdownComponent(markdownContent, relativePath = '') {
@@ -33,7 +35,7 @@ function buildMarkdownComponent(markdownContent, relativePath = '') {
}
describe('Markdown component', () => {
- let vm;
+ let wrapper;
let cell;
let json;
@@ -43,21 +45,30 @@ describe('Markdown component', () => {
// eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
- vm = buildCellComponent(cell);
+ wrapper = buildCellComponent(cell);
await nextTick();
});
- it('does not render prompt', () => {
- expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ const findPrompt = () => wrapper.findComponent(Prompt);
+
+ it('renders a prompt by default', () => {
+ expect(findPrompt().exists()).toBe(true);
+ });
+
+ it('does not render a prompt if hidePrompt is true', () => {
+ wrapper = buildCellComponent(cell, '', true);
+ expect(findPrompt().exists()).toBe(false);
});
it('does not render the markdown text', () => {
- expect(vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(cell.source.join(''));
+ expect(wrapper.vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(
+ cell.source.join(''),
+ );
});
it('renders the markdown HTML', () => {
- expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('.markdown h1')).not.toBeNull();
});
it('sanitizes Markdown output', async () => {
@@ -68,11 +79,11 @@ describe('Markdown component', () => {
});
await nextTick();
- expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
+ expect(wrapper.vm.$el.querySelector('a').getAttribute('href')).toBeNull();
});
it('sanitizes HTML', async () => {
- const findLink = () => vm.$el.querySelector('.xss-link');
+ const findLink = () => wrapper.vm.$el.querySelector('.xss-link');
Object.assign(cell, {
source: ['<a href="test.js" data-remote=true data-type="script" class="xss-link">XSS</a>\n'],
});
@@ -97,11 +108,11 @@ describe('Markdown component', () => {
["for embedded images, it doesn't", '![](data:image/jpeg;base64)\n', 'src="data:'],
["for images urls, it doesn't", '![](http://image.png)\n', 'src="http:'],
])('%s', async ([testMd, mustContain]) => {
- vm = buildMarkdownComponent([testMd], '/raw/');
+ wrapper = buildMarkdownComponent([testMd], '/raw/');
await nextTick();
- expect(vm.$el.innerHTML).toContain(mustContain);
+ expect(wrapper.vm.$el.innerHTML).toContain(mustContain);
});
});
@@ -111,13 +122,13 @@ describe('Markdown component', () => {
});
it('renders images and text', async () => {
- vm = buildCellComponent(json.cells[0]);
+ wrapper = buildCellComponent(json.cells[0]);
await nextTick();
- const images = vm.$el.querySelectorAll('img');
+ const images = wrapper.vm.$el.querySelectorAll('img');
expect(images.length).toBe(5);
- const columns = vm.$el.querySelectorAll('td');
+ const columns = wrapper.vm.$el.querySelectorAll('td');
expect(columns.length).toBe(6);
expect(columns[0].textContent).toEqual('Hello ');
@@ -141,81 +152,93 @@ describe('Markdown component', () => {
});
it('renders multi-line katex', async () => {
- vm = buildCellComponent(json.cells[0]);
+ wrapper = buildCellComponent(json.cells[0]);
await nextTick();
- expect(vm.$el.querySelector('.katex')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('.katex')).not.toBeNull();
});
it('renders inline katex', async () => {
- vm = buildCellComponent(json.cells[1]);
+ wrapper = buildCellComponent(json.cells[1]);
await nextTick();
- expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
+ expect(wrapper.vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
});
it('renders multiple inline katex', async () => {
- vm = buildCellComponent(json.cells[1]);
+ wrapper = buildCellComponent(json.cells[1]);
await nextTick();
- expect(vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
+ expect(wrapper.vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4);
});
it('output cell in case of katex error', async () => {
- vm = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
+ wrapper = buildMarkdownComponent(['Some invalid $a & b$ inline formula $b & c$\n', '\n']);
await nextTick();
// expect one paragraph with no katex formula in it
- expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(0);
+ expect(wrapper.vm.$el.querySelectorAll('p')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p .katex')).toHaveLength(0);
});
it('output cell and render remaining formula in case of katex error', async () => {
- vm = buildMarkdownComponent([
+ wrapper = buildMarkdownComponent([
'An invalid $a & b$ inline formula and a vaild one $b = c$\n',
'\n',
]);
await nextTick();
// expect one paragraph with no katex formula in it
- expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('p .katex')).toHaveLength(1);
});
it('renders math formula in list object', async () => {
- vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
+ wrapper = buildMarkdownComponent([
+ "- list with inline $a=2$ inline formula $a' + b = c$\n",
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it("renders math formula with tick ' in it", async () => {
- vm = buildMarkdownComponent(["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n']);
+ wrapper = buildMarkdownComponent([
+ "- list with inline $a=2$ inline formula $a' + b = c$\n",
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it('renders math formula with less-than-operator < in it', async () => {
- vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b < c$\n', '\n']);
+ wrapper = buildMarkdownComponent([
+ '- list with inline $a=2$ inline formula $a + b < c$\n',
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
it('renders math formula with greater-than-operator > in it', async () => {
- vm = buildMarkdownComponent(['- list with inline $a=2$ inline formula $a + b > c$\n', '\n']);
+ wrapper = buildMarkdownComponent([
+ '- list with inline $a=2$ inline formula $a + b > c$\n',
+ '\n',
+ ]);
await nextTick();
// expect one list with a katex formula in it
- expect(vm.$el.querySelectorAll('li')).toHaveLength(1);
- expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
+ expect(wrapper.vm.$el.querySelectorAll('li')).toHaveLength(1);
+ expect(wrapper.vm.$el.querySelectorAll('li .katex')).toHaveLength(2);
});
});
});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 8bf049235a9..585cbb68eeb 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -1,12 +1,15 @@
import { mount } from '@vue/test-utils';
import json from 'test_fixtures/blob/notebook/basic.json';
import Output from '~/notebook/cells/output/index.vue';
+import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
+import { relativeRawPath, markdownCellContent } from '../../mock_data';
describe('Output component', () => {
let wrapper;
const createComponent = (output) => {
wrapper = mount(Output, {
+ provide: { relativeRawPath },
propsData: {
outputs: [].concat(output),
count: 1,
@@ -95,6 +98,17 @@ describe('Output component', () => {
});
});
+ describe('Markdown output', () => {
+ beforeEach(() => {
+ const markdownType = { data: { 'text/markdown': markdownCellContent } };
+ createComponent(markdownType);
+ });
+
+ it('renders a markdown component', () => {
+ expect(wrapper.findComponent(MarkdownOutput).props('rawCode')).toBe(markdownCellContent);
+ });
+ });
+
describe('default to plain text', () => {
beforeEach(() => {
const unknownType = json.cells[6];
diff --git a/spec/frontend/notebook/cells/output/markdown_spec.js b/spec/frontend/notebook/cells/output/markdown_spec.js
new file mode 100644
index 00000000000..e3490ed3bea
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/markdown_spec.js
@@ -0,0 +1,44 @@
+import { mount } from '@vue/test-utils';
+import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
+import Markdown from '~/notebook/cells/markdown.vue';
+import { relativeRawPath, markdownCellContent } from '../../mock_data';
+
+describe('markdown output cell', () => {
+ let wrapper;
+
+ const createComponent = ({ count = 0, index = 0 } = {}) => {
+ wrapper = mount(MarkdownOutput, {
+ provide: { relativeRawPath },
+ propsData: {
+ rawCode: markdownCellContent,
+ count,
+ index,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findPrompt = () => wrapper.findComponent(Prompt);
+ const findMarkdown = () => wrapper.findComponent(Markdown);
+
+ it.each`
+ index | count | showOutput
+ ${0} | ${1} | ${true}
+ ${1} | ${2} | ${false}
+ ${2} | ${3} | ${false}
+ `('renders a prompt', ({ index, count, showOutput }) => {
+ createComponent({ count, index });
+ expect(findPrompt().props()).toMatchObject({ count, showOutput, type: 'Out' });
+ });
+
+ it('renders a Markdown component', () => {
+ expect(findMarkdown().props()).toMatchObject({
+ cell: { source: markdownCellContent },
+ hidePrompt: true,
+ });
+ });
+});
diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js
new file mode 100644
index 00000000000..b1419e1256f
--- /dev/null
+++ b/spec/frontend/notebook/mock_data.js
@@ -0,0 +1,2 @@
+export const relativeRawPath = '/test';
+export const markdownCellContent = ['# Test'];
diff --git a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
index bc29903d4bf..a4611149432 100644
--- a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
+++ b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
@@ -2,7 +2,7 @@
exports[`note_app when sort direction is asc shows skeleton notes after the loaded discussions 1`] = `
"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
- <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\"></noteable-discussion-stub>
+ <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\" shouldscrolltonote=\\"true\\"></noteable-discussion-stub>
<skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
<!---->
</ul>"
@@ -11,7 +11,7 @@ exports[`note_app when sort direction is asc shows skeleton notes after the load
exports[`note_app when sort direction is desc shows skeleton notes before the loaded discussions 1`] = `
"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
<skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
- <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\"></noteable-discussion-stub>
+ <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\" shouldscrolltonote=\\"true\\"></noteable-discussion-stub>
<!---->
</ul>"
`;
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index cbe11c20798..c7420ca9c48 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -5,6 +5,8 @@ import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import noteActions from '~/notes/components/note_actions.vue';
+import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
+import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
import createStore from '~/notes/stores';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { userDataMock } from '../mock_data';
@@ -18,6 +20,23 @@ describe('noteActions', () => {
const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx);
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
+ const findTimelineButton = () => wrapper.findComponent(TimelineEventButton);
+
+ const setupStoreForIncidentTimelineEvents = ({
+ userCanAdd,
+ noteableType,
+ isPromotionInProgress = true,
+ }) => {
+ store.dispatch('setUserData', {
+ ...userDataMock,
+ can_add_timeline_events: userCanAdd,
+ });
+ store.state.noteableData = {
+ ...store.state.noteableData,
+ type: noteableType,
+ };
+ store.state.isPromoteCommentToTimelineEventInProgress = isPromotionInProgress;
+ };
const mountNoteActions = (propsData, computed) => {
return mount(noteActions, {
@@ -238,7 +257,8 @@ describe('noteActions', () => {
describe('user is not logged in', () => {
beforeEach(() => {
- store.dispatch('setUserData', {});
+ // userData can be null https://gitlab.com/gitlab-org/gitlab/-/issues/379375
+ store.dispatch('setUserData', null);
wrapper = mountNoteActions({
...props,
canDelete: false,
@@ -301,4 +321,56 @@ describe('noteActions', () => {
expect(resolveButton.attributes('title')).toBe('Thread stays unresolved');
});
});
+
+ describe('timeline event button', () => {
+ // why: We are working with an integrated store, so let's imply the getter is used
+ describe.each`
+ desc | userCanAdd | noteableType | exists
+ ${'default'} | ${true} | ${NOTEABLE_TYPE_MAPPING.Incident} | ${true}
+ ${'when cannot add incident timeline event'} | ${false} | ${NOTEABLE_TYPE_MAPPING.Incident} | ${false}
+ ${'when is not incident'} | ${true} | ${NOTEABLE_TYPE_MAPPING.MergeRequest} | ${false}
+ `('$desc', ({ userCanAdd, noteableType, exists }) => {
+ beforeEach(() => {
+ setupStoreForIncidentTimelineEvents({
+ userCanAdd,
+ noteableType,
+ });
+
+ wrapper = mountNoteActions({ ...props });
+ });
+
+ it(`handles rendering of timeline button (exists=${exists})`, () => {
+ expect(findTimelineButton().exists()).toBe(exists);
+ });
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ setupStoreForIncidentTimelineEvents({
+ userCanAdd: true,
+ noteableType: NOTEABLE_TYPE_MAPPING.Incident,
+ });
+
+ wrapper = mountNoteActions({ ...props });
+ });
+
+ it('should render timeline-event-button', () => {
+ expect(findTimelineButton().props()).toEqual({
+ noteId: props.noteId,
+ isPromotionInProgress: true,
+ });
+ });
+
+ it('when timeline-event-button emits click-promote-comment-to-event, dispatches action', () => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ expect(store.dispatch).not.toHaveBeenCalled();
+
+ findTimelineButton().vm.$emit('click-promote-comment-to-event');
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenCalledWith('promoteCommentToTimelineEvent');
+ });
+ });
+ });
});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index b870cda2a24..56c22b09e1b 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -18,7 +18,7 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.findComponent({ ref: 'actionText' });
const findTimestampLink = () => wrapper.findComponent({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.findComponent({ ref: 'noteTimestamp' });
- const findInternalNoteIndicator = () => wrapper.findByTestId('internalNoteIndicator');
+ const findInternalNoteIndicator = () => wrapper.findByTestId('internal-note-indicator');
const findSpinner = () => wrapper.findComponent({ ref: 'spinner' });
const statusHtml =
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 45625d0a23f..81e4ed3ebe7 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -34,7 +34,7 @@ describe('Discussion navigation mixin', () => {
beforeEach(() => {
setHTMLFixture(
- `<div class="notes">
+ `<div class="tab-pane notes">
${[...'abcde']
.map(
(id) =>
diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js
index e03fa854e54..1514602d424 100644
--- a/spec/frontend/notes/stores/getters_spec.js
+++ b/spec/frontend/notes/stores/getters_spec.js
@@ -1,5 +1,5 @@
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
-import { DESC, ASC } from '~/notes/constants';
+import { DESC, ASC, NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
import * as getters from '~/notes/stores/getters';
import {
notesDataMock,
@@ -536,4 +536,24 @@ describe('Getters Notes Store', () => {
expect(getters.sortDirection(state)).toBe(DESC);
});
});
+
+ describe('canUserAddIncidentTimelineEvents', () => {
+ it.each`
+ userData | noteableData | expected
+ ${{ can_add_timeline_events: true }} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${true}
+ ${{ can_add_timeline_events: true }} | ${{ type: NOTEABLE_TYPE_MAPPING.Issue }} | ${false}
+ ${null} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${false}
+ ${{ can_add_timeline_events: false }} | ${{ type: NOTEABLE_TYPE_MAPPING.Incident }} | ${false}
+ `(
+ 'with userData=$userData and noteableData=$noteableData, expected=$expected',
+ ({ userData, noteableData, expected }) => {
+ Object.assign(state, {
+ userData,
+ noteableData,
+ });
+
+ expect(getters.canUserAddIncidentTimelineEvents(state)).toBe(expected);
+ },
+ );
+ });
});
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
new file mode 100644
index 00000000000..f0b318e69ec
--- /dev/null
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -0,0 +1,73 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ObservabilityApp from '~/observability/components/observability_app.vue';
+
+describe('Observability root app', () => {
+ let wrapper;
+ const replace = jest.fn();
+ const $router = {
+ replace,
+ };
+ const $route = {
+ pathname: 'https://gitlab.com/gitlab-org/',
+ query: { otherQuery: 100 },
+ };
+
+ const findIframe = () => wrapper.findByTestId('observability-ui-iframe');
+
+ const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
+
+ const mountComponent = (route = $route) => {
+ wrapper = shallowMountExtended(ObservabilityApp, {
+ propsData: {
+ observabilityIframeSrc: TEST_IFRAME_SRC,
+ },
+ mocks: {
+ $router,
+ $route: route,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render an iframe with observabilityIframeSrc as src', () => {
+ mountComponent();
+ const iframe = findIframe();
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('src')).toBe(TEST_IFRAME_SRC);
+ });
+
+ it('should not call replace method from vue router if message event does not have url', () => {
+ mountComponent();
+ wrapper.vm.messageHandler({ data: 'some other data' });
+ expect(replace).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ condition | origin | observability_path | url
+ ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'}
+ ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'}
+ `(
+ 'should not call replace method from vue router if $condition',
+ async ({ origin, observability_path, url }) => {
+ mountComponent({ ...$route, query: { observability_path } });
+ wrapper.vm.messageHandler({ data: { url }, origin });
+ expect(replace).not.toHaveBeenCalled();
+ },
+ );
+
+ it('should call replace method from vue router on messageHandle call', () => {
+ mountComponent();
+ wrapper.vm.messageHandler({ data: { url: '/explore' }, origin: 'https://observe.gitlab.com' });
+ expect(replace).toHaveBeenCalled();
+ expect(replace).toHaveBeenCalledWith({
+ name: 'https://gitlab.com/gitlab-org/',
+ query: {
+ otherQuery: 100,
+ observability_path: '/explore',
+ },
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index 0b59fe2d8ce..7da91c4af96 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -33,7 +33,7 @@ describe('Image List Row', () => {
const findListItemComponent = () => wrapper.findComponent(ListItem);
const findShowFullPathButton = () => wrapper.findComponent(GlButton);
- const mountComponent = (props, features = {}) => {
+ const mountComponent = (props) => {
wrapper = shallowMount(Component, {
stubs: {
RouterLink,
@@ -47,9 +47,6 @@ describe('Image List Row', () => {
},
provide: {
config: {},
- glFeatures: {
- ...features,
- },
},
directives: {
GlTooltip: createMockDirective(),
@@ -88,23 +85,43 @@ describe('Image List Row', () => {
});
describe('image title and path', () => {
- it('contains a link to the details page', () => {
+ it('renders shortened name of image and contains a link to the details page', () => {
mountComponent();
const link = findDetailsLink();
- expect(link.text()).toBe(item.path);
- expect(findDetailsLink().props('to')).toMatchObject({
+ expect(link.text()).toBe('gitlab-test/rails-12009');
+
+ expect(link.props('to')).toMatchObject({
name: 'details',
params: {
id: getIdFromGraphQLId(item.id),
},
});
+
+ expect(findShowFullPathButton().exists()).toBe(true);
});
it('when the image has no name lists the path', () => {
mountComponent({ item: { ...item, name: '' } });
+ expect(findDetailsLink().text()).toBe('gitlab-test');
+ });
+
+ it('clicking on shortened name of image hides the button & shows full path', async () => {
+ mountComponent();
+
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ const mockFocusFn = jest.fn();
+ wrapper.vm.$refs.imageName.$el.focus = mockFocusFn;
+
+ await findShowFullPathButton().trigger('click');
+
+ expect(findShowFullPathButton().exists()).toBe(false);
expect(findDetailsLink().text()).toBe(item.path);
+ expect(mockFocusFn).toHaveBeenCalled();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_show_full_path', {
+ label: 'registry_image_list',
+ });
});
it('contains a clipboard button', () => {
@@ -149,35 +166,6 @@ describe('Image List Row', () => {
expect(findClipboardButton().attributes('disabled')).toBe('true');
});
});
-
- describe('when containerRegistryShowShortenedPath feature enabled', () => {
- let trackingSpy;
-
- beforeEach(() => {
- mountComponent({}, { containerRegistryShowShortenedPath: true });
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('renders shortened name of image', () => {
- expect(findShowFullPathButton().exists()).toBe(true);
- expect(findDetailsLink().text()).toBe('gitlab-test/rails-12009');
- });
-
- it('clicking on shortened name of image hides the button & shows full path', async () => {
- const btn = findShowFullPathButton();
- const mockFocusFn = jest.fn();
- wrapper.vm.$refs.imageName.$el.focus = mockFocusFn;
-
- await btn.trigger('click');
-
- expect(findShowFullPathButton().exists()).toBe(false);
- expect(findDetailsLink().text()).toBe(item.path);
- expect(mockFocusFn).toHaveBeenCalled();
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_show_full_path', {
- label: 'registry_image_list',
- });
- });
- });
});
describe('delete button', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index e6d81d4a28f..bcc8e41fce8 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -58,6 +58,12 @@ describe('registry_header', () => {
describe('sub header parts', () => {
describe('images count', () => {
+ it('does not exist', async () => {
+ await mountComponent({ imagesCount: 0 });
+
+ expect(findImagesCountSubHeader().exists()).toBe(false);
+ });
+
it('exists', async () => {
await mountComponent({ imagesCount: 1 });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index ee6470a9df8..310398b01cf 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -26,6 +26,7 @@ import {
import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
+import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue';
import Tracking from '~/tracking';
@@ -34,6 +35,7 @@ import {
graphQLImageDetailsMock,
graphQLDeleteImageRepositoryTagsMock,
graphQLDeleteImageRepositoryTagImportingErrorMock,
+ graphQLProjectImageRepositoriesDetailsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
tagsMock,
@@ -64,6 +66,9 @@ describe('Details Page', () => {
const defaultConfig = {
noContainersImage: 'noContainersImage',
+ projectListUrl: 'projectListUrl',
+ groupListUrl: 'groupListUrl',
+ isGroupPage: false,
};
const cleanTags = tagsMock.map((t) => {
@@ -81,7 +86,8 @@ describe('Details Page', () => {
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()),
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)),
+ tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock())),
+ detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
options,
config = defaultConfig,
} = {}) => {
@@ -91,6 +97,7 @@ describe('Details Page', () => {
[getContainerRepositoryDetailsQuery, resolver],
[deleteContainerRepositoryTagsMutation, mutationResolver],
[getContainerRepositoryTagsQuery, tagsResolver],
+ [getContainerRepositoriesDetails, detailsResolver],
];
apolloProvider = createMockApollo(requestHandlers);
@@ -256,11 +263,13 @@ describe('Details Page', () => {
describe('confirmDelete event', () => {
let mutationResolver;
let tagsResolver;
+ let detailsResolver;
beforeEach(() => {
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock));
- mountComponent({ mutationResolver, tagsResolver });
+ tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
+ detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
+ mountComponent({ mutationResolver, tagsResolver, detailsResolver });
return waitForApolloRequestRender();
});
@@ -280,6 +289,7 @@ describe('Details Page', () => {
await waitForPromises();
expect(tagsResolver).toHaveBeenCalled();
+ expect(detailsResolver).toHaveBeenCalled();
});
});
@@ -298,6 +308,7 @@ describe('Details Page', () => {
await waitForPromises();
expect(tagsResolver).toHaveBeenCalled();
+ expect(detailsResolver).toHaveBeenCalled();
});
});
});
@@ -359,14 +370,16 @@ describe('Details Page', () => {
describe('importing repository error', () => {
let mutationResolver;
let tagsResolver;
+ let detailsResolver;
beforeEach(async () => {
mutationResolver = jest
.fn()
.mockResolvedValue(graphQLDeleteImageRepositoryTagImportingErrorMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock));
+ tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
+ detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
- mountComponent({ mutationResolver, tagsResolver });
+ mountComponent({ mutationResolver, tagsResolver, detailsResolver });
await waitForApolloRequestRender();
});
@@ -378,6 +391,7 @@ describe('Details Page', () => {
await waitForPromises();
expect(tagsResolver).toHaveBeenCalled();
+ expect(detailsResolver).toHaveBeenCalled();
const deleteAlert = findDeleteAlert();
expect(deleteAlert.exists()).toBe(true);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index fb5ee4e6884..0164d92ce34 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -1,12 +1,13 @@
-import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
+import { GlTable, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import { last } from 'lodash';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
import PackagesList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue';
import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants';
import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants';
import Tracking from '~/tracking';
@@ -22,7 +23,7 @@ describe('packages_list', () => {
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
const findPackageListPagination = () => wrapper.findComponent(GlPagination);
- const findPackageListDeleteModal = () => wrapper.findComponent(GlModal);
+ const findPackageListDeleteModal = () => wrapper.findComponent(DeletePackageModal);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
@@ -65,7 +66,7 @@ describe('packages_list', () => {
stubs: {
...stubChildren(PackagesList),
GlTable,
- GlModal,
+ DeletePackageModal,
},
...options,
});
@@ -109,52 +110,38 @@ describe('packages_list', () => {
expect(sorting.exists()).toBe(true);
});
- it('contains a modal component', () => {
- const sorting = findPackageListDeleteModal();
- expect(sorting.exists()).toBe(true);
+ it("doesn't contain a modal component", () => {
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
});
describe('when the user can destroy the package', () => {
- beforeEach(() => {
+ let itemToBeDeleted;
+
+ beforeEach(async () => {
mountComponent();
+ itemToBeDeleted = last(packageList);
+ await findPackagesListRow().vm.$emit('packageToDelete', itemToBeDeleted);
});
- it('setItemToBeDeleted sets itemToBeDeleted and open the modal', async () => {
- const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
- const item = last(wrapper.vm.list);
-
- findPackagesListRow().vm.$emit('packageToDelete', item);
-
- await nextTick();
- expect(wrapper.vm.itemToBeDeleted).toEqual(item);
- expect(mockModalShow).toHaveBeenCalled();
+ afterEach(() => {
+ itemToBeDeleted = null;
});
- it('deleteItemConfirmation resets itemToBeDeleted', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemConfirmation();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ it('passes itemToBeDeleted to the modal', () => {
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(itemToBeDeleted);
});
it('deleteItemConfirmation emit package:delete', async () => {
- const itemToBeDeleted = { id: 2 };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted });
- wrapper.vm.deleteItemConfirmation();
- await nextTick();
+ await findPackageListDeleteModal().vm.$emit('ok');
+
expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
});
- it('deleteItemCanceled resets itemToBeDeleted', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted: 1 });
- wrapper.vm.deleteItemCanceled();
- expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
+ await findPackageListDeleteModal().vm.$emit(event);
+
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
new file mode 100644
index 00000000000..e0e26434680
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
@@ -0,0 +1,71 @@
+import { GlModal as RealGlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
+
+const GlModal = stubComponent(RealGlModal, {
+ methods: {
+ show: jest.fn(),
+ },
+});
+
+describe('DeleteModal', () => {
+ let wrapper;
+
+ const defaultItemsToBeDeleted = [
+ {
+ name: 'package 01',
+ },
+ {
+ name: 'package 02',
+ },
+ ];
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const mountComponent = ({ itemsToBeDeleted = defaultItemsToBeDeleted } = {}) => {
+ wrapper = shallowMountExtended(DeleteModal, {
+ propsData: {
+ itemsToBeDeleted,
+ },
+ stubs: {
+ GlModal,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('passes title prop', () => {
+ expect(findModal().props('title')).toMatchInterpolatedText('Delete packages');
+ });
+
+ it('passes actionPrimary prop', () => {
+ expect(findModal().props('actionPrimary')).toStrictEqual({
+ text: 'Permanently delete',
+ attributes: [{ variant: 'danger' }, { category: 'primary' }],
+ });
+ });
+
+ it('renders description', () => {
+ expect(findModal().text()).toContain(
+ 'You are about to delete 2 packages. This operation is irreversible.',
+ );
+ });
+
+ it('emits confirm when primary event is emitted', () => {
+ expect(wrapper.emitted('confirm')).toBeUndefined();
+
+ findModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('confirm')).toHaveLength(1);
+ });
+
+ it('show calls gl-modal show', () => {
+ findModal().vm.show();
+
+ expect(GlModal.methods.show).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
new file mode 100644
index 00000000000..f0fa9592419
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -0,0 +1,152 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import { packageData } from '../../mock_data';
+
+describe('PackageVersionsList', () => {
+ let wrapper;
+
+ const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>empty message</div>' };
+ const packageList = [
+ packageData({
+ name: 'version 1',
+ }),
+ packageData({
+ id: `gid://gitlab/Packages::Package/112`,
+ name: 'version 2',
+ }),
+ ];
+
+ const uiElements = {
+ findLoader: () => wrapper.findComponent(PackagesListLoader),
+ findListPagination: () => wrapper.findComponent(GlKeysetPagination),
+ findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
+ findListRow: () => wrapper.findAllComponents(VersionRow),
+ };
+ const mountComponent = (props) => {
+ wrapper = shallowMountExtended(PackageVersionsList, {
+ propsData: {
+ versions: packageList,
+ pageInfo: {},
+ isLoading: false,
+ ...props,
+ },
+ slots: {
+ 'empty-state': EmptySlotStub,
+ },
+ });
+ };
+
+ describe('when list is loading', () => {
+ beforeEach(() => {
+ mountComponent({ isLoading: true, versions: [] });
+ });
+ it('displays loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(true);
+ });
+
+ it('does not display rows', () => {
+ expect(uiElements.findListRow().exists()).toBe(false);
+ });
+
+ it('does not display empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(false);
+ });
+
+ it('does not display pagination', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when list is loaded and has no data', () => {
+ beforeEach(() => {
+ mountComponent({ isLoading: false, versions: [] });
+ });
+
+ it('displays empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(true);
+ });
+
+ it('does not display loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(false);
+ });
+
+ it('does not display rows', () => {
+ expect(uiElements.findListRow().exists()).toBe(false);
+ });
+
+ it('does not display pagination', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when list is loaded with data', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('displays package version rows', () => {
+ expect(uiElements.findListRow().exists()).toEqual(true);
+ expect(uiElements.findListRow()).toHaveLength(packageList.length);
+ });
+
+ it('binds the correct props', () => {
+ expect(uiElements.findListRow().at(0).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageList[0]),
+ });
+
+ expect(uiElements.findListRow().at(1).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageList[1]),
+ });
+ });
+
+ describe('pagination display', () => {
+ it('does not display pagination if there is no previous or next page', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+
+ it('displays pagination if pageInfo.hasNextPage is true', async () => {
+ await wrapper.setProps({ pageInfo: { hasNextPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+
+ it('displays pagination if pageInfo.hasPreviousPage is true', async () => {
+ await wrapper.setProps({ pageInfo: { hasPreviousPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+
+ it('displays pagination if both pageInfo.hasNextPage and pageInfo.hasPreviousPage are true', async () => {
+ await wrapper.setProps({ pageInfo: { hasNextPage: true, hasPreviousPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+ });
+
+ it('does not display loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(false);
+ });
+
+ it('does not display empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(false);
+ });
+ });
+
+ describe('when user interacts with pagination', () => {
+ beforeEach(() => {
+ mountComponent({ pageInfo: { hasNextPage: true } });
+ });
+
+ it('emits prev-page event when paginator emits prev event', () => {
+ uiElements.findListPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toHaveLength(1);
+ });
+
+ it('emits next-page when paginator emits next event', () => {
+ uiElements.findListPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index 5be05ddf629..a7de751aadd 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -8,7 +8,14 @@ exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-py-3"
>
- <!---->
+ <div
+ class="gl-w-7 gl-display-flex gl-justify-content-start gl-pl-2"
+ >
+ <gl-form-checkbox-stub
+ class="gl-m-0"
+ id="2"
+ />
+ </div>
<div
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index b5a512b8806..913b4f5926f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,4 +1,4 @@
-import { GlSprintf } from '@gitlab/ui';
+import { GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -40,9 +40,11 @@ describe('packages_list_row', () => {
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+ const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const mountComponent = ({
packageEntity = packageWithoutTags,
+ selected = false,
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
@@ -53,6 +55,7 @@ describe('packages_list_row', () => {
},
propsData: {
packageEntity,
+ selected,
},
directives: {
GlTooltip: createMockDirective(),
@@ -117,14 +120,13 @@ describe('packages_list_row', () => {
});
});
- it('emits the packageToDelete event when the delete button is clicked', async () => {
+ it('emits the delete event when the delete button is clicked', async () => {
mountComponent({ packageEntity: packageWithoutTags });
findDeleteDropdown().vm.$emit('click');
await nextTick();
- expect(wrapper.emitted('packageToDelete')).toHaveLength(1);
- expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
+ expect(wrapper.emitted('delete')).toHaveLength(1);
});
});
@@ -151,6 +153,39 @@ describe('packages_list_row', () => {
});
});
+ describe('left action template', () => {
+ it('does not render checkbox if not permitted', () => {
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, canDestroy: false },
+ });
+
+ expect(findBulkDeleteAction().exists()).toBe(false);
+ });
+
+ it('renders checkbox', () => {
+ mountComponent();
+
+ expect(findBulkDeleteAction().exists()).toBe(true);
+ expect(findBulkDeleteAction().attributes('checked')).toBeUndefined();
+ });
+
+ it('emits select when checked', () => {
+ mountComponent();
+
+ findBulkDeleteAction().vm.$emit('change');
+
+ expect(wrapper.emitted('select')).toHaveLength(1);
+ });
+
+ it('renders checkbox in selected state if selected', () => {
+ mountComponent({
+ selected: true,
+ });
+
+ expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ });
+ });
+
describe('secondary left info', () => {
it('has the package version', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 3e3607a361c..7cc5bea0f7a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -1,8 +1,10 @@
-import { GlAlert, GlKeysetPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
@@ -35,16 +37,11 @@ describe('packages_list', () => {
};
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
- const GlModalStub = {
- name: GlModal.name,
- template: '<div><slot></slot></div>',
- methods: { show: jest.fn() },
- };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
- const findPackageListPagination = () => wrapper.findComponent(GlKeysetPagination);
- const findPackageListDeleteModal = () => wrapper.findComponent(GlModalStub);
+ const findPackageListDeleteModal = () => wrapper.findComponent(DeletePackageModal);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
+ const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
@@ -55,8 +52,9 @@ describe('packages_list', () => {
...props,
},
stubs: {
- GlModal: GlModalStub,
+ DeletePackageModal,
GlSprintf,
+ RegistryList,
},
slots: {
'empty-state': EmptySlotStub,
@@ -64,10 +62,6 @@ describe('packages_list', () => {
});
};
- beforeEach(() => {
- GlModalStub.methods.show.mockReset();
- });
-
afterEach(() => {
wrapper.destroy();
});
@@ -81,12 +75,12 @@ describe('packages_list', () => {
expect(findPackagesListLoader().exists()).toBe(true);
});
- it('does not show the rows', () => {
- expect(findPackagesListRow().exists()).toBe(false);
+ it('does not show the registry list', () => {
+ expect(findRegistryList().exists()).toBe(false);
});
- it('does not show the pagination', () => {
- expect(findPackageListPagination().exists()).toBe(false);
+ it('does not show the rows', () => {
+ expect(findPackagesListRow().exists()).toBe(false);
});
});
@@ -99,22 +93,29 @@ describe('packages_list', () => {
expect(findPackagesListLoader().exists()).toBe(false);
});
+ it('shows the registry list', () => {
+ expect(findRegistryList().exists()).toBe(true);
+ });
+
+ it('shows the registry list with the right props', () => {
+ expect(findRegistryList().props()).toMatchObject({
+ title: '2 packages',
+ items: defaultProps.list,
+ pagination: defaultProps.pageInfo,
+ isLoading: false,
+ });
+ });
+
it('shows the rows', () => {
expect(findPackagesListRow().exists()).toBe(true);
});
});
describe('layout', () => {
- it('contains a pagination component', () => {
- mountComponent({ pageInfo: { hasPreviousPage: true } });
-
- expect(findPackageListPagination().exists()).toBe(true);
- });
-
- it('contains a modal component', () => {
+ it("doesn't contain a visible modal component", () => {
mountComponent();
- expect(findPackageListDeleteModal().exists()).toBe(true);
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
it('does not have an error alert displayed', () => {
@@ -125,31 +126,46 @@ describe('packages_list', () => {
});
describe('when the user can destroy the package', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mountComponent();
- findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
- return nextTick();
+ await findPackagesListRow().vm.$emit('delete', firstPackage);
});
- it('deleting a package opens the modal', () => {
- expect(findPackageListDeleteModal().text()).toContain(firstPackage.name);
+ it('passes itemToBeDeleted to the modal', () => {
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
});
- it('confirming on the modal emits package:delete', async () => {
- findPackageListDeleteModal().vm.$emit('ok');
-
- await nextTick();
+ it('emits package:delete when modal confirms', async () => {
+ await findPackageListDeleteModal().vm.$emit('ok');
expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
});
- it('closing the modal resets itemToBeDeleted', async () => {
- // triggering the v-model
- findPackageListDeleteModal().vm.$emit('input', false);
+ it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
+ await findPackageListDeleteModal().vm.$emit(event);
- await nextTick();
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
+ });
+ });
+
+ describe('when the user can bulk destroy packages', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('passes itemToBeDeleted to the modal when there is only one package', async () => {
+ await findRegistryList().vm.$emit('delete', [firstPackage]);
+
+ expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
+ expect(wrapper.emitted('delete')).toBeUndefined();
+ });
+
+ it('emits delete when there is more than one package', () => {
+ const items = [firstPackage, secondPackage];
+ findRegistryList().vm.$emit('delete', items);
- expect(findPackageListDeleteModal().text()).not.toContain(firstPackage.name);
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0]).toEqual([items]);
});
});
@@ -196,15 +212,15 @@ describe('packages_list', () => {
});
it('emits prev-page events when the prev event is fired', () => {
- findPackageListPagination().vm.$emit('prev');
+ findRegistryList().vm.$emit('prev-page');
- expect(wrapper.emitted('prev-page')).toEqual([[]]);
+ expect(wrapper.emitted('prev-page')).toHaveLength(1);
});
it('emits next-page events when the next event is fired', () => {
- findPackageListPagination().vm.$emit('next');
+ findRegistryList().vm.$emit('next-page');
- expect(wrapper.emitted('next-page')).toEqual([[]]);
+ expect(wrapper.emitted('next-page')).toHaveLength(1);
});
});
@@ -215,7 +231,7 @@ describe('packages_list', () => {
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
- findPackagesListRow().vm.$emit('packageToDelete', firstPackage);
+ findPackagesListRow().vm.$emit('delete', firstPackage);
return nextTick();
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index c2b6fb734d6..f36c5923532 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -233,6 +233,12 @@ export const packageDetailsQuery = (extendPackage) => ({
},
versions: {
nodes: packageVersions(),
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
__typename: 'PackageConnection',
},
dependencyLinks: {
@@ -288,6 +294,33 @@ export const packageDestroyMutation = () => ({
},
});
+export const packagesDestroyMutation = () => ({
+ data: {
+ destroyPackages: {
+ errors: [],
+ },
+ },
+});
+
+export const packagesDestroyMutationError = () => ({
+ data: {
+ destroyPackages: null,
+ },
+ errors: [
+ {
+ message:
+ "The resource that you are attempting to access does not exist or you don't have permission to perform this action",
+ locations: [
+ {
+ line: 2,
+ column: 3,
+ },
+ ],
+ path: ['destroyPackages'],
+ },
+ ],
+});
+
export const packageDestroyMutationError = () => ({
data: {
destroyPackage: null,
@@ -314,6 +347,7 @@ export const packageDestroyFilesMutation = () => ({
},
},
});
+
export const packageDestroyFilesMutationError = () => ({
data: {
destroyPackageFiles: null,
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
index 17905a8db2d..c2fecf87428 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
@@ -2,12 +2,62 @@
exports[`PackagesListApp renders 1`] = `
<div>
+ <!---->
+
+ <gl-card-stub
+ bodyclass="gl-display-flex gl-p-0!"
+ class="gl-px-8 gl-py-6 gl-line-height-20 gl-mt-3"
+ footerclass=""
+ headerclass=""
+ >
+ <!---->
+
+ <div
+ class="gl-banner-content"
+ >
+ <h2
+ class="gl-banner-title"
+ >
+ Help us learn about your registry migration needs
+ </h2>
+
+ <p>
+ If you are interested in migrating packages from your private registry to the GitLab Package Registry, take our survey and tell us more about your needs.
+ </p>
+
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ data-testid="gl-banner-primary-button"
+ href="https://gitlab.fra1.qualtrics.com/jfe/form/SV_cHomH9FPzOaiDTU"
+ icon=""
+ size="medium"
+ variant="confirm"
+ >
+ Take survey
+ </gl-button-stub>
+
+ </div>
+
+ <gl-button-stub
+ aria-label="Close banner"
+ buttontextclasses=""
+ category="tertiary"
+ class="gl-banner-close"
+ icon="close"
+ size="small"
+ variant="default"
+ />
+ </gl-card-stub>
+
<package-title-stub
count="2"
helpurl="/help/user/packages/index"
/>
- <package-search-stub />
+ <package-search-stub
+ class="gl-mb-5"
+ />
<div>
<section
@@ -69,5 +119,7 @@ exports[`PackagesListApp renders 1`] = `
</div>
</section>
</div>
+
+ <div />
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index a32e76a132e..f942a334f40 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -15,8 +15,8 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
-import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
@@ -99,6 +99,7 @@ describe('PackagesApp', () => {
GlSprintf,
GlTabs,
GlTab,
+ PackageVersionsList,
},
mocks: {
$route: {
@@ -120,8 +121,7 @@ describe('PackagesApp', () => {
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
- const findVersionRows = () => wrapper.findAllComponents(VersionRow);
- const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
+ const findVersionsList = () => wrapper.findComponent(PackageVersionsList);
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
@@ -558,38 +558,23 @@ describe('PackagesApp', () => {
});
describe('versions', () => {
- it('displays the correct version count when the package has versions', async () => {
+ it('displays versions list when the package has versions', async () => {
createComponent();
await waitForPromises();
- expect(findVersionRows()).toHaveLength(packageVersions().length);
+ expect(findVersionsList()).toBeDefined();
});
it('binds the correct props', async () => {
- const [versionPackage] = packageVersions();
- // eslint-disable-next-line no-underscore-dangle
- delete versionPackage.__typename;
- delete versionPackage.tags;
-
- createComponent();
-
+ const versionNodes = packageVersions();
+ createComponent({ packageEntity: { versions: { nodes: versionNodes } } });
await waitForPromises();
- expect(findVersionRows().at(0).props()).toMatchObject({
- packageEntity: expect.objectContaining(versionPackage),
+ expect(findVersionsList().props()).toMatchObject({
+ versions: expect.arrayContaining(versionNodes),
});
});
-
- it('displays the no versions message when there are none', async () => {
- createComponent({
- resolver: jest.fn().mockResolvedValue(packageDetailsQuery({ versions: { nodes: [] } })),
- });
-
- await waitForPromises();
-
- expect(noVersionsMessage().exists()).toBe(true);
- });
});
describe('dependency links', () => {
it('does not show the dependency links for a non nuget package', async () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index 0e74fbbc6d9..abdb875e839 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -1,30 +1,39 @@
-import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlAlert, GlBanner, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-
+import * as utils from '~/lib/utils/common_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import ListPage from '~/packages_and_registries/package_registry/pages/list.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
-
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
+ HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
+ DELETE_PACKAGES_ERROR_MESSAGE,
+ DELETE_PACKAGES_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
+import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
+import {
+ packagesListQuery,
+ packageData,
+ pagination,
+ packagesDestroyMutation,
+ packagesDestroyMutationError,
+} from '../mock_data';
-import { packagesListQuery, packageData, pagination } from '../mock_data';
-
-jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
describe('PackagesListApp', () => {
@@ -49,31 +58,44 @@ describe('PackagesListApp', () => {
filters: { packageName: 'foo', packageType: 'CONAN' },
};
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findBanner = () => wrapper.findComponent(GlBanner);
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDeletePackage = () => wrapper.findComponent(DeletePackage);
+ const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
+ mutationResolver,
provide = defaultProvide,
} = {}) => {
Vue.use(VueApollo);
- const requestHandlers = [[getPackagesQuery, resolver]];
+ const requestHandlers = [
+ [getPackagesQuery, resolver],
+ [destroyPackagesMutation, mutationResolver],
+ ];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMountExtended(ListPage, {
apolloProvider,
provide,
stubs: {
+ GlBanner,
GlEmptyState,
GlLoadingIcon,
GlSprintf,
GlLink,
PackageList,
DeletePackage,
+ DeleteModal: stubComponent(DeleteModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
},
});
};
@@ -116,6 +138,70 @@ describe('PackagesListApp', () => {
});
});
+ describe('package migration survey banner', () => {
+ describe('with no cookie set', () => {
+ beforeEach(() => {
+ utils.setCookie = jest.fn();
+
+ mountComponent();
+ });
+
+ it('displays the banner', () => {
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('does not call setCookie', () => {
+ expect(utils.setCookie).not.toHaveBeenCalled();
+ });
+
+ describe('when the close button is clicked', () => {
+ beforeEach(() => {
+ findBanner().vm.$emit('close');
+ });
+
+ it('sets the dismissed cookie', () => {
+ expect(utils.setCookie).toHaveBeenCalledWith(
+ HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
+ 'true',
+ );
+ });
+
+ it('does not display the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('when the primary button is clicked', () => {
+ beforeEach(() => {
+ findBanner().vm.$emit('primary');
+ });
+
+ it('sets the dismissed cookie', () => {
+ expect(utils.setCookie).toHaveBeenCalledWith(
+ HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
+ 'true',
+ );
+ });
+
+ it('does not display the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with the dismissed cookie set', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getCookie').mockReturnValue('true');
+
+ mountComponent();
+ });
+
+ it('does not display the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+ });
+
describe('search component', () => {
it('exists', () => {
mountComponent();
@@ -282,4 +368,62 @@ describe('PackagesListApp', () => {
expect(findListComponent().props('isLoading')).toBe(false);
});
});
+
+ describe('bulk delete package', () => {
+ const items = [{ id: '1' }, { id: '2' }];
+
+ it('deletePackage is bound to package-list package:delete event', async () => {
+ mountComponent();
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('delete', [{ id: '1' }, { id: '2' }]);
+
+ await waitForPromises();
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual(items);
+ });
+
+ it('calls mutation with the right values and shows success alert', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
+ mountComponent({
+ mutationResolver,
+ });
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('delete', items);
+
+ findDeletePackagesModal().vm.$emit('confirm');
+
+ expect(mutationResolver).toHaveBeenCalledWith({
+ ids: items.map((item) => item.id),
+ });
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().props('variant')).toEqual('success');
+ expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_SUCCESS_MESSAGE);
+ });
+
+ it('on error shows danger alert', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutationError());
+ mountComponent({
+ mutationResolver,
+ });
+
+ await waitForFirstRequest();
+
+ findListComponent().vm.$emit('delete', items);
+
+ findDeletePackagesModal().vm.$emit('confirm');
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().props('variant')).toEqual('danger');
+ expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_ERROR_MESSAGE);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
new file mode 100644
index 00000000000..8f229182fe5
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
@@ -0,0 +1,78 @@
+import { GlFormGroup, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import component from '~/packages_and_registries/settings/group/components/forwarding_settings.vue';
+
+describe('Forwarding Settings', () => {
+ let wrapper;
+
+ const defaultProps = {
+ disabled: false,
+ forwarding: false,
+ label: 'label',
+ lockForwarding: false,
+ modelNames: {
+ forwarding: 'forwardField',
+ lockForwarding: 'lockForwardingField',
+ isLocked: 'lockedField',
+ },
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findForwardingCheckbox = () => wrapper.findByTestId('forwarding-checkbox');
+ const findLockForwardingCheckbox = () => wrapper.findByTestId('lock-forwarding-checkbox');
+
+ it('has a form group', () => {
+ mountComponent();
+
+ expect(findFormGroup().exists()).toBe(true);
+ expect(findFormGroup().attributes()).toMatchObject({
+ label: defaultProps.label,
+ });
+ });
+
+ describe.each`
+ name | finder | label | extraProps | field
+ ${'forwarding'} | ${findForwardingCheckbox} | ${'Forward label package requests'} | ${{ forwarding: true }} | ${defaultProps.modelNames.forwarding}
+ ${'lock forwarding'} | ${findLockForwardingCheckbox} | ${'Enforce label setting for all subgroups'} | ${{ lockForwarding: true }} | ${defaultProps.modelNames.lockForwarding}
+ `('$name checkbox', ({ name, finder, label, extraProps, field }) => {
+ it('is rendered', () => {
+ mountComponent();
+ expect(finder().exists()).toBe(true);
+ expect(finder().text()).toMatchInterpolatedText(label);
+ expect(finder().attributes('disabled')).toBeUndefined();
+ expect(finder().attributes('checked')).toBeUndefined();
+ });
+
+ it(`is checked when ${name} set`, () => {
+ mountComponent({ ...defaultProps, ...extraProps });
+
+ expect(finder().attributes('checked')).toBe('true');
+ });
+
+ it(`emits an update event with field ${field} set`, () => {
+ mountComponent();
+
+ finder().vm.$emit('change', true);
+
+ expect(wrapper.emitted('update')).toStrictEqual([[field, true]]);
+ });
+ });
+
+ describe('disabled', () => {
+ it('disables both checkboxes', () => {
+ mountComponent({ ...defaultProps, disabled: true });
+
+ expect(findForwardingCheckbox().attributes('disabled')).toEqual('true');
+ expect(findLockForwardingCheckbox().attributes('disabled')).toEqual('true');
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 31fc3ad419c..7edc321867c 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
+import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
@@ -60,6 +61,7 @@ describe('Group Settings App', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findPackageSettings = () => wrapper.findComponent(PackagesSettings);
+ const findPackageForwardingSettings = () => wrapper.findComponent(PackagesForwardingSettings);
const findDependencyProxySettings = () => wrapper.findComponent(DependencyProxySettings);
const waitForApolloQueryAndRender = async () => {
@@ -67,16 +69,18 @@ describe('Group Settings App', () => {
await nextTick();
};
- const packageSettingsProps = { packageSettings: packageSettings() };
+ const packageSettingsProps = { packageSettings };
+ const packageForwardingSettingsProps = { forwardSettings: { ...packageSettings } };
const dependencyProxyProps = {
dependencyProxySettings: dependencyProxySettings(),
dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(),
};
describe.each`
- finder | entitySpecificProps | successMessage | errorMessage
- ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
- ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'}
+ finder | entitySpecificProps | successMessage | errorMessage
+ ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
+ ${findPackageForwardingSettings} | ${packageForwardingSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
+ ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'}
`('settings blocks', ({ finder, entitySpecificProps, successMessage, errorMessage }) => {
beforeEach(() => {
mountComponent();
@@ -88,10 +92,7 @@ describe('Group Settings App', () => {
});
it('binds the correctProps', () => {
- expect(finder().props()).toMatchObject({
- isLoading: false,
- ...entitySpecificProps,
- });
+ expect(finder().props()).toMatchObject(entitySpecificProps);
});
describe('success event', () => {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 13eba39ec8c..807f332f4d3 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -48,7 +48,7 @@ describe('Packages Settings', () => {
apolloProvider,
provide: defaultProvide,
propsData: {
- packageSettings: packageSettings(),
+ packageSettings,
},
stubs: {
SettingsBlock,
@@ -83,7 +83,7 @@ describe('Packages Settings', () => {
};
const emitMavenSettingsUpdate = (override) => {
- findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', {
+ findMavenDuplicatedSettingsExceptionsInput().vm.$emit('update', {
mavenDuplicateExceptionRegex: ')',
...override,
});
@@ -117,7 +117,7 @@ describe('Packages Settings', () => {
it('renders toggle', () => {
mountComponent({ mountFn: mountExtended });
- const { mavenDuplicatesAllowed } = packageSettings();
+ const { mavenDuplicatesAllowed } = packageSettings;
expect(findMavenDuplicatedSettingsToggle().exists()).toBe(true);
@@ -132,7 +132,7 @@ describe('Packages Settings', () => {
it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
mountComponent({ mountFn: mountExtended });
- const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings();
+ const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings;
expect(findMavenDuplicatedSettingsExceptionsInput().exists()).toBe(true);
@@ -170,7 +170,7 @@ describe('Packages Settings', () => {
it('renders toggle', () => {
mountComponent({ mountFn: mountExtended });
- const { genericDuplicatesAllowed } = packageSettings();
+ const { genericDuplicatesAllowed } = packageSettings;
expect(findGenericDuplicatedSettingsToggle().exists()).toBe(true);
expect(findGenericDuplicatedSettingsToggle().props()).toMatchObject({
@@ -184,7 +184,7 @@ describe('Packages Settings', () => {
it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => {
mountComponent({ mountFn: mountExtended });
- const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings();
+ const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings;
expect(findGenericDuplicatedSettingsExceptionsInput().props()).toMatchObject({
duplicatesAllowed: genericDuplicatesAllowed,
@@ -239,7 +239,7 @@ describe('Packages Settings', () => {
emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex });
expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
- ...packageSettings(),
+ ...packageSettings,
mavenDuplicateExceptionRegex,
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
new file mode 100644
index 00000000000..a0b257a9496
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
@@ -0,0 +1,280 @@
+import Vue from 'vue';
+import { GlButton } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import component from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
+import {
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ PACKAGE_FORWARDING_SETTINGS_HEADER,
+} from '~/packages_and_registries/settings/group/constants';
+
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ packageSettings,
+ packageForwardingSettings,
+ groupPackageSettingsMock,
+ groupPackageForwardSettingsMutationMock,
+ mutationErrorMock,
+ npmProps,
+ pypiProps,
+ mavenProps,
+} from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
+
+describe('Packages Forwarding Settings', () => {
+ let wrapper;
+ let apolloProvider;
+ const mutationResolverFn = jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock());
+
+ const defaultProvide = {
+ groupPath: 'foo_group_path',
+ };
+
+ const mountComponent = ({
+ forwardSettings = { ...packageSettings },
+ features = {},
+ mutationResolver = mutationResolverFn,
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[updateNamespacePackageSettings, mutationResolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(component, {
+ apolloProvider,
+ provide: {
+ ...defaultProvide,
+ glFeatures: {
+ ...features,
+ },
+ },
+ propsData: {
+ forwardSettings,
+ },
+ stubs: {
+ SettingsBlock,
+ },
+ });
+ };
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findForm = () => wrapper.find('form');
+ const findSubmitButton = () => findForm().findComponent(GlButton);
+ const findDescription = () => wrapper.findByTestId('description');
+ const findMavenForwardingSettings = () => wrapper.findByTestId('maven');
+ const findNpmForwardingSettings = () => wrapper.findByTestId('npm');
+ const findPyPiForwardingSettings = () => wrapper.findByTestId('pypi');
+
+ const fillApolloCache = () => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getGroupPackagesSettingsQuery,
+ variables: {
+ fullPath: defaultProvide.groupPath,
+ },
+ ...groupPackageSettingsMock,
+ });
+ };
+
+ const updateNpmSettings = () => {
+ findNpmForwardingSettings().vm.$emit('update', 'npmPackageRequestsForwarding', false);
+ };
+
+ const submitForm = () => {
+ findForm().trigger('submit');
+ return waitForPromises();
+ };
+
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(PACKAGE_FORWARDING_SETTINGS_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ );
+ });
+
+ it('watches changes to props', async () => {
+ mountComponent();
+
+ expect(findNpmForwardingSettings().props()).toMatchObject(npmProps);
+
+ await wrapper.setProps({
+ forwardSettings: {
+ ...packageSettings,
+ npmPackageRequestsForwardingLocked: true,
+ },
+ });
+
+ expect(findNpmForwardingSettings().props()).toMatchObject({ ...npmProps, disabled: true });
+ });
+
+ it('submit button is disabled', () => {
+ mountComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ });
+
+ describe.each`
+ type | finder | props | field
+ ${'npm'} | ${findNpmForwardingSettings} | ${npmProps} | ${'npmPackageRequestsForwarding'}
+ ${'pypi'} | ${findPyPiForwardingSettings} | ${pypiProps} | ${'pypiPackageRequestsForwarding'}
+ ${'maven'} | ${findMavenForwardingSettings} | ${mavenProps} | ${'mavenPackageRequestsForwarding'}
+ `('$type settings', ({ finder, props, field }) => {
+ beforeEach(() => {
+ mountComponent({ features: { mavenCentralRequestForwarding: true } });
+ });
+
+ it('assigns forwarding settings props', () => {
+ expect(finder().props()).toMatchObject(props);
+ });
+
+ it('on update event enables submit button', async () => {
+ finder().vm.$emit('update', field, false);
+
+ await waitForPromises();
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('maven settings', () => {
+ describe('with feature turned off', () => {
+ it('does not exist', () => {
+ mountComponent();
+
+ expect(findMavenForwardingSettings().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('settings update', () => {
+ describe('success state', () => {
+ it('calls the mutation with the right variables', async () => {
+ const {
+ mavenPackageRequestsForwardingLocked,
+ npmPackageRequestsForwardingLocked,
+ pypiPackageRequestsForwardingLocked,
+ ...packageSettingsInput
+ } = packageForwardingSettings;
+
+ mountComponent();
+
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(mutationResolverFn).toHaveBeenCalledWith({
+ input: {
+ namespacePath: defaultProvide.groupPath,
+ ...packageSettingsInput,
+ npmPackageRequestsForwarding: false,
+ },
+ });
+ });
+
+ it('when field are locked calls the mutation with the right variables', async () => {
+ mountComponent({
+ forwardSettings: {
+ ...packageSettings,
+ mavenPackageRequestsForwardingLocked: true,
+ pypiPackageRequestsForwardingLocked: true,
+ },
+ });
+
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(mutationResolverFn).toHaveBeenCalledWith({
+ input: {
+ namespacePath: defaultProvide.groupPath,
+ lockNpmPackageRequestsForwarding: false,
+ npmPackageRequestsForwarding: false,
+ },
+ });
+ });
+
+ it('emits a success event', async () => {
+ mountComponent();
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(wrapper.emitted('success')).toHaveLength(1);
+ });
+
+ it('has an optimistic response', async () => {
+ const npmPackageRequestsForwarding = false;
+ mountComponent();
+
+ fillApolloCache();
+
+ expect(findNpmForwardingSettings().props('forwarding')).toBe(true);
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
+ ...packageSettings,
+ npmPackageRequestsForwarding,
+ });
+ expect(findNpmForwardingSettings().props('forwarding')).toBe(npmPackageRequestsForwarding);
+ });
+ });
+
+ describe('errors', () => {
+ it('mutation payload with root level errors', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(mutationErrorMock);
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+
+ it.each`
+ type | mutationResolver
+ ${'local'} | ${jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock({ errors: ['foo'] }))}
+ ${'network'} | ${jest.fn().mockRejectedValue()}
+ `('mutation payload with $type error', async ({ mutationResolver }) => {
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index d53446de910..1ca9dc6daeb 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -1,9 +1,26 @@
-export const packageSettings = () => ({
+const packageDuplicateSettings = {
mavenDuplicatesAllowed: true,
mavenDuplicateExceptionRegex: '',
genericDuplicatesAllowed: true,
genericDuplicateExceptionRegex: '',
-});
+};
+
+export const packageForwardingSettings = {
+ mavenPackageRequestsForwarding: true,
+ lockMavenPackageRequestsForwarding: false,
+ npmPackageRequestsForwarding: true,
+ lockNpmPackageRequestsForwarding: false,
+ pypiPackageRequestsForwarding: true,
+ lockPypiPackageRequestsForwarding: false,
+ mavenPackageRequestsForwardingLocked: false,
+ npmPackageRequestsForwardingLocked: false,
+ pypiPackageRequestsForwardingLocked: false,
+};
+
+export const packageSettings = {
+ ...packageDuplicateSettings,
+ ...packageForwardingSettings,
+};
export const dependencyProxySettings = (extend) => ({
enabled: true,
@@ -21,13 +38,52 @@ export const groupPackageSettingsMock = {
group: {
id: '1',
fullPath: 'foo_group_path',
- packageSettings: packageSettings(),
+ packageSettings: {
+ ...packageSettings,
+ __typename: 'PackageSettings',
+ },
dependencyProxySetting: dependencyProxySettings(),
dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(),
},
},
};
+export const npmProps = {
+ forwarding: packageForwardingSettings.npmPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockNpmPackageRequestsForwarding,
+ label: 'npm',
+ disabled: false,
+ modelNames: {
+ forwarding: 'npmPackageRequestsForwarding',
+ lockForwarding: 'lockNpmPackageRequestsForwarding',
+ isLocked: 'npmPackageRequestsForwardingLocked',
+ },
+};
+
+export const pypiProps = {
+ forwarding: packageForwardingSettings.pypiPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockPypiPackageRequestsForwarding,
+ label: 'PyPI',
+ disabled: false,
+ modelNames: {
+ forwarding: 'pypiPackageRequestsForwarding',
+ lockForwarding: 'lockPypiPackageRequestsForwarding',
+ isLocked: 'pypiPackageRequestsForwardingLocked',
+ },
+};
+
+export const mavenProps = {
+ forwarding: packageForwardingSettings.mavenPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockMavenPackageRequestsForwarding,
+ label: 'Maven',
+ disabled: false,
+ modelNames: {
+ forwarding: 'mavenPackageRequestsForwarding',
+ lockForwarding: 'lockMavenPackageRequestsForwarding',
+ isLocked: 'mavenPackageRequestsForwardingLocked',
+ },
+};
+
export const groupPackageSettingsMutationMock = (override) => ({
data: {
updateNamespacePackageSettings: {
@@ -43,6 +99,19 @@ export const groupPackageSettingsMutationMock = (override) => ({
},
});
+export const groupPackageForwardSettingsMutationMock = (override) => ({
+ data: {
+ updateNamespacePackageSettings: {
+ packageSettings: {
+ npmPackageRequestsForwarding: true,
+ lockNpmPackageRequestsForwarding: false,
+ },
+ errors: [],
+ ...override,
+ },
+ },
+});
+
export const dependencyProxySettingMutationMock = (override) => ({
data: {
updateDependencyProxySettings: {
diff --git a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
new file mode 100644
index 00000000000..357dab593e8
--- /dev/null
+++ b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
@@ -0,0 +1,82 @@
+import { GlSprintf, GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
+
+describe('DeletePackageModal', () => {
+ let wrapper;
+
+ const defaultItemToBeDeleted = {
+ name: 'package 01',
+ };
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const mountComponent = ({ itemToBeDeleted = defaultItemToBeDeleted } = {}) => {
+ wrapper = shallowMountExtended(DeletePackageModal, {
+ propsData: {
+ itemToBeDeleted,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when itemToBeDeleted prop is defined', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('displays modal', () => {
+ expect(findModal().props('visible')).toBe(true);
+ });
+
+ it('passes title prop', () => {
+ expect(findModal().props('title')).toBe(wrapper.vm.$options.i18n.modalTitle);
+ });
+
+ it('passes actionPrimary prop', () => {
+ expect(findModal().props('actionPrimary')).toStrictEqual({
+ text: wrapper.vm.$options.i18n.modalAction,
+ attributes: {
+ variant: 'danger',
+ },
+ });
+ });
+
+ it('displays description', () => {
+ const descriptionEl = findModal().findComponent(GlSprintf);
+
+ expect(descriptionEl.exists()).toBe(true);
+ expect(descriptionEl.attributes('message')).toBe(wrapper.vm.$options.i18n.modalDescription);
+ });
+
+ it('emits ok when modal is validate', () => {
+ expect(wrapper.emitted().ok).toBeUndefined();
+
+ findModal().vm.$emit('ok');
+
+ expect(wrapper.emitted().ok).toHaveLength(1);
+ });
+
+ it('emits cancel when modal close', () => {
+ expect(wrapper.emitted().cancel).toBeUndefined();
+
+ findModal().vm.$emit('change', false);
+
+ expect(wrapper.emitted().cancel).toHaveLength(1);
+ });
+ });
+
+ describe('when itemToBeDeleted prop is null', () => {
+ beforeEach(() => {
+ mountComponent({ itemToBeDeleted: null });
+ });
+
+ it("doesn't display modal", () => {
+ expect(findModal().props('visible')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
index aab78c99190..6b6833b00c3 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
@@ -147,6 +147,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="set_up_your_first_project_s_ci_cd"
@@ -171,6 +172,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="start_a_free_trial_of_gitlab_ultimate"
@@ -196,6 +198,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="add_code_owners"
@@ -228,6 +231,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="enable_require_merge_approvals"
@@ -294,6 +298,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="create_an_issue"
@@ -318,6 +323,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="submit_a_merge_request_mr"
@@ -376,6 +382,7 @@ exports[`Learn GitLab renders correctly 1`] = `
<div>
<a
class="gl-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="run_a_security_scan_using_ci_cd"
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index f54d56c3af4..4cac642bb50 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -1,139 +1,7 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { formatUtcOffset, formatTimezone } from '~/lib/utils/datetime_utility';
-import TimezoneDropdown, {
- findTimezoneByIdentifier,
-} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
+import { findTimezoneByIdentifier } from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
describe('Timezone Dropdown', () => {
- let $inputEl = null;
- let $dropdownEl = null;
- let $wrapper = null;
- const tzListSel = '.dropdown-content ul li a.is-active';
-
- const initTimezoneDropdown = (options = {}) => {
- // eslint-disable-next-line no-new
- new TimezoneDropdown({
- $inputEl,
- $dropdownEl,
- ...options,
- });
- };
-
- const findDropdownToggleText = () => $wrapper.find('.dropdown-toggle-text');
-
- describe('Initialize', () => {
- describe('with dropdown already loaded', () => {
- beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
- $wrapper = $('.dropdown');
- $inputEl = $('#schedule_cron_timezone');
- $inputEl.val('');
- $dropdownEl = $('.js-timezone-dropdown');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('can take an $inputEl in the constructor', () => {
- initTimezoneDropdown();
-
- const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
- const tzValue = 'Asia/Colombo';
-
- expect($inputEl.val()).toBe('Etc/UTC');
-
- $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click');
-
- const val = $inputEl.val();
-
- expect(val).toBe(tzValue);
- expect(val).not.toBe('Etc/UTC');
- });
-
- it('will format data array of timezones into a list of offsets', () => {
- initTimezoneDropdown();
-
- const data = $dropdownEl.data('data');
- const formatted = $wrapper.find(tzListSel).text();
-
- data.forEach((item) => {
- expect(formatted).toContain(formatTimezone(item));
- });
- });
-
- describe('when `allowEmpty` property is `false`', () => {
- beforeEach(() => {
- initTimezoneDropdown();
- });
-
- it('will default the timezone to UTC', () => {
- const tz = $inputEl.val();
-
- expect(tz).toBe('Etc/UTC');
- });
- });
-
- describe('when `allowEmpty` property is `true`', () => {
- beforeEach(() => {
- initTimezoneDropdown({
- allowEmpty: true,
- });
- });
-
- it('will default the value of the input to an empty string', () => {
- expect($inputEl.val()).toBe('');
- });
- });
- });
-
- describe('without dropdown loaded', () => {
- beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
- $wrapper = $('.dropdown');
- $inputEl = $('#schedule_cron_timezone');
- $dropdownEl = $('.js-timezone-dropdown');
- });
-
- it('will populate the list of UTC offsets after the dropdown is loaded', () => {
- expect($wrapper.find(tzListSel).length).toEqual(0);
-
- initTimezoneDropdown();
-
- expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length);
- });
-
- it('will call a provided handler when a new timezone is selected', () => {
- const onSelectTimezone = jest.fn();
-
- initTimezoneDropdown({ onSelectTimezone });
-
- $wrapper.find(tzListSel).first().trigger('click');
-
- expect(onSelectTimezone).toHaveBeenCalled();
- });
-
- it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => {
- $inputEl.val('America/St_Johns');
-
- initTimezoneDropdown({ displayFormat: (selectedItem) => formatTimezone(selectedItem) });
-
- expect(findDropdownToggleText().html()).toEqual('[UTC - 2.5] Newfoundland');
- });
-
- it('will call a provided `displayFormat` handler to format the dropdown value', () => {
- const displayFormat = jest.fn();
-
- initTimezoneDropdown({ displayFormat });
-
- $wrapper.find(tzListSel).first().trigger('click');
-
- expect(displayFormat).toHaveBeenCalled();
- });
- });
- });
-
describe('formatUtcOffset', () => {
it('will convert negative utc offsets in seconds to hours and minutes', () => {
expect(formatUtcOffset(-21600)).toEqual('- 6');
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index ed7d4ad269e..b202a148306 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -137,6 +137,8 @@ describe('Settings Panel', () => {
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findFeatureFlagsSettings = () => wrapper.findComponent({ ref: 'feature-flags-settings' });
+ const findInfrastructureSettings = () =>
+ wrapper.findComponent({ ref: 'infrastructure-settings' });
const findReleasesSettings = () => wrapper.findComponent({ ref: 'environments-settings' });
const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' });
@@ -841,6 +843,24 @@ describe('Settings Panel', () => {
});
});
});
+ describe('Infrastructure', () => {
+ describe('with feature flag', () => {
+ it('should show the infrastructure toggle', () => {
+ wrapper = mountComponent({
+ glFeatures: { splitOperationsVisibilityPermissions: true },
+ });
+
+ expect(findInfrastructureSettings().exists()).toBe(true);
+ });
+ });
+ describe('without feature flag', () => {
+ it('should not show the infrastructure toggle', () => {
+ wrapper = mountComponent({});
+
+ expect(findInfrastructureSettings().exists()).toBe(false);
+ });
+ });
+ });
describe('Releases', () => {
describe('with feature flag', () => {
it('should show the releases toggle', () => {
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 0f947e84e0f..67d0fbdd9d1 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -116,7 +116,7 @@ describe('WikiForm', () => {
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
markdownDocsPath: pageInfoPersisted.markdownHelpPath,
uploadsPath: pageInfoPersisted.uploadsPath,
- initOnAutofocus: pageInfoPersisted.persisted,
+ autofocus: pageInfoPersisted.persisted,
formFieldId: 'wiki_content',
formFieldName: 'wiki[content]',
}),
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 3b79739630d..27707f8b01a 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -253,7 +253,7 @@ describe('Pipeline editor tabs component', () => {
appStatus | editor | viz | validate | merged
${undefined} | ${true} | ${true} | ${true} | ${true}
${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${true} | ${false}
- ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false}
+ ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${true}
${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true}
`(
'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged',
diff --git a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
deleted file mode 100644
index cce8f480928..00000000000
--- a/spec/frontend/pipeline_schedules/components/pipeline_schedules_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import PipelineSchedules from '~/pipeline_schedules/components/pipeline_schedules.vue';
-import PipelineSchedulesTable from '~/pipeline_schedules/components/table/pipeline_schedules_table.vue';
-import deletePipelineScheduleMutation from '~/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql';
-import getPipelineSchedulesQuery from '~/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
-import {
- mockGetPipelineSchedulesGraphQLResponse,
- mockPipelineScheduleNodes,
- deleteMutationResponse,
-} from '../mock_data';
-
-Vue.use(VueApollo);
-
-describe('Pipeline schedules app', () => {
- let wrapper;
-
- const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
- const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
-
- const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
- const deleteMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
-
- const createMockApolloProvider = (
- requestHandlers = [[getPipelineSchedulesQuery, successHandler]],
- ) => {
- return createMockApollo(requestHandlers);
- };
-
- const createComponent = (requestHandlers) => {
- wrapper = shallowMount(PipelineSchedules, {
- provide: {
- fullPath: 'gitlab-org/gitlab',
- },
- apolloProvider: createMockApolloProvider(requestHandlers),
- });
- };
-
- const findTable = () => wrapper.findComponent(PipelineSchedulesTable);
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findModal = () => wrapper.findComponent(GlModal);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays table', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findTable().exists()).toBe(true);
- expect(findAlert().exists()).toBe(false);
- });
-
- it('fetches query and passes an array of pipeline schedules', async () => {
- createComponent();
-
- expect(successHandler).toHaveBeenCalled();
-
- await waitForPromises();
-
- expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
- });
-
- it('handles loading state', async () => {
- createComponent();
-
- expect(findLoadingIcon().exists()).toBe(true);
-
- await waitForPromises();
-
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('shows query error alert', async () => {
- createComponent([[getPipelineSchedulesQuery, failedHandler]]);
-
- await waitForPromises();
-
- expect(findAlert().text()).toBe('There was a problem fetching pipeline schedules.');
- });
-
- it('shows delete mutation error alert', async () => {
- createComponent([
- [getPipelineSchedulesQuery, successHandler],
- [deletePipelineScheduleMutation, deleteMutationHandlerFailed],
- ]);
-
- await waitForPromises();
-
- findModal().vm.$emit('primary');
-
- await waitForPromises();
-
- expect(findAlert().text()).toBe('There was a problem deleting the pipeline schedule.');
- });
-
- it('deletes pipeline schedule and refetches query', async () => {
- createComponent([
- [getPipelineSchedulesQuery, successHandler],
- [deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
- ]);
-
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
-
- await waitForPromises();
-
- const scheduleId = mockPipelineScheduleNodes[0].id;
-
- findTable().vm.$emit('showDeleteModal', scheduleId);
-
- expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
-
- findModal().vm.$emit('primary');
-
- await waitForPromises();
-
- expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
- id: scheduleId,
- });
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
- });
-
- it('modal should be visible after event', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findModal().props('visible')).toBe(false);
-
- findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
-
- await nextTick();
-
- expect(findModal().props('visible')).toBe(true);
- });
-
- it('modal should be hidden', async () => {
- createComponent();
-
- await waitForPromises();
-
- findTable().vm.$emit('showDeleteModal', mockPipelineScheduleNodes[0].id);
-
- await nextTick();
-
- expect(findModal().props('visible')).toBe(true);
-
- findModal().vm.$emit('hide');
-
- await nextTick();
-
- expect(findModal().props('visible')).toBe(false);
- });
-});
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index 3680d9d62c7..c2cb95d4320 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -2,10 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue';
-import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
-import Dag from '~/pipelines/components/dag/dag.vue';
-import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
-import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
describe('The Pipeline Tabs', () => {
let wrapper;
@@ -16,12 +12,6 @@ describe('The Pipeline Tabs', () => {
const findPipelineTab = () => wrapper.findByTestId('pipeline-tab');
const findTestsTab = () => wrapper.findByTestId('tests-tab');
- const findDagApp = () => wrapper.findComponent(Dag);
- const findFailedJobsApp = () => wrapper.findComponent(JobsApp);
- const findJobsApp = () => wrapper.findComponent(JobsApp);
- const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
- const findTestsApp = () => wrapper.findComponent(TestReports);
-
const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter');
const findJobsBadge = () => wrapper.findByTestId('builds-counter');
const findTestsBadge = () => wrapper.findByTestId('tests-counter');
@@ -43,6 +33,7 @@ describe('The Pipeline Tabs', () => {
},
stubs: {
GlTab,
+ RouterView: true,
},
}),
);
@@ -54,17 +45,16 @@ describe('The Pipeline Tabs', () => {
describe('Tabs', () => {
it.each`
- tabName | tabComponent | appComponent
- ${'Pipeline'} | ${findPipelineTab} | ${findPipelineApp}
- ${'Dag'} | ${findDagTab} | ${findDagApp}
- ${'Jobs'} | ${findJobsTab} | ${findJobsApp}
- ${'Failed Jobs'} | ${findFailedJobsTab} | ${findFailedJobsApp}
- ${'Tests'} | ${findTestsTab} | ${findTestsApp}
- `('shows $tabName tab with its associated component', ({ appComponent, tabComponent }) => {
+ tabName | tabComponent
+ ${'Pipeline'} | ${findPipelineTab}
+ ${'Dag'} | ${findDagTab}
+ ${'Jobs'} | ${findJobsTab}
+ ${'Failed Jobs'} | ${findFailedJobsTab}
+ ${'Tests'} | ${findTestsTab}
+ `('shows $tabName tab', ({ tabComponent }) => {
createComponent();
expect(tabComponent().exists()).toBe(true);
- expect(appComponent().exists()).toBe(true);
});
describe('with no failed jobs', () => {
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 57d1511d859..36bce65dd56 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -730,6 +730,7 @@ export const mockPipelineTag = () => {
},
active: false,
source: 'push',
+ name: 'Build pipeline',
created_at: '2022-02-02T15:39:04.012Z',
updated_at: '2022-02-02T15:40:59.573Z',
path: '/root/mr-widgets/-/pipelines/311',
@@ -835,6 +836,7 @@ export const mockPipelineTag = () => {
],
duration: 93,
finished_at: '2022-02-02T15:40:59.384Z',
+ event_type_name: 'Pipeline',
name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
@@ -954,6 +956,7 @@ export const mockPipelineBranch = () => {
},
active: false,
source: 'push',
+ name: 'Build pipeline',
created_at: '2022-01-14T17:40:27.866Z',
updated_at: '2022-01-14T18:02:35.850Z',
path: '/root/mr-widgets/-/pipelines/268',
@@ -1041,6 +1044,7 @@ export const mockPipelineBranch = () => {
],
duration: 75,
finished_at: '2022-01-14T18:02:35.842Z',
+ event_type_name: 'Pipeline',
name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
index d6b13da3c3a..41b020189d0 100644
--- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/utils_spec.js
@@ -1,5 +1,5 @@
import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils';
-import { TAB_QUERY_PARAM, validPipelineTabNames } from '~/pipelines/constants';
+import { validPipelineTabNames } from '~/pipelines/constants';
describe('utils functions', () => {
const jobName1 = 'build_1';
@@ -173,18 +173,25 @@ describe('utils functions', () => {
describe('getPipelineDefaultTab', () => {
const baseUrl = 'http://gitlab.com/user/multi-projects-small/-/pipelines/332/';
- it('returns null if there was no `tab` params', () => {
+ it('returns null if there is only the base url', () => {
expect(getPipelineDefaultTab(baseUrl)).toBe(null);
});
- it('returns null if there was no valid tab param', () => {
- expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=invalid`)).toBe(null);
+ it('returns null if there was no valid last url part', () => {
+ expect(getPipelineDefaultTab(`${baseUrl}something`)).toBe(null);
});
it('returns the correct tab name if present', () => {
validPipelineTabNames.forEach((tabName) => {
- expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=${tabName}`)).toBe(tabName);
+ expect(getPipelineDefaultTab(`${baseUrl}${tabName}`)).toBe(tabName);
});
});
+
+ it('returns the right value even with query params', () => {
+ const [tabName] = validPipelineTabNames;
+ expect(getPipelineDefaultTab(`${baseUrl}${tabName}?query="something"&query2="else"`)).toBe(
+ tabName,
+ );
+ });
});
});
diff --git a/spec/frontend/pipelines/pipeline_tabs_spec.js b/spec/frontend/pipelines/pipeline_tabs_spec.js
index b184ce31d20..099748a5cca 100644
--- a/spec/frontend/pipelines/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/pipeline_tabs_spec.js
@@ -1,9 +1,7 @@
-import { createAppOptions, createPipelineTabs } from '~/pipelines/pipeline_tabs';
-import { updateHistory } from '~/lib/utils/url_utility';
+import { createAppOptions } from '~/pipelines/pipeline_tabs';
jest.mock('~/lib/utils/url_utility', () => ({
removeParams: () => 'gitlab.com',
- updateHistory: jest.fn(),
joinPaths: () => {},
setUrlFragment: () => {},
}));
@@ -64,32 +62,4 @@ describe('~/pipelines/pipeline_tabs.js', () => {
expect(createAppOptions('foo', null)).toBe(null);
});
});
-
- describe('createPipelineTabs', () => {
- const title = 'Pipeline Tabs';
-
- beforeAll(() => {
- document.title = title;
- });
-
- afterAll(() => {
- document.title = '';
- });
-
- it('calls `updateHistory` with correct params', () => {
- createPipelineTabs({});
-
- expect(updateHistory).toHaveBeenCalledWith({
- title,
- url: 'gitlab.com',
- replace: true,
- });
- });
-
- it("returns early if options aren't provided", () => {
- createPipelineTabs();
-
- expect(updateHistory).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 1d66607e72b..c62898f0c83 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,3 +1,4 @@
+import { merge } from 'lodash';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
@@ -20,6 +21,7 @@ describe('Pipeline Url Component', () => {
const findCommitRefName = () => wrapper.findByTestId('commit-ref-name');
const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
+ const findPipelineNameContainer = () => wrapper.findByTestId('pipeline-name-container');
const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]');
const defaultProps = mockPipeline(projectPath);
@@ -51,7 +53,16 @@ describe('Pipeline Url Component', () => {
expect(findPipelineUrlLink().text()).toBe('#1');
});
- it('should render the commit title, commit reference and commit-short-sha', () => {
+ it('should render the pipeline name instead of commit title', () => {
+ createComponent(merge(mockPipeline(projectPath), { pipeline: { name: 'Build pipeline' } }));
+
+ expect(findCommitTitleContainer().exists()).toBe(false);
+ expect(findPipelineNameContainer().exists()).toBe(true);
+ expect(findRefName().exists()).toBe(true);
+ expect(findCommitShortSha().exists()).toBe(true);
+ });
+
+ it('should render the commit title when pipeline has no name', () => {
createComponent();
const commitWrapper = findCommitTitleContainer();
@@ -59,6 +70,7 @@ describe('Pipeline Url Component', () => {
expect(findCommitTitle(commitWrapper).exists()).toBe(true);
expect(findRefName().exists()).toBe(true);
expect(findCommitShortSha().exists()).toBe(true);
+ expect(findPipelineNameContainer().exists()).toBe(false);
});
describe('commit user avatar', () => {
@@ -142,7 +154,7 @@ describe('Pipeline Url Component', () => {
});
it('tracks commit title click', () => {
- createComponent(mockPipelineBranch());
+ createComponent(merge(mockPipelineBranch(), { pipeline: { name: null } }));
findCommitTitle(findCommitTitleContainer()).vm.$emit('click');
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index 26e61efc4f6..a70ef10aa7b 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -13,11 +13,7 @@ import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
jest.mock('~/flash');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('Pipelines Actions dropdown', () => {
let wrapper;
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index 736d149f06d..974650a2c7c 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -19,6 +19,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<gl-button-stub
buttontextclasses=""
category="primary"
+ data-qa-selector="delete_button"
icon=""
role="button"
size="medium"
@@ -102,6 +103,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
</p>
<gl-form-input-stub
+ data-qa-selector="confirm_name_field"
id="confirm_name_input"
name="confirm_name_input"
type="text"
diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
index 26495fbcf83..ac020fe6915 100644
--- a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
@@ -20,6 +20,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
<gl-button-stub
buttontextclasses=""
category="primary"
+ data-qa-selector="delete_button"
icon=""
role="button"
size="medium"
@@ -103,6 +104,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
</p>
<gl-form-input-stub
+ data-qa-selector="confirm_name_field"
id="confirm_name_input"
name="confirm_name_input"
type="text"
diff --git a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
index f50dd393174..16b4493c622 100644
--- a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
+++ b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
@@ -71,7 +71,7 @@ describe('New project push tip popover', () => {
it('displays a link to open the push command help page reference', () => {
expect(findHelpLink().attributes().href).toBe(
- `${workingWithProjectsHelpPath}#push-to-create-a-new-project`,
+ `${workingWithProjectsHelpPath}#create-a-new-project-with-git-push`,
);
});
});
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index e3aaf760d1e..d8876349c5e 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -8,6 +8,12 @@ import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/u
import Component from '~/projects/pipelines/charts/components/app.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import API from '~/api';
+import { mockTracking } from 'helpers/tracking_helper';
+import {
+ SNOWPLOW_DATA_SOURCE,
+ SNOWPLOW_LABEL,
+ SNOWPLOW_SCHEMA,
+} from '~/projects/pipelines/charts/constants';
jest.mock('~/lib/utils/url_utility');
@@ -125,21 +131,59 @@ describe('ProjectsPipelinesChartsApp', () => {
});
describe('event tracking', () => {
- it.each`
- testId | event
- ${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
- ${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
- ${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
- ${'time-to-restore-service-tab'} | ${'p_analytics_ci_cd_time_to_restore_service'}
- ${'change-failure-rate-tab'} | ${'p_analytics_ci_cd_change_failure_rate'}
- `('tracks the $event event when clicked', ({ testId, event }) => {
- jest.spyOn(API, 'trackRedisHllUserEvent');
+ describe('RedisHLL events', () => {
+ it.each`
+ testId | event
+ ${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
+ ${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
+ ${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
+ ${'time-to-restore-service-tab'} | ${'p_analytics_ci_cd_time_to_restore_service'}
+ ${'change-failure-rate-tab'} | ${'p_analytics_ci_cd_change_failure_rate'}
+ `('tracks the $event event when clicked', ({ testId, event }) => {
+ const trackApiSpy = jest.spyOn(API, 'trackRedisHllUserEvent');
+
+ expect(trackApiSpy).not.toHaveBeenCalled();
+
+ wrapper.findByTestId(testId).vm.$emit('click');
+
+ expect(trackApiSpy).toHaveBeenCalledWith(event);
+ });
+ });
- expect(API.trackRedisHllUserEvent).not.toHaveBeenCalled();
+ describe('Snowplow events', () => {
+ it.each`
+ testId | event
+ ${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
+ ${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
+ ${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
+ `('tracks the $event event when clicked', ({ testId, event }) => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ wrapper.findByTestId(testId).vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', {
+ label: SNOWPLOW_LABEL,
+ context: {
+ schema: SNOWPLOW_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: SNOWPLOW_DATA_SOURCE,
+ },
+ },
+ });
+ });
- wrapper.findByTestId(testId).vm.$emit('click');
+ it.each`
+ tab
+ ${'time-to-restore-service-tab'}
+ ${'change-failure-rate-tab'}
+ `('does not track when tab $tab is clicked', ({ tab }) => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event);
+ wrapper.findByTestId(tab).vm.$emit('click');
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index bf4026b65db..27065a704e2 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -12,7 +12,11 @@ import {
import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { sprintf } from '~/locale';
-import { branchProtectionsMockResponse } from './mock_data';
+import {
+ branchProtectionsMockResponse,
+ approvalRulesMock,
+ statusChecksRulesMock,
+} from './mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
@@ -34,6 +38,7 @@ describe('View branch rules', () => {
const projectPath = 'test/testing';
const protectedBranchesPath = 'protected/branches';
const approvalRulesPath = 'approval/rules';
+ const statusChecksPath = 'status/checks';
const branchProtectionsMockRequestHandler = jest
.fn()
.mockResolvedValue(branchProtectionsMockResponse);
@@ -43,7 +48,7 @@ describe('View branch rules', () => {
wrapper = shallowMountExtended(RuleView, {
apolloProvider: fakeApollo,
- provide: { projectPath, protectedBranchesPath, approvalRulesPath },
+ provide: { projectPath, protectedBranchesPath, approvalRulesPath, statusChecksPath },
});
await waitForPromises();
@@ -59,6 +64,7 @@ describe('View branch rules', () => {
const findBranchProtections = () => wrapper.findAllComponents(Protection);
const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription);
const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
+ const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
it('gets the branch param from url and renders it in the view', () => {
expect(util.getParameterByName).toHaveBeenCalledWith('branch');
@@ -105,9 +111,21 @@ describe('View branch rules', () => {
expect(findApprovalsTitle().exists()).toBe(true);
expect(findBranchProtections().at(2).props()).toMatchObject({
- header: sprintf(I18N.approvalsHeader, { total: 0 }),
+ header: sprintf(I18N.approvalsHeader, { total: 3 }),
headerLinkHref: approvalRulesPath,
headerLinkTitle: I18N.manageApprovalsLinkTitle,
+ approvals: approvalRulesMock,
+ });
+ });
+
+ it('renders a branch protection component for status checks', () => {
+ expect(findStatusChecksTitle().exists()).toBe(true);
+
+ expect(findBranchProtections().at(3).props()).toMatchObject({
+ header: sprintf(I18N.statusChecksHeader, { total: 2 }),
+ headerLinkHref: statusChecksPath,
+ headerLinkTitle: I18N.statusChecksLinkTitle,
+ statusChecks: statusChecksRulesMock,
});
});
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
index c3f573061da..c07d4673344 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
@@ -1,29 +1,34 @@
const usersMock = [
{
+ id: '123',
username: 'usr1',
webUrl: 'http://test.test/usr1',
name: 'User 1',
avatarUrl: 'http://test.test/avt1.png',
},
{
+ id: '456',
username: 'usr2',
webUrl: 'http://test.test/usr2',
name: 'User 2',
avatarUrl: 'http://test.test/avt2.png',
},
{
+ id: '789',
username: 'usr3',
webUrl: 'http://test.test/usr3',
name: 'User 3',
avatarUrl: 'http://test.test/avt3.png',
},
{
+ id: '987',
username: 'usr4',
webUrl: 'http://test.test/usr4',
name: 'User 4',
avatarUrl: 'http://test.test/avt4.png',
},
{
+ id: '654',
username: 'usr5',
webUrl: 'http://test.test/usr5',
name: 'User 5',
@@ -40,6 +45,22 @@ const approvalsRequired = 3;
const groupsMock = [{ name: 'test_group_1' }, { name: 'test_group_2' }];
+export const approvalRulesMock = [
+ {
+ __typename: 'ApprovalProjectRule',
+ id: '123',
+ name: 'test',
+ type: 'REGULAR',
+ eligibleApprovers: { nodes: usersMock },
+ approvalsRequired,
+ },
+];
+
+export const statusChecksRulesMock = [
+ { __typename: 'StatusCheckRule', id: '123', name: 'test', externalUrl: 'https://test.test' },
+ { __typename: 'StatusCheckRule', id: '456', name: 'test 2', externalUrl: 'https://test2.test2' },
+];
+
export const protectionPropsMock = {
header: 'Test protection',
headerLinkTitle: 'Test link title',
@@ -47,13 +68,8 @@ export const protectionPropsMock = {
roles: accessLevelsMock,
users: usersMock,
groups: groupsMock,
- approvals: [
- {
- name: 'test',
- eligibleApprovers: { nodes: usersMock },
- approvalsRequired,
- },
- ],
+ approvals: approvalRulesMock,
+ statusChecks: statusChecksRulesMock,
};
export const protectionRowPropsMock = {
@@ -61,6 +77,7 @@ export const protectionRowPropsMock = {
users: usersMock,
accessLevels: accessLevelsMock,
approvalsRequired,
+ statusCheckUrl: statusChecksRulesMock[0].externalUrl,
};
export const accessLevelsMockResponse = [
@@ -116,6 +133,14 @@ export const branchProtectionsMockResponse = {
edges: accessLevelsMockResponse,
},
},
+ approvalRules: {
+ __typename: 'ApprovalProjectRuleConnection',
+ nodes: approvalRulesMock,
+ },
+ externalStatusChecks: {
+ __typename: 'ExternalStatusCheckConnection',
+ nodes: statusChecksRulesMock,
+ },
},
{
__typename: 'BranchRule',
@@ -133,6 +158,14 @@ export const branchProtectionsMockResponse = {
edges: [],
},
},
+ approvalRules: {
+ __typename: 'ApprovalProjectRuleConnection',
+ nodes: [],
+ },
+ externalStatusChecks: {
+ __typename: 'ExternalStatusCheckConnection',
+ nodes: [],
+ },
},
],
},
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
index b0a69bedd3e..a98b156f94e 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
@@ -27,6 +27,7 @@ describe('Branch rule protection row', () => {
const findAccessLevels = () => wrapper.findAllByTestId('access-level');
const findApprovalsRequired = () =>
wrapper.findByText(`${protectionRowPropsMock.approvalsRequired} approvals required`);
+ const findStatusChecksUrl = () => wrapper.findByText(protectionRowPropsMock.statusCheckUrl);
it('renders a title', () => {
expect(findTitle().exists()).toBe(true);
@@ -68,4 +69,8 @@ describe('Branch rule protection row', () => {
it('renders the number of approvals required', () => {
expect(findApprovalsRequired().exists()).toBe(true);
});
+
+ it('renders status checks URL', () => {
+ expect(findStatusChecksUrl().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
index e2fbb4f5bbb..caf967b4257 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
@@ -65,4 +65,15 @@ describe('Branch rule protection', () => {
approvalsRequired: approval.approvalsRequired,
});
});
+
+ it('renders a protection row for status checks', () => {
+ const statusCheck = protectionPropsMock.statusChecks[0];
+ expect(findProtectionRows().at(4).props()).toMatchObject({
+ title: statusCheck.name,
+ showDivider: false,
+ statusCheckUrl: statusCheck.externalUrl,
+ });
+
+ expect(findProtectionRows().at(5).props('showDivider')).toBe(true);
+ });
});
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index 6e639f895a8..e091f3e25c3 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,18 +1,9 @@
-import Vue, { nextTick } from 'vue';
-import { GlAlert } from '@gitlab/ui';
-import VueApollo from 'vue-apollo';
-import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
-import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
import { getTransferLocations } from '~/api/projects_api';
-import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/api/projects_api', () => ({
getTransferLocations: jest.fn(),
@@ -21,68 +12,36 @@ jest.mock('~/api/projects_api', () => ({
describe('Transfer project form', () => {
let wrapper;
- const projectId = '1';
+ const resourceId = '1';
const confirmButtonText = 'Confirm';
const confirmationPhrase = 'You must construct additional pylons!';
- Vue.use(VueApollo);
-
- const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
- const mockResolvedGetTransferLocations = ({
- data = transferLocationsResponsePage1,
- page = '1',
- nextPage = '2',
- prevPage = null,
- } = {}) => {
- getTransferLocations.mockResolvedValueOnce({
- data,
- headers: {
- 'x-per-page': '2',
- 'x-page': page,
- 'x-total': '4',
- 'x-total-pages': '2',
- 'x-next-page': nextPage,
- 'x-prev-page': prevPage,
- },
- });
- };
- const mockRejectedGetTransferLocations = () => {
- const error = new Error();
-
- getTransferLocations.mockRejectedValueOnce(error);
- };
-
- const createComponent = ({
- requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
- } = {}) => {
+ const createComponent = () => {
wrapper = shallowMountExtended(TransferProjectForm, {
provide: {
- projectId,
+ resourceId,
},
propsData: {
confirmButtonText,
confirmationPhrase,
},
- apolloProvider: createMockApollo(requestHandlers),
});
};
- const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
- const showNamespaceSelect = async () => {
- findNamespaceSelect().vm.$emit('show');
- await waitForPromises();
- };
+ const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
});
- it('renders the namespace selector', () => {
+ it('renders the namespace selector and passes `groupTransferLocationsApiMethod` prop', () => {
createComponent();
- expect(findNamespaceSelect().exists()).toBe(true);
+ expect(findTransferLocations().exists()).toBe(true);
+
+ findTransferLocations().props('groupTransferLocationsApiMethod')();
+ expect(getTransferLocations).toHaveBeenCalled();
});
it('renders the confirm button', () => {
@@ -100,220 +59,29 @@ describe('Transfer project form', () => {
describe('with a selected namespace', () => {
const [selectedItem] = transferLocationsResponsePage1;
- const arrange = async () => {
- mockResolvedGetTransferLocations();
+ beforeEach(() => {
createComponent();
- await showNamespaceSelect();
- findNamespaceSelect().vm.$emit('select', selectedItem);
- };
+ findTransferLocations().vm.$emit('input', selectedItem);
+ });
- it('emits the `selectNamespace` event when a namespace is selected', async () => {
- await arrange();
+ it('sets `value` prop on `TransferLocations` component', () => {
+ expect(findTransferLocations().props('value')).toEqual(selectedItem);
+ });
+ it('emits the `selectTransferLocation` event when a namespace is selected', async () => {
const args = [selectedItem.id];
- expect(wrapper.emitted('selectNamespace')).toEqual([args]);
+ expect(wrapper.emitted('selectTransferLocation')).toEqual([args]);
});
it('enables the confirm button', async () => {
- await arrange();
-
expect(findConfirmDanger().attributes('disabled')).toBeUndefined();
});
it('clicking the confirm button emits the `confirm` event', async () => {
- await arrange();
-
findConfirmDanger().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toBeDefined();
});
});
-
- describe('when `NamespaceSelect` is opened', () => {
- it('fetches user and group namespaces and passes correct props to `NamespaceSelect` component', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
-
- expect(findNamespaceSelect().props()).toMatchObject({
- userNamespaces: [
- {
- id: getIdFromGraphQLId(namespace.id),
- humanName: namespace.fullName,
- },
- ],
- groupNamespaces: transferLocationsResponsePage1.map(({ id, full_name: humanName }) => ({
- id,
- humanName,
- })),
- hasNextPageOfGroups: true,
- isLoading: false,
- isSearchLoading: false,
- shouldFilterNamespaces: false,
- });
- });
-
- describe('when namespaces have already been fetched', () => {
- beforeEach(async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- });
-
- it('does not fetch namespaces', async () => {
- getTransferLocations.mockClear();
- defaultQueryHandler.mockClear();
-
- await showNamespaceSelect();
-
- expect(getTransferLocations).not.toHaveBeenCalled();
- expect(defaultQueryHandler).not.toHaveBeenCalled();
- });
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays error alert', async () => {
- mockRejectedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
-
- describe('when `currentUser` GraphQL query fails', () => {
- it('displays error alert', async () => {
- mockResolvedGetTransferLocations();
- const error = new Error();
- createComponent({
- requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
- });
- await showNamespaceSelect();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
- });
-
- describe('when `search` event is fired', () => {
- const arrange = async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockResolvedGetTransferLocations();
- findNamespaceSelect().vm.$emit('search', 'foo');
- await nextTick();
- };
-
- it('sets `isSearchLoading` prop to `true`', async () => {
- await arrange();
-
- expect(findNamespaceSelect().props('isSearchLoading')).toBe(true);
- });
-
- it('passes `search` param to API call', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(getTransferLocations).toHaveBeenCalledWith(
- projectId,
- expect.objectContaining({ search: 'foo' }),
- );
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays dismissible error alert', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockRejectedGetTransferLocations();
- findNamespaceSelect().vm.$emit('search', 'foo');
- await waitForPromises();
-
- const alert = findAlert();
-
- expect(alert.exists()).toBe(true);
-
- alert.vm.$emit('dismiss');
- await nextTick();
-
- expect(alert.exists()).toBe(false);
- });
- });
- });
-
- describe('when `load-more-groups` event is fired', () => {
- const arrange = async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
-
- mockResolvedGetTransferLocations({
- data: transferLocationsResponsePage2,
- page: '2',
- nextPage: null,
- prevPage: '1',
- });
-
- findNamespaceSelect().vm.$emit('load-more-groups');
- await nextTick();
- };
-
- it('sets `isLoading` prop to `true`', async () => {
- await arrange();
-
- expect(findNamespaceSelect().props('isLoading')).toBe(true);
- });
-
- it('passes `page` param to API call', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(getTransferLocations).toHaveBeenCalledWith(
- projectId,
- expect.objectContaining({ page: 2 }),
- );
- });
-
- it('updates `groupNamespaces` prop with new groups', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(findNamespaceSelect().props('groupNamespaces')).toMatchObject(
- [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
- ({ id, full_name: humanName }) => ({
- id,
- humanName,
- }),
- ),
- );
- });
-
- it('updates `hasNextPageOfGroups` prop', async () => {
- await arrange();
-
- await waitForPromises();
-
- expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false);
- });
-
- describe('when `getTransferLocations` API call fails', () => {
- it('displays error alert', async () => {
- mockResolvedGetTransferLocations();
- createComponent();
- await showNamespaceSelect();
- mockRejectedGetTransferLocations();
- findNamespaceSelect().vm.$emit('load-more-groups');
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- });
- });
- });
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index 4603436c40a..6369f04781f 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -52,6 +52,10 @@ describe('Branch rules app', () => {
expect(findAllBranchRules().at(0).props('name')).toBe(nodes[0].name);
+ expect(findAllBranchRules().at(0).props('branchProtection')).toEqual(nodes[0].branchProtection);
+
expect(findAllBranchRules().at(1).props('name')).toBe(nodes[1].name);
+
+ expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection);
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index 2bc705f538b..2aa93fd0e28 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -2,7 +2,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BranchRule, {
i18n,
} from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
-import { branchRuleProvideMock, branchRulePropsMock } from '../mock_data';
+import { sprintf, n__ } from '~/locale';
+import {
+ branchRuleProvideMock,
+ branchRulePropsMock,
+ branchRuleWithoutDetailsPropsMock,
+} from '../mock_data';
describe('Branch rule', () => {
let wrapper;
@@ -15,7 +20,6 @@ describe('Branch rule', () => {
};
const findDefaultBadge = () => wrapper.findByText(i18n.defaultLabel);
- const findProtectedBadge = () => wrapper.findByText(i18n.protectedLabel);
const findBranchName = () => wrapper.findByText(branchRulePropsMock.name);
const findProtectionDetailsList = () => wrapper.findByRole('list');
const findProtectionDetailsListItems = () => wrapper.findAllByRole('listitem');
@@ -28,33 +32,36 @@ describe('Branch rule', () => {
});
describe('badges', () => {
- it('renders both default and protected badges', () => {
+ it('renders default badge', () => {
expect(findDefaultBadge().exists()).toBe(true);
- expect(findProtectedBadge().exists()).toBe(true);
});
it('does not render default badge if isDefault is set to false', () => {
createComponent({ isDefault: false });
expect(findDefaultBadge().exists()).toBe(false);
});
-
- it('does not render protected badge if isProtected is set to false', () => {
- createComponent({ isProtected: false });
- expect(findProtectedBadge().exists()).toBe(false);
- });
});
- it('does not render the protection details list of no details are present', () => {
- createComponent({ approvalDetails: null });
+ it('does not render the protection details list if no details are present', () => {
+ createComponent(branchRuleWithoutDetailsPropsMock);
expect(findProtectionDetailsList().exists()).toBe(false);
});
it('renders the protection details list items', () => {
- expect(findProtectionDetailsListItems().at(0).text()).toBe(
- branchRulePropsMock.approvalDetails[0],
+ expect(findProtectionDetailsListItems()).toHaveLength(wrapper.vm.approvalDetails.length);
+ expect(findProtectionDetailsListItems().at(0).text()).toBe(i18n.allowForcePush);
+ expect(findProtectionDetailsListItems().at(1).text()).toBe(i18n.codeOwnerApprovalRequired);
+ expect(findProtectionDetailsListItems().at(2).text()).toMatchInterpolatedText(
+ sprintf(i18n.statusChecks, {
+ total: branchRulePropsMock.statusChecksTotal,
+ subject: n__('check', 'checks', branchRulePropsMock.statusChecksTotal),
+ }),
);
- expect(findProtectionDetailsListItems().at(1).text()).toBe(
- branchRulePropsMock.approvalDetails[1],
+ expect(findProtectionDetailsListItems().at(3).text()).toMatchInterpolatedText(
+ sprintf(i18n.approvalRules, {
+ total: branchRulePropsMock.approvalRulesTotal,
+ subject: n__('rule', 'rules', branchRulePropsMock.approvalRulesTotal),
+ }),
);
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index bac82992c4d..8aa03a12996 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -8,10 +8,36 @@ export const branchRulesMockResponse = {
nodes: [
{
name: 'main',
+ isDefault: true,
+ branchProtection: {
+ allowForcePush: true,
+ codeOwnerApprovalRequired: true,
+ },
+ approvalRules: {
+ nodes: [{ id: 1 }],
+ __typename: 'ApprovalProjectRuleConnection',
+ },
+ externalStatusChecks: {
+ nodes: [{ id: 1 }, { id: 2 }],
+ __typename: 'BranchRule',
+ },
__typename: 'BranchRule',
},
{
name: 'test-*',
+ isDefault: false,
+ branchProtection: {
+ allowForcePush: false,
+ codeOwnerApprovalRequired: false,
+ },
+ approvalRules: {
+ nodes: [],
+ __typename: 'ApprovalProjectRuleConnection',
+ },
+ externalStatusChecks: {
+ nodes: [],
+ __typename: 'BranchRule',
+ },
__typename: 'BranchRule',
},
],
@@ -31,6 +57,21 @@ export const branchRuleProvideMock = {
export const branchRulePropsMock = {
name: 'main',
isDefault: true,
- isProtected: true,
- approvalDetails: ['requires approval from TEST', '2 status checks'],
+ branchProtection: {
+ allowForcePush: true,
+ codeOwnerApprovalRequired: true,
+ },
+ approvalRulesTotal: 1,
+ statusChecksTotal: 2,
+};
+
+export const branchRuleWithoutDetailsPropsMock = {
+ name: 'main',
+ isDefault: false,
+ branchProtection: {
+ allowForcePush: false,
+ codeOwnerApprovalRequired: false,
+ },
+ approvalRulesTotal: 0,
+ statusChecksTotal: 0,
};
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index 7c3f4e76ae5..f9762491507 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -64,20 +64,14 @@ describe('ServiceDeskSetting', () => {
});
});
- describe('when customEmailEnabled', () => {
- beforeEach(() => {
- wrapper = createComponent({
- props: { customEmailEnabled: true },
- });
- });
+ describe('service desk email "from" name', () => {
+ it('service desk e-mail "from" name input appears', () => {
+ wrapper = createComponent();
- it('should not display help text', () => {
- expect(findSuffixFormGroup().text()).not.toContain(
- 'To add a custom suffix, set up a Service Desk email address',
- );
- expect(findSuffixFormGroup().text()).toContain(
- 'Add a suffix to Service Desk email address',
- );
+ const input = wrapper.findByTestId('email-from-name');
+
+ expect(input.exists()).toBe(true);
+ expect(input.attributes('disabled')).toBeUndefined();
});
});
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index 1ff5766b074..b1e9d8d1256 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -292,6 +292,42 @@ describe('Release edit component', () => {
});
});
+ describe('remove button state', () => {
+ describe('when there is only one link', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: release.assets.links.slice(0, 1),
+ },
+ },
+ });
+ });
+
+ it('remove asset link button should not be present', () => {
+ expect(wrapper.find('.remove-button').exists()).toBe(false);
+ });
+ });
+
+ describe('when there are multiple links', () => {
+ beforeEach(() => {
+ factory({
+ release: {
+ ...release,
+ assets: {
+ links: release.assets.links.slice(0, 2),
+ },
+ },
+ });
+ });
+
+ it('remove asset link button should be visible', () => {
+ expect(wrapper.find('.remove-button').exists()).toBe(true);
+ });
+ });
+ });
+
describe('empty state', () => {
describe('when the release fetched from the API has no links', () => {
beforeEach(() => {
@@ -325,12 +361,6 @@ describe('Release edit component', () => {
it('does not call the addEmptyAssetLink store method when the component is created', () => {
expect(actions.addEmptyAssetLink).not.toHaveBeenCalled();
});
-
- it('calls addEmptyAssetLink when the final link is deleted by the user', () => {
- wrapper.find('.remove-button').vm.$emit('click');
-
- expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1);
- });
});
});
});
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
deleted file mode 100644
index 962ff068b92..00000000000
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ /dev/null
@@ -1,151 +0,0 @@
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
-import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
-import { getStoreConfig } from '~/reports/codequality_report/store';
-import { STATUS_NOT_FOUND } from '~/reports/constants';
-import { parsedReportIssues } from './mock_data';
-
-Vue.use(Vuex);
-
-describe('Grouped code quality reports app', () => {
- let wrapper;
- let mockStore;
-
- const PATHS = {
- codequalityHelpPath: 'codequality_help.html',
- baseBlobPath: 'base/blob/path/',
- headBlobPath: 'head/blob/path/',
- };
-
- const mountComponent = (props = {}) => {
- wrapper = mount(GroupedCodequalityReportsApp, {
- store: mockStore,
- propsData: {
- ...PATHS,
- ...props,
- },
- });
- };
-
- const findWidget = () => wrapper.find('.js-codequality-widget');
- const findIssueBody = () => wrapper.findComponent(CodequalityIssueBody);
-
- beforeEach(() => {
- const { state, ...storeConfig } = getStoreConfig();
- mockStore = new Vuex.Store({
- ...storeConfig,
- actions: {
- setPaths: () => {},
- fetchReports: () => {},
- },
- state: {
- ...state,
- ...PATHS,
- },
- });
-
- mountComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when it is loading reports', () => {
- beforeEach(() => {
- mockStore.state.isLoading = true;
- });
-
- it('should render loading text', () => {
- expect(findWidget().text()).toEqual('Loading Code quality report');
- });
- });
-
- describe('when base and head reports are loaded and compared', () => {
- describe('with no issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = [];
- mockStore.state.resolvedIssues = [];
- });
-
- it('renders no changes text', () => {
- expect(findWidget().text()).toEqual('No changes to code quality');
- });
- });
-
- describe('with issues', () => {
- describe('with new issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = parsedReportIssues.newIssues;
- mockStore.state.resolvedIssues = [];
- });
-
- it('renders summary text', () => {
- expect(findWidget().text()).toContain('Code quality degraded');
- });
-
- it('renders custom codequality issue body', () => {
- expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.newIssues[0]);
- });
- });
-
- describe('with resolved issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = [];
- mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
- });
-
- it('renders summary text', () => {
- expect(findWidget().text()).toContain('Code quality improved');
- });
-
- it('renders custom codequality issue body', () => {
- expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.resolvedIssues[0]);
- });
- });
-
- describe('with new and resolved issues', () => {
- beforeEach(() => {
- mockStore.state.newIssues = parsedReportIssues.newIssues;
- mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
- });
-
- it('renders summary text', () => {
- expect(findWidget().text()).toContain(
- 'Code quality scanning detected 2 changes in merged results',
- );
- });
-
- it('renders custom codequality issue body', () => {
- expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.newIssues[0]);
- });
- });
- });
- });
-
- describe('on error', () => {
- beforeEach(() => {
- mockStore.state.hasError = true;
- });
-
- it('renders error text', () => {
- expect(findWidget().text()).toContain('Failed to load Code quality report');
- });
-
- it('does not render a help icon', () => {
- expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(false);
- });
-
- describe('when base report was not found', () => {
- beforeEach(() => {
- mockStore.state.status = STATUS_NOT_FOUND;
- });
-
- it('renders a help icon with more information', () => {
- expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
index 71f1a0f4de0..1878b9f44b2 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -28,7 +28,6 @@ describe('Codequality Reports actions', () => {
baseBlobPath: 'baseBlobPath',
headBlobPath: 'headBlobPath',
reportsPath: 'reportsPath',
- helpPath: 'codequalityHelpPath',
};
return testAction(
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
index 111757e2d30..311a67a3e31 100644
--- a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
@@ -13,7 +13,7 @@ Object {
exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
Object {
- "component": "TestIssueBody",
+ "component": "CodequalityIssueBody",
"iconComponent": "IssueStatusIcon",
"isNew": false,
"issue": Object {
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js
index 6c0275dc47d..cacbde590d6 100644
--- a/spec/frontend/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/reports/components/grouped_issues_list_spec.js
@@ -74,7 +74,7 @@ describe('Grouped Issues List', () => {
createComponent({
propsData: {
resolvedIssues: [{ name: 'foo' }],
- component: 'TestIssueBody',
+ component: 'CodequalityIssueBody',
},
stubs: {
ReportItem,
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js
index b52c163eb26..60c7e5f2b44 100644
--- a/spec/frontend/reports/components/report_item_spec.js
+++ b/spec/frontend/reports/components/report_item_spec.js
@@ -10,7 +10,7 @@ describe('ReportItem', () => {
const wrapper = shallowMount(ReportItem, {
propsData: {
issue: { foo: 'bar' },
- component: componentNames.TestIssueBody,
+ component: componentNames.CodequalityIssueBody,
status: STATUS_SUCCESS,
showReportSectionStatusIcon: false,
},
@@ -23,7 +23,7 @@ describe('ReportItem', () => {
const wrapper = shallowMount(ReportItem, {
propsData: {
issue: { foo: 'bar' },
- component: componentNames.TestIssueBody,
+ component: componentNames.CodequalityIssueBody,
status: STATUS_SUCCESS,
},
});
diff --git a/spec/frontend/reports/grouped_test_report/components/modal_spec.js b/spec/frontend/reports/grouped_test_report/components/modal_spec.js
deleted file mode 100644
index e8564d2428d..00000000000
--- a/spec/frontend/reports/grouped_test_report/components/modal_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-
-import ReportsModal from '~/reports/grouped_test_report/components/modal.vue';
-import state from '~/reports/grouped_test_report/store/state';
-import CodeBlock from '~/vue_shared/components/code_block.vue';
-
-const StubbedGlModal = { template: '<div><slot></slot></div>', name: 'GlModal', props: ['title'] };
-
-describe('Grouped Test Reports Modal', () => {
- const modalDataStructure = state().modal.data;
- const title = 'Test#sum when a is 1 and b is 2 returns summary';
-
- // populate data
- modalDataStructure.execution_time.value = 0.009411;
- modalDataStructure.system_output.value = 'Failure/Error: is_expected.to eq(3)\n\n';
- modalDataStructure.filename.value = {
- text: 'link',
- path: '/file/path',
- };
-
- let wrapper;
-
- beforeEach(() => {
- wrapper = extendedWrapper(
- shallowMount(ReportsModal, {
- propsData: {
- title,
- modalData: modalDataStructure,
- visible: true,
- },
- stubs: { GlModal: StubbedGlModal, GlSprintf },
- }),
- );
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders code block', () => {
- expect(wrapper.findComponent(CodeBlock).props().code).toEqual(
- modalDataStructure.system_output.value,
- );
- });
-
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
-
- expect(link.attributes().href).toEqual(modalDataStructure.filename.value.path);
-
- expect(link.text()).toEqual(modalDataStructure.filename.value.text);
- });
-
- it('renders seconds', () => {
- expect(wrapper.text()).toContain(`${modalDataStructure.execution_time.value} s`);
- });
-
- it('render title', () => {
- expect(wrapper.findComponent(StubbedGlModal).props().title).toEqual(title);
- });
-
- it('re-emits hide event', () => {
- wrapper.findComponent(StubbedGlModal).vm.$emit('hide');
- expect(wrapper.emitted().hide).toEqual([[]]);
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js b/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
deleted file mode 100644
index 8a854a92ad7..00000000000
--- a/spec/frontend/reports/grouped_test_report/components/test_issue_body_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { GlBadge, GlButton } 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 IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
-import TestIssueBody from '~/reports/grouped_test_report/components/test_issue_body.vue';
-import { failedIssue, successIssue } from '../../mock_data/mock_data';
-
-Vue.use(Vuex);
-
-describe('Test issue body', () => {
- let wrapper;
- let store;
-
- const findDescription = () => wrapper.findByTestId('test-issue-body-description');
- const findStatusIcon = () => wrapper.findComponent(IssueStatusIcon);
- const findBadge = () => wrapper.findComponent(GlBadge);
-
- const actionSpies = {
- openModal: jest.fn(),
- };
-
- const createComponent = ({ issue = failedIssue } = {}) => {
- store = new Vuex.Store({
- actions: actionSpies,
- });
-
- wrapper = extendedWrapper(
- shallowMount(TestIssueBody, {
- store,
- propsData: {
- issue,
- },
- stubs: {
- GlBadge,
- GlButton,
- IssueStatusIcon,
- },
- }),
- );
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when issue has failed status', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders issue name', () => {
- expect(findDescription().text()).toContain(failedIssue.name);
- });
-
- it('renders failed status icon', () => {
- expect(findStatusIcon().props('status')).toBe('failed');
- });
-
- describe('when issue has recent failures', () => {
- it('renders recent failures badge', () => {
- expect(findBadge().exists()).toBe(true);
- });
- });
- });
-
- describe('when issue has success status', () => {
- beforeEach(() => {
- createComponent({ issue: successIssue });
- });
-
- it('does not render recent failures', () => {
- expect(findBadge().exists()).toBe(false);
- });
-
- it('renders issue name', () => {
- expect(findDescription().text()).toBe(successIssue.name);
- });
-
- it('renders success status icon', () => {
- expect(findStatusIcon().props('status')).toBe('success');
- });
- });
-
- describe('when clicking on an issue', () => {
- it('calls openModal action', () => {
- createComponent();
- wrapper.findComponent(GlButton).trigger('click');
-
- expect(actionSpies.openModal).toHaveBeenCalledWith(expect.any(Object), {
- issue: failedIssue,
- });
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js b/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
deleted file mode 100644
index 90edb27d1d6..00000000000
--- a/spec/frontend/reports/grouped_test_report/grouped_test_reports_app_spec.js
+++ /dev/null
@@ -1,355 +0,0 @@
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import Api from '~/api';
-import GroupedTestReportsApp from '~/reports/grouped_test_report/grouped_test_reports_app.vue';
-import { getStoreConfig } from '~/reports/grouped_test_report/store';
-
-import { failedReport } from '../mock_data/mock_data';
-import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
-import newErrorsTestReports from '../mock_data/new_errors_report.json';
-import newFailedTestReports from '../mock_data/new_failures_report.json';
-import successTestReports from '../mock_data/no_failures_report.json';
-import recentFailuresTestReports from '../mock_data/recent_failures_report.json';
-import resolvedFailures from '../mock_data/resolved_failures.json';
-
-jest.mock('~/api.js');
-
-Vue.use(Vuex);
-
-describe('Grouped test reports app', () => {
- const endpoint = 'endpoint.json';
- const headBlobPath = '/blob/path';
- const pipelinePath = '/path/to/pipeline';
- let wrapper;
- let mockStore;
-
- const mountComponent = ({ props = { pipelinePath } } = {}) => {
- wrapper = mount(GroupedTestReportsApp, {
- store: mockStore,
- propsData: {
- endpoint,
- headBlobPath,
- pipelinePath,
- ...props,
- },
- });
- };
-
- const setReports = (reports) => {
- mockStore.state.status = reports.status;
- mockStore.state.summary = reports.summary;
- mockStore.state.reports = reports.suites;
- };
-
- const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]');
- const findExpandButton = () => wrapper.find('[data-testid="report-section-expand-button"]');
- const findFullTestReportLink = () => wrapper.find('[data-testid="group-test-reports-full-link"]');
- const findSummaryDescription = () => wrapper.find('[data-testid="summary-row-description"]');
- const findIssueListUnresolvedHeading = () => wrapper.find('[data-testid="unresolvedHeading"]');
- const findIssueListResolvedHeading = () => wrapper.find('[data-testid="resolvedHeading"]');
- const findIssueDescription = () => wrapper.find('[data-testid="test-issue-body-description"]');
- const findIssueRecentFailures = () =>
- wrapper.find('[data-testid="test-issue-body-recent-failures"]');
- const findAllIssueDescriptions = () =>
- wrapper.findAll('[data-testid="test-issue-body-description"]');
-
- beforeEach(() => {
- mockStore = new Vuex.Store({
- ...getStoreConfig(),
- actions: {
- fetchReports: () => {},
- setPaths: () => {},
- },
- });
- mountComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('with success result', () => {
- beforeEach(() => {
- setReports(successTestReports);
- mountComponent();
- });
-
- it('renders success summary text', () => {
- expect(findHeader().text()).toBe(
- 'Test summary contained no changed test results out of 11 total tests',
- );
- });
- });
-
- describe('`View full report` button', () => {
- it('should render the full test report link', () => {
- const fullTestReportLink = findFullTestReportLink();
-
- expect(fullTestReportLink.exists()).toBe(true);
- expect(pipelinePath).not.toBe('');
- expect(fullTestReportLink.attributes('href')).toBe(`${pipelinePath}/test_report`);
- });
-
- describe('Without a pipelinePath', () => {
- beforeEach(() => {
- mountComponent({
- props: { pipelinePath: '' },
- });
- });
-
- it('should not render the full test report link', () => {
- expect(findFullTestReportLink().exists()).toBe(false);
- });
- });
- });
-
- describe('`Expand` button', () => {
- beforeEach(() => {
- setReports(newFailedTestReports);
- });
-
- it('tracks service ping metric', () => {
- mountComponent();
- findExpandButton().trigger('click');
-
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(wrapper.vm.$options.expandEvent);
- });
-
- it('only tracks the first expansion', () => {
- mountComponent();
- const expandButton = findExpandButton();
- expandButton.trigger('click');
- expandButton.trigger('click');
- expandButton.trigger('click');
-
- expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('with new failed result', () => {
- beforeEach(() => {
- setReports(newFailedTestReports);
- mountComponent();
- });
-
- it('renders New heading', () => {
- expect(findIssueListUnresolvedHeading().text()).toBe('New');
- });
-
- it('renders failed summary text', () => {
- expect(findHeader().text()).toBe('Test summary contained 2 failed out of 11 total tests');
- });
-
- it('renders failed test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'rspec:pg found 2 failed out of 8 total tests',
- );
- });
-
- it('renders failed issue in list', () => {
- expect(findIssueDescription().text()).toContain(
- 'Test#sum when a is 1 and b is 2 returns summary',
- );
- });
- });
-
- describe('with new error result', () => {
- beforeEach(() => {
- setReports(newErrorsTestReports);
- mountComponent();
- });
-
- it('renders New heading', () => {
- expect(findIssueListUnresolvedHeading().text()).toBe('New');
- });
-
- it('renders error summary text', () => {
- expect(findHeader().text()).toBe('Test summary contained 2 errors out of 11 total tests');
- });
-
- it('renders error test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'karma found 2 errors out of 3 total tests',
- );
- });
-
- it('renders error issue in list', () => {
- expect(findIssueDescription().text()).toContain(
- 'Test#sum when a is 1 and b is 2 returns summary',
- );
- });
- });
-
- describe('with mixed results', () => {
- beforeEach(() => {
- setReports(mixedResultsTestReports);
- mountComponent();
- });
-
- it('renders New and Fixed headings', () => {
- expect(findIssueListUnresolvedHeading().text()).toBe('New');
- expect(findIssueListResolvedHeading().text()).toBe('Fixed');
- });
-
- it('renders summary text', () => {
- expect(findHeader().text()).toBe(
- 'Test summary contained 2 failed and 2 fixed test results out of 11 total tests',
- );
- });
-
- it('renders failed test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'rspec:pg found 1 failed and 2 fixed test results out of 8 total tests',
- );
- });
-
- it('renders failed issue in list', () => {
- expect(findIssueDescription().text()).toContain(
- 'Test#subtract when a is 2 and b is 1 returns correct result',
- );
- });
- });
-
- describe('with resolved failures and resolved errors', () => {
- beforeEach(() => {
- setReports(resolvedFailures);
- mountComponent();
- });
-
- it('renders Fixed heading', () => {
- expect(findIssueListResolvedHeading().text()).toBe('Fixed');
- });
-
- it('renders summary text', () => {
- expect(findHeader().text()).toBe(
- 'Test summary contained 4 fixed test results out of 11 total tests',
- );
- });
-
- it('renders resolved test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- 'rspec:pg found 4 fixed test results out of 8 total tests',
- );
- });
-
- it('renders resolved failures', () => {
- expect(findIssueDescription().text()).toContain(
- resolvedFailures.suites[0].resolved_failures[0].name,
- );
- });
-
- it('renders resolved errors', () => {
- expect(findAllIssueDescriptions().at(2).text()).toContain(
- resolvedFailures.suites[0].resolved_errors[0].name,
- );
- });
- });
-
- describe('recent failures counts', () => {
- describe('with recent failures counts', () => {
- beforeEach(() => {
- setReports(recentFailuresTestReports);
- mountComponent();
- });
-
- it('renders the recently failed tests summary', () => {
- expect(findHeader().text()).toContain(
- '2 out of 3 failed tests have failed more than once in the last 14 days',
- );
- });
-
- it('renders the recently failed count on the test suite', () => {
- expect(findSummaryDescription().text()).toContain(
- '1 out of 2 failed tests has failed more than once in the last 14 days',
- );
- });
-
- it('renders the recent failures count on the test case', () => {
- expect(findIssueRecentFailures().text()).toBe('Failed 8 times in main in the last 14 days');
- });
- });
-
- describe('without recent failures counts', () => {
- beforeEach(() => {
- setReports(mixedResultsTestReports);
- mountComponent();
- });
-
- it('does not render the recently failed tests summary', () => {
- expect(findHeader().text()).not.toContain('failed more than once in the last 14 days');
- });
-
- it('does not render the recently failed count on the test suite', () => {
- expect(findSummaryDescription().text()).not.toContain(
- 'failed more than once in the last 14 days',
- );
- });
-
- it('does not render the recent failures count on the test case', () => {
- expect(findIssueDescription().text()).not.toContain('in the last 14 days');
- });
- });
- });
-
- describe('with a report that failed to load', () => {
- beforeEach(() => {
- setReports(failedReport);
- mountComponent();
- });
-
- it('renders an error status for the report', () => {
- const { name } = failedReport.suites[0];
-
- expect(findSummaryDescription().text()).toContain(
- `An error occurred while loading ${name} result`,
- );
- });
- });
-
- describe('with a report parsing errors', () => {
- beforeEach(() => {
- const reports = failedReport;
- reports.suites[0].suite_errors = {
- head: 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
- base: 'JUnit data parsing failed: string not matched',
- };
- setReports(reports);
- mountComponent();
- });
-
- it('renders the error messages', () => {
- expect(findSummaryDescription().text()).toContain(
- 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
- );
- expect(findSummaryDescription().text()).toContain(
- 'JUnit data parsing failed: string not matched',
- );
- });
- });
-
- describe('with error', () => {
- beforeEach(() => {
- mockStore.state.isLoading = false;
- mockStore.state.hasError = true;
- mountComponent();
- });
-
- it('renders loading state', () => {
- expect(findHeader().text()).toBe('Test summary failed loading results');
- });
- });
-
- describe('while loading', () => {
- beforeEach(() => {
- mockStore.state.isLoading = true;
- mountComponent();
- });
-
- it('renders loading state', () => {
- expect(findHeader().text()).toBe('Test summary results are being parsed');
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/actions_spec.js b/spec/frontend/reports/grouped_test_report/store/actions_spec.js
deleted file mode 100644
index 7469c31cf84..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/actions_spec.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
-import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import {
- setPaths,
- requestReports,
- fetchReports,
- stopPolling,
- clearEtagPoll,
- receiveReportsSuccess,
- receiveReportsError,
- openModal,
- closeModal,
-} from '~/reports/grouped_test_report/store/actions';
-import * as types from '~/reports/grouped_test_report/store/mutation_types';
-import state from '~/reports/grouped_test_report/store/state';
-
-describe('Reports Store Actions', () => {
- let mockedState;
-
- beforeEach(() => {
- mockedState = state();
- });
-
- describe('setPaths', () => {
- it('should commit SET_PATHS mutation', () => {
- return testAction(
- setPaths,
- { endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
- mockedState,
- [
- {
- type: types.SET_PATHS,
- payload: { endpoint: 'endpoint.json', headBlobPath: '/blob/path' },
- },
- ],
- [],
- );
- });
- });
-
- describe('requestReports', () => {
- it('should commit REQUEST_REPORTS mutation', () => {
- return testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], []);
- });
- });
-
- describe('fetchReports', () => {
- let mock;
-
- beforeEach(() => {
- mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- stopPolling();
- clearEtagPoll();
- });
-
- describe('success', () => {
- it('dispatches requestReports and receiveReportsSuccess', () => {
- mock
- .onGet(`${TEST_HOST}/endpoint.json`)
- .replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
-
- return testAction(
- fetchReports,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestReports',
- },
- {
- payload: { data: { summary: {}, suites: [{ name: 'rspec' }] }, status: 200 },
- type: 'receiveReportsSuccess',
- },
- ],
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- });
-
- it('dispatches requestReports and receiveReportsError', () => {
- return testAction(
- fetchReports,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestReports',
- },
- {
- type: 'receiveReportsError',
- },
- ],
- );
- });
- });
- });
-
- describe('receiveReportsSuccess', () => {
- it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', () => {
- return testAction(
- receiveReportsSuccess,
- { data: { summary: {} }, status: 200 },
- mockedState,
- [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
- [],
- );
- });
-
- it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', () => {
- return testAction(
- receiveReportsSuccess,
- { data: { summary: {} }, status: 204 },
- mockedState,
- [],
- [],
- );
- });
- });
-
- describe('receiveReportsError', () => {
- it('should commit RECEIVE_REPORTS_ERROR mutation', () => {
- return testAction(
- receiveReportsError,
- null,
- mockedState,
- [{ type: types.RECEIVE_REPORTS_ERROR }],
- [],
- );
- });
- });
-
- describe('openModal', () => {
- it('should commit SET_ISSUE_MODAL_DATA', () => {
- return testAction(
- openModal,
- { name: 'foo' },
- mockedState,
- [{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }],
- [],
- );
- });
- });
-
- describe('closeModal', () => {
- it('should commit RESET_ISSUE_MODAL_DATA', () => {
- return testAction(
- closeModal,
- {},
- mockedState,
- [{ type: types.RESET_ISSUE_MODAL_DATA, payload: {} }],
- [],
- );
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/mutations_spec.js b/spec/frontend/reports/grouped_test_report/store/mutations_spec.js
deleted file mode 100644
index b2890d7285f..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/mutations_spec.js
+++ /dev/null
@@ -1,162 +0,0 @@
-import * as types from '~/reports/grouped_test_report/store/mutation_types';
-import mutations from '~/reports/grouped_test_report/store/mutations';
-import state from '~/reports/grouped_test_report/store/state';
-import { failedIssue } from '../../mock_data/mock_data';
-
-describe('Reports Store Mutations', () => {
- let stateCopy;
-
- beforeEach(() => {
- stateCopy = state();
- });
-
- describe('SET_PATHS', () => {
- it('should set endpoint', () => {
- mutations[types.SET_PATHS](stateCopy, {
- endpoint: 'endpoint.json',
- headBlobPath: '/blob/path',
- });
-
- expect(stateCopy.endpoint).toEqual('endpoint.json');
- expect(stateCopy.headBlobPath).toEqual('/blob/path');
- });
- });
-
- describe('REQUEST_REPORTS', () => {
- it('should set isLoading to true', () => {
- mutations[types.REQUEST_REPORTS](stateCopy);
-
- expect(stateCopy.isLoading).toEqual(true);
- });
- });
-
- describe('RECEIVE_REPORTS_SUCCESS', () => {
- const mockedResponse = {
- summary: {
- total: 14,
- resolved: 0,
- failed: 7,
- },
- suites: [
- {
- name: 'build:linux',
- summary: {
- total: 2,
- resolved: 0,
- failed: 1,
- },
- new_failures: [
- {
- name: 'StringHelper#concatenate when a is git and b is lab returns summary',
- execution_time: 0.0092435,
- system_output: "Failure/Error: is_expected.to eq('gitlab')",
- recent_failures: {
- count: 4,
- base_branch: 'main',
- },
- },
- ],
- resolved_failures: [
- {
- name: 'StringHelper#concatenate when a is git and b is lab returns summary',
- execution_time: 0.009235,
- system_output: "Failure/Error: is_expected.to eq('gitlab')",
- },
- ],
- existing_failures: [
- {
- name: 'StringHelper#concatenate when a is git and b is lab returns summary',
- execution_time: 1232.08,
- system_output: "Failure/Error: is_expected.to eq('gitlab')",
- },
- ],
- },
- ],
- };
-
- beforeEach(() => {
- mutations[types.RECEIVE_REPORTS_SUCCESS](stateCopy, mockedResponse);
- });
-
- it('should reset isLoading', () => {
- expect(stateCopy.isLoading).toEqual(false);
- });
-
- it('should reset hasError', () => {
- expect(stateCopy.hasError).toEqual(false);
- });
-
- it('should set summary counts', () => {
- expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total);
- expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved);
- expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed);
- expect(stateCopy.summary.recentlyFailed).toEqual(1);
- });
-
- it('should set reports', () => {
- expect(stateCopy.reports).toEqual(mockedResponse.suites);
- });
- });
-
- describe('RECEIVE_REPORTS_ERROR', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_REPORTS_ERROR](stateCopy);
- });
-
- it('should reset isLoading', () => {
- expect(stateCopy.isLoading).toEqual(false);
- });
-
- it('should set hasError to true', () => {
- expect(stateCopy.hasError).toEqual(true);
- });
-
- it('should reset reports', () => {
- expect(stateCopy.reports).toEqual([]);
- });
- });
-
- describe('SET_ISSUE_MODAL_DATA', () => {
- beforeEach(() => {
- mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
- issue: failedIssue,
- });
- });
-
- it('should set modal title', () => {
- expect(stateCopy.modal.title).toEqual(failedIssue.name);
- });
-
- it('should set modal data', () => {
- expect(stateCopy.modal.data.execution_time.value).toEqual(failedIssue.execution_time);
- expect(stateCopy.modal.data.system_output.value).toEqual(failedIssue.system_output);
- });
-
- it('should open modal', () => {
- expect(stateCopy.modal.open).toEqual(true);
- });
- });
-
- describe('RESET_ISSUE_MODAL_DATA', () => {
- beforeEach(() => {
- mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, {
- issue: failedIssue,
- });
-
- mutations[types.RESET_ISSUE_MODAL_DATA](stateCopy);
- });
-
- it('should reset modal title', () => {
- expect(stateCopy.modal.title).toEqual(null);
- });
-
- it('should reset modal data', () => {
- expect(stateCopy.modal.data.execution_time.value).toEqual(null);
- expect(stateCopy.modal.data.system_output.value).toEqual(null);
- });
-
- it('should close modal', () => {
- expect(stateCopy.modal.open).toEqual(false);
- });
- });
-});
diff --git a/spec/frontend/reports/grouped_test_report/store/utils_spec.js b/spec/frontend/reports/grouped_test_report/store/utils_spec.js
deleted file mode 100644
index 760afe1c11a..00000000000
--- a/spec/frontend/reports/grouped_test_report/store/utils_spec.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import {
- STATUS_FAILED,
- STATUS_SUCCESS,
- ICON_WARNING,
- ICON_SUCCESS,
- ICON_NOTFOUND,
-} from '~/reports/constants';
-import * as utils from '~/reports/grouped_test_report/store/utils';
-
-describe('Reports store utils', () => {
- describe('summaryTextbuilder', () => {
- it('should render text for no changed results in multiple tests', () => {
- const name = 'Test summary';
- const data = { total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained no changed test results out of 10 total tests');
- });
-
- it('should render text for no changed results in one test', () => {
- const name = 'Test summary';
- const data = { total: 1 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained no changed test results out of 1 total test');
- });
-
- it('should render text for multiple failed results', () => {
- const name = 'Test summary';
- const data = { failed: 3, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained 3 failed out of 10 total tests');
- });
-
- it('should render text for multiple errored results', () => {
- const name = 'Test summary';
- const data = { errored: 7, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained 7 errors out of 10 total tests');
- });
-
- it('should render text for multiple fixed results', () => {
- const name = 'Test summary';
- const data = { resolved: 4, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe('Test summary contained 4 fixed test results out of 10 total tests');
- });
-
- it('should render text for multiple fixed, and multiple failed results', () => {
- const name = 'Test summary';
- const data = { failed: 3, resolved: 4, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 3 failed and 4 fixed test results out of 10 total tests',
- );
- });
-
- it('should render text for a singular fixed, and a singular failed result', () => {
- const name = 'Test summary';
- const data = { failed: 1, resolved: 1, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 1 failed and 1 fixed test result out of 10 total tests',
- );
- });
-
- it('should render text for singular failed, errored, and fixed results', () => {
- const name = 'Test summary';
- const data = { failed: 1, errored: 1, resolved: 1, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 1 failed, 1 error and 1 fixed test result out of 10 total tests',
- );
- });
-
- it('should render text for multiple failed, errored, and fixed results', () => {
- const name = 'Test summary';
- const data = { failed: 2, errored: 3, resolved: 4, total: 10 };
- const result = utils.summaryTextBuilder(name, data);
-
- expect(result).toBe(
- 'Test summary contained 2 failed, 3 errors and 4 fixed test results out of 10 total tests',
- );
- });
- });
-
- describe('reportTextBuilder', () => {
- it('should render text for no changed results in multiple tests', () => {
- const name = 'Rspec';
- const data = { total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found no changed test results out of 10 total tests');
- });
-
- it('should render text for no changed results in one test', () => {
- const name = 'Rspec';
- const data = { total: 1 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found no changed test results out of 1 total test');
- });
-
- it('should render text for multiple failed results', () => {
- const name = 'Rspec';
- const data = { failed: 3, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 3 failed out of 10 total tests');
- });
-
- it('should render text for multiple errored results', () => {
- const name = 'Rspec';
- const data = { errored: 7, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 7 errors out of 10 total tests');
- });
-
- it('should render text for multiple fixed results', () => {
- const name = 'Rspec';
- const data = { resolved: 4, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 4 fixed test results out of 10 total tests');
- });
-
- it('should render text for multiple fixed, and multiple failed results', () => {
- const name = 'Rspec';
- const data = { failed: 3, resolved: 4, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 3 failed and 4 fixed test results out of 10 total tests');
- });
-
- it('should render text for a singular fixed, and a singular failed result', () => {
- const name = 'Rspec';
- const data = { failed: 1, resolved: 1, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe('Rspec found 1 failed and 1 fixed test result out of 10 total tests');
- });
-
- it('should render text for singular failed, errored, and fixed results', () => {
- const name = 'Rspec';
- const data = { failed: 1, errored: 1, resolved: 1, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe(
- 'Rspec found 1 failed, 1 error and 1 fixed test result out of 10 total tests',
- );
- });
-
- it('should render text for multiple failed, errored, and fixed results', () => {
- const name = 'Rspec';
- const data = { failed: 2, errored: 3, resolved: 4, total: 10 };
- const result = utils.reportTextBuilder(name, data);
-
- expect(result).toBe(
- 'Rspec found 2 failed, 3 errors and 4 fixed test results out of 10 total tests',
- );
- });
- });
-
- describe('recentFailuresTextBuilder', () => {
- it.each`
- recentlyFailed | failed | expected
- ${0} | ${1} | ${''}
- ${1} | ${1} | ${'1 out of 1 failed test has failed more than once in the last 14 days'}
- ${1} | ${2} | ${'1 out of 2 failed tests has failed more than once in the last 14 days'}
- ${2} | ${3} | ${'2 out of 3 failed tests have failed more than once in the last 14 days'}
- `(
- 'should render summary for $recentlyFailed out of $failed failures',
- ({ recentlyFailed, failed, expected }) => {
- const result = utils.recentFailuresTextBuilder({ recentlyFailed, failed });
-
- expect(result).toBe(expected);
- },
- );
- });
-
- describe('countRecentlyFailedTests', () => {
- it('counts tests with more than one recent failure in a report', () => {
- const report = {
- new_failures: [{ recent_failures: { count: 2 } }],
- existing_failures: [{ recent_failures: { count: 1 } }],
- resolved_failures: [{ recent_failures: { count: 20 } }, { recent_failures: { count: 5 } }],
- };
- const result = utils.countRecentlyFailedTests(report);
-
- expect(result).toBe(3);
- });
-
- it('counts tests with more than one recent failure in an array of reports', () => {
- const reports = [
- {
- new_failures: [{ recent_failures: { count: 2 } }],
- existing_failures: [
- { recent_failures: { count: 20 } },
- { recent_failures: { count: 5 } },
- ],
- resolved_failures: [{ recent_failures: { count: 2 } }],
- },
- {
- new_failures: [{ recent_failures: { count: 8 } }, { recent_failures: { count: 14 } }],
- existing_failures: [{ recent_failures: { count: 1 } }],
- resolved_failures: [{ recent_failures: { count: 7 } }, { recent_failures: { count: 5 } }],
- },
- ];
- const result = utils.countRecentlyFailedTests(reports);
-
- expect(result).toBe(8);
- });
- });
-
- describe('statusIcon', () => {
- describe('with failed status', () => {
- it('returns ICON_WARNING', () => {
- expect(utils.statusIcon(STATUS_FAILED)).toEqual(ICON_WARNING);
- });
- });
-
- describe('with success status', () => {
- it('returns ICON_SUCCESS', () => {
- expect(utils.statusIcon(STATUS_SUCCESS)).toEqual(ICON_SUCCESS);
- });
- });
-
- describe('without a status', () => {
- it('returns ICON_NOTFOUND', () => {
- expect(utils.statusIcon()).toEqual(ICON_NOTFOUND);
- });
- });
- });
-
- describe('formatFilePath', () => {
- it.each`
- file | expected
- ${'./test.js'} | ${'test.js'}
- ${'/test.js'} | ${'test.js'}
- ${'.//////////////test.js'} | ${'test.js'}
- ${'test.js'} | ${'test.js'}
- ${'mock/path./test.js'} | ${'mock/path./test.js'}
- ${'./mock/path./test.js'} | ${'mock/path./test.js'}
- `('should format $file to be $expected', ({ file, expected }) => {
- expect(utils.formatFilePath(file)).toBe(expected);
- });
- });
-});
diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
deleted file mode 100644
index d1f04f0ee37..00000000000
--- a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { nextTick } from 'vue';
-import { GlBanner } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-
-describe('RunnerStackedLayoutBanner', () => {
- let wrapper;
-
- const findBanner = () => wrapper.findComponent(GlBanner);
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
-
- const createComponent = ({ ...options } = {}, mountFn = shallowMount) => {
- wrapper = mountFn(RunnerStackedLayoutBanner, {
- ...options,
- });
- };
-
- it('Displays a banner', () => {
- createComponent();
-
- expect(findBanner().props()).toMatchObject({
- svgPath: expect.stringContaining('data:image/svg+xml;utf8,'),
- title: expect.any(String),
- buttonText: expect.any(String),
- buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'),
- });
- expect(findLocalStorageSync().exists()).toBe(true);
- });
-
- it('Does not display a banner when dismissed', async () => {
- createComponent();
-
- findLocalStorageSync().vm.$emit('input', true);
-
- await nextTick();
-
- expect(findBanner().exists()).toBe(false);
- expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal
- });
-});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index 0542e96c77c..fa5ccfeb478 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -107,3 +107,87 @@ export const PROMISE_ALL_EXPECTED_MUTATIONS = {
payload: { key: PROJECTS_LOCAL_STORAGE_KEY, data: [MOCK_FRESH_DATA_RES, MOCK_FRESH_DATA_RES] },
},
};
+
+export const MOCK_NAVIGATION = {
+ projects: {
+ label: 'Projects',
+ scope: 'projects',
+ link: '/search?scope=projects&search=et',
+ count_link: '/search/count?scope=projects&search=et',
+ },
+ blobs: {
+ label: 'Code',
+ scope: 'blobs',
+ link: '/search?scope=blobs&search=et',
+ count_link: '/search/count?scope=blobs&search=et',
+ },
+ issues: {
+ label: 'Issues',
+ scope: 'issues',
+ link: '/search?scope=issues&search=et',
+ active: true,
+ count: '2,430',
+ },
+ merge_requests: {
+ label: 'Merge requests',
+ scope: 'merge_requests',
+ link: '/search?scope=merge_requests&search=et',
+ count_link: '/search/count?scope=merge_requests&search=et',
+ },
+ wiki_blobs: {
+ label: 'Wiki',
+ scope: 'wiki_blobs',
+ link: '/search?scope=wiki_blobs&search=et',
+ count_link: '/search/count?scope=wiki_blobs&search=et',
+ },
+ commits: {
+ label: 'Commits',
+ scope: 'commits',
+ link: '/search?scope=commits&search=et',
+ count_link: '/search/count?scope=commits&search=et',
+ },
+ notes: {
+ label: 'Comments',
+ scope: 'notes',
+ link: '/search?scope=notes&search=et',
+ count_link: '/search/count?scope=notes&search=et',
+ },
+ milestones: {
+ label: 'Milestones',
+ scope: 'milestones',
+ link: '/search?scope=milestones&search=et',
+ count_link: '/search/count?scope=milestones&search=et',
+ },
+ users: {
+ label: 'Users',
+ scope: 'users',
+ link: '/search?scope=users&search=et',
+ count_link: '/search/count?scope=users&search=et',
+ },
+};
+
+export const MOCK_NAVIGATION_DATA = {
+ projects: {
+ label: 'Projects',
+ scope: 'projects',
+ link: '/search?scope=projects&search=et',
+ count_link: '/search/count?scope=projects&search=et',
+ },
+};
+
+export const MOCK_ENDPOINT_RESPONSE = { count: '13' };
+
+export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = {
+ projects: {
+ count: '13',
+ label: 'Projects',
+ scope: 'projects',
+ link: '/search?scope=projects&search=et',
+ count_link: '/search/count?scope=projects&search=et',
+ },
+};
+
+export const MOCK_NAVIGATION_ACTION_MUTATION = {
+ type: types.RECEIVE_NAVIGATION_COUNT,
+ payload: { key: 'projects', count: '13' },
+};
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 89959feec39..e87217950cd 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -1,11 +1,10 @@
-import { GlButton, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
-import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
-import StatusFilter from '~/search/sidebar/components/status_filter.vue';
+import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
+import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
Vue.use(Vuex);
@@ -17,7 +16,7 @@ describe('GlobalSearchSidebar', () => {
resetQuery: jest.fn(),
};
- const createComponent = (initialState) => {
+ const createComponent = (initialState, featureFlags) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -28,6 +27,11 @@ describe('GlobalSearchSidebar', () => {
wrapper = shallowMount(GlobalSearchSidebar, {
store,
+ provide: {
+ glFeatures: {
+ ...featureFlags,
+ },
+ },
});
};
@@ -35,118 +39,68 @@ describe('GlobalSearchSidebar', () => {
wrapper.destroy();
});
- const findSidebarForm = () => wrapper.find('form');
- const findStatusFilter = () => wrapper.findComponent(StatusFilter);
- const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
- const findApplyButton = () => wrapper.findComponent(GlButton);
- const findResetLinkButton = () => wrapper.findComponent(GlLink);
+ const findSidebarSection = () => wrapper.find('section');
+ const findFilters = () => wrapper.findComponent(ResultsFilters);
+ const findSidebarNavigation = () => wrapper.findComponent(ScopeNavigation);
- describe('template', () => {
+ describe('renders properly', () => {
describe('scope=projects', () => {
beforeEach(() => {
createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'projects' } });
});
- it("doesn't render StatusFilter", () => {
- expect(findStatusFilter().exists()).toBe(false);
- });
-
- it("doesn't render ConfidentialityFilter", () => {
- expect(findConfidentialityFilter().exists()).toBe(false);
+ it('shows section', () => {
+ expect(findSidebarSection().exists()).toBe(true);
});
- it("doesn't render ApplyButton", () => {
- expect(findApplyButton().exists()).toBe(false);
+ it("doesn't shows filters", () => {
+ expect(findFilters().exists()).toBe(false);
});
});
- describe('scope=issues', () => {
+ describe('scope=merge_requests', () => {
beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY });
- });
- it('renders StatusFilter', () => {
- expect(findStatusFilter().exists()).toBe(true);
+ createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'merge_requests' } });
});
- it('renders ConfidentialityFilter', () => {
- expect(findConfidentialityFilter().exists()).toBe(true);
+ it('shows section', () => {
+ expect(findSidebarSection().exists()).toBe(true);
});
- it('renders ApplyButton', () => {
- expect(findApplyButton().exists()).toBe(true);
+ it('shows filters', () => {
+ expect(findFilters().exists()).toBe(true);
});
});
- });
- describe('ApplyButton', () => {
- describe('when sidebarDirty is false', () => {
+ describe('scope=issues', () => {
beforeEach(() => {
- createComponent({ sidebarDirty: false });
- });
-
- it('disables the button', () => {
- expect(findApplyButton().attributes('disabled')).toBe('true');
+ createComponent({ urlQuery: MOCK_QUERY });
});
- });
-
- describe('when sidebarDirty is true', () => {
- beforeEach(() => {
- createComponent({ sidebarDirty: true });
+ it('shows section', () => {
+ expect(findSidebarSection().exists()).toBe(true);
});
- it('enables the button', () => {
- expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ it('shows filters', () => {
+ expect(findFilters().exists()).toBe(true);
});
});
});
- describe('ResetLinkButton', () => {
- describe('with no filter selected', () => {
- beforeEach(() => {
- createComponent({ urlQuery: {} });
- });
-
- it('does not render', () => {
- expect(findResetLinkButton().exists()).toBe(false);
- });
- });
-
- describe('with filter selected', () => {
- beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY });
- });
-
- it('does render', () => {
- expect(findResetLinkButton().exists()).toBe(true);
- });
+ describe('when search_page_vertical_nav is enabled', () => {
+ beforeEach(() => {
+ createComponent({}, { searchPageVerticalNav: true });
});
-
- describe('with filter selected and user updated query back to default', () => {
- beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY, query: {} });
- });
-
- it('does render', () => {
- expect(findResetLinkButton().exists()).toBe(true);
- });
+ it('shows the vertical navigation', () => {
+ expect(findSidebarNavigation().exists()).toBe(true);
});
});
- describe('actions', () => {
+ describe('when search_page_vertical_nav is disabled', () => {
beforeEach(() => {
- createComponent({});
+ createComponent({}, { searchPageVerticalNav: false });
});
-
- it('clicking ApplyButton calls applyQuery', () => {
- findSidebarForm().trigger('submit');
-
- expect(actionSpies.applyQuery).toHaveBeenCalled();
- });
-
- it('clicking ResetLinkButton calls resetQuery', () => {
- findResetLinkButton().vm.$emit('click');
-
- expect(actionSpies.resetQuery).toHaveBeenCalled();
+ it('hides the vertical navigation', () => {
+ expect(findSidebarNavigation().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js
new file mode 100644
index 00000000000..4f217709297
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/filters_spec.js
@@ -0,0 +1,132 @@
+import { GlButton, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { MOCK_QUERY } from 'jest/search/mock_data';
+import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
+import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
+import StatusFilter from '~/search/sidebar/components/status_filter.vue';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchSidebarFilters', () => {
+ let wrapper;
+
+ const actionSpies = {
+ applyQuery: jest.fn(),
+ resetQuery: jest.fn(),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(ResultsFilters, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSidebarForm = () => wrapper.find('form');
+ const findStatusFilter = () => wrapper.findComponent(StatusFilter);
+ const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
+ const findApplyButton = () => wrapper.findComponent(GlButton);
+ const findResetLinkButton = () => wrapper.findComponent(GlLink);
+
+ describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY });
+ });
+ it('renders StatusFilter', () => {
+ expect(findStatusFilter().exists()).toBe(true);
+ });
+
+ it('renders ConfidentialityFilter', () => {
+ expect(findConfidentialityFilter().exists()).toBe(true);
+ });
+
+ it('renders ApplyButton', () => {
+ expect(findApplyButton().exists()).toBe(true);
+ });
+ });
+
+ describe('ApplyButton', () => {
+ describe('when sidebarDirty is false', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: false });
+ });
+
+ it('disables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when sidebarDirty is true', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: true });
+ });
+
+ it('enables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('ResetLinkButton', () => {
+ describe('with no filter selected', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: {} });
+ });
+
+ it('does not render', () => {
+ expect(findResetLinkButton().exists()).toBe(false);
+ });
+ });
+
+ describe('with filter selected', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY });
+ });
+
+ it('does render', () => {
+ expect(findResetLinkButton().exists()).toBe(true);
+ });
+ });
+
+ describe('with filter selected and user updated query back to default', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: MOCK_QUERY, query: {} });
+ });
+
+ it('does render', () => {
+ expect(findResetLinkButton().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent({});
+ });
+
+ it('clicking ApplyButton calls applyQuery', () => {
+ findSidebarForm().trigger('submit');
+
+ expect(actionSpies.applyQuery).toHaveBeenCalled();
+ });
+
+ it('clicking ResetLinkButton calls resetQuery', () => {
+ findResetLinkButton().vm.$emit('click');
+
+ expect(actionSpies.resetQuery).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
new file mode 100644
index 00000000000..6262a52e01a
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
@@ -0,0 +1,80 @@
+import { GlNav, GlNavItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { MOCK_QUERY, MOCK_NAVIGATION } from 'jest/search/mock_data';
+import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
+
+Vue.use(Vuex);
+
+describe('ScopeNavigation', () => {
+ let wrapper;
+
+ const actionSpies = {
+ fetchSidebarCount: jest.fn(),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ navigation: MOCK_NAVIGATION,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(ScopeNavigation, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findNavElement = () => wrapper.find('nav');
+ const findGlNav = () => wrapper.findComponent(GlNav);
+ const findGlNavItems = () => wrapper.findAllComponents(GlNavItem);
+ const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active'));
+ const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1);
+
+ describe('scope navigation', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders section', () => {
+ expect(findNavElement().exists()).toBe(true);
+ });
+
+ it('renders nav component', () => {
+ expect(findGlNav().exists()).toBe(true);
+ });
+
+ it('renders all nav item components', () => {
+ expect(findGlNavItems()).toHaveLength(9);
+ });
+
+ it('nav items have proper links', () => {
+ const linkAtPosition = 3;
+ const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
+
+ expect(findGlNavItems().at(linkAtPosition).attributes('href')).toBe(link);
+ });
+ });
+
+ describe('scope navigation sets proper state', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets proper class to active item', () => {
+ expect(findGlNavItemActive()).toHaveLength(1);
+ });
+
+ it('active item', () => {
+ expect(findGlNavItemActiveCount().text()).toBe('2.4K');
+ });
+ });
+});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index c442ffa521d..3d19b27ff86 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { createAlert } from '~/flash';
+import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
@@ -23,6 +24,9 @@ import {
MOCK_FRESH_DATA_RES,
PRELOAD_EXPECTED_MUTATIONS,
PROMISE_ALL_EXPECTED_MUTATIONS,
+ MOCK_NAVIGATION_DATA,
+ MOCK_NAVIGATION_ACTION_MUTATION,
+ MOCK_ENDPOINT_RESPONSE,
} from '../mock_data';
jest.mock('~/flash');
@@ -31,6 +35,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
joinPaths: jest.fn().mockReturnValue(''),
visitUrl: jest.fn(),
}));
+jest.mock('~/lib/logger', () => ({
+ logError: jest.fn(),
+}));
describe('Global Search Store Actions', () => {
let mock;
@@ -260,4 +267,32 @@ describe('Global Search Store Actions', () => {
);
});
});
+
+ describe.each`
+ action | axiosMock | type | scope | expectedMutations | errorLogs
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${'issues'} | ${[]} | ${1}
+ `('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => {
+ describe(`on ${type}`, () => {
+ beforeEach(() => {
+ state.navigation = MOCK_NAVIGATION_DATA;
+ state.urlQuery = {
+ scope,
+ };
+
+ if (axiosMock.method) {
+ mock[axiosMock.method]().reply(axiosMock.code, MOCK_ENDPOINT_RESPONSE);
+ }
+ });
+
+ it(`should ${expectedMutations.length === 0 ? 'NOT ' : ''}dispatch ${
+ expectedMutations.length === 0 ? '' : 'the correct '
+ }mutations for ${scope}`, () => {
+ return testAction({ action, state, expectedMutations }).then(() => {
+ expect(logger.logError).toHaveBeenCalledTimes(errorLogs);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index 25f9b692955..a79ec8f70b0 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -1,13 +1,20 @@
import * as types from '~/search/store/mutation_types';
import mutations from '~/search/store/mutations';
import createState from '~/search/store/state';
-import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
+import {
+ MOCK_QUERY,
+ MOCK_GROUPS,
+ MOCK_PROJECTS,
+ MOCK_NAVIGATION_DATA,
+ MOCK_NAVIGATION_ACTION_MUTATION,
+ MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION,
+} from '../mock_data';
describe('Global Search Store Mutations', () => {
let state;
beforeEach(() => {
- state = createState({ query: MOCK_QUERY });
+ state = createState({ query: MOCK_QUERY, navigation: MOCK_NAVIGATION_DATA });
});
describe('REQUEST_GROUPS', () => {
@@ -90,4 +97,15 @@ describe('Global Search Store Mutations', () => {
expect(state.frequentItems[payload.key]).toStrictEqual(payload.data);
});
});
+
+ describe('RECEIVE_NAVIGATION_COUNT', () => {
+ it('sets frequentItems[key] to data', () => {
+ const { payload } = MOCK_NAVIGATION_ACTION_MUTATION;
+ mutations[types.RECEIVE_NAVIGATION_COUNT](state, payload);
+
+ expect(state.navigation[payload.key]).toStrictEqual(
+ MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION[payload.key],
+ );
+ });
+ });
});
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 11841106ed0..efe3f7e8dbf 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
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`self monitor component When the self monitor project has not been created default state to match the default snapshot 1`] = `
+exports[`self-monitor component When the self-monitor project has not been created default state to match the default snapshot 1`] = `
<section
class="settings no-animate js-self-monitoring-settings"
>
@@ -11,7 +11,7 @@ exports[`self monitor component When the self monitor project has not been creat
class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only"
>
- Self monitoring
+ Self-monitoring
</h4>
@@ -30,7 +30,7 @@ exports[`self monitor component When the self monitor project has not been creat
class="js-section-sub-header"
>
- Activate or deactivate instance self monitoring.
+ Activate or deactivate instance self-monitoring.
<gl-link-stub
href="/help/administration/monitoring/gitlab_self_monitoring_project/index"
@@ -47,7 +47,7 @@ exports[`self monitor component When the self monitor project has not been creat
name="self-monitoring-form"
>
<p>
- Activate self monitoring to create a project to use to monitor the health of your instance.
+ Activate self-monitoring to create a project to use to monitor the health of your instance.
</p>
<gl-form-group-stub
@@ -55,7 +55,7 @@ exports[`self monitor component When the self monitor project has not been creat
optionaltext="(optional)"
>
<gl-toggle-stub
- label="Self monitoring"
+ label="Self-monitoring"
labelposition="top"
/>
</gl-form-group-stub>
@@ -69,15 +69,15 @@ exports[`self monitor component When the self monitor project has not been creat
dismisslabel="Close"
modalclass=""
modalid="delete-self-monitor-modal"
- ok-title="Delete self monitoring project"
+ ok-title="Delete self-monitoring project"
ok-variant="danger"
size="md"
- title="Deactivate self monitoring?"
+ title="Deactivate self-monitoring?"
titletag="h4"
>
<div>
- Deactivating self monitoring deletes the self monitoring project. Are you sure you want to deactivate self monitoring and delete the project?
+ Deactivating self-monitoring deletes the self-monitoring project. Are you sure you want to deactivate self-monitoring and delete the project?
</div>
</gl-modal-stub>
diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
index c690bbf1c57..35f2734dde3 100644
--- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js
+++ b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
@@ -4,11 +4,11 @@ import { TEST_HOST } from 'helpers/test_constants';
import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
import { createStore } from '~/self_monitor/store';
-describe('self monitor component', () => {
+describe('self-monitor component', () => {
let wrapper;
let store;
- describe('When the self monitor project has not been created', () => {
+ describe('When the self-monitor project has not been created', () => {
beforeEach(() => {
store = createStore({
projectEnabled: false,
@@ -35,7 +35,7 @@ describe('self monitor component', () => {
it('renders header text', () => {
wrapper = shallowMount(SelfMonitor, { store });
- expect(wrapper.find('.js-section-header').text()).toBe('Self monitoring');
+ expect(wrapper.find('.js-section-header').text()).toBe('Self-monitoring');
});
describe('expand/collapse button', () => {
@@ -53,7 +53,7 @@ describe('self monitor component', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.find('.js-section-sub-header').text()).toContain(
- 'Activate or deactivate instance self monitoring.',
+ 'Activate or deactivate instance self-monitoring.',
);
});
});
@@ -63,7 +63,7 @@ describe('self monitor component', () => {
wrapper = shallowMount(SelfMonitor, { store });
expect(wrapper.vm.selfMonitoringFormText).toContain(
- 'Activate self monitoring to create a project to use to monitor the health of your instance.',
+ 'Activate self-monitoring to create a project to use to monitor the health of your instance.',
);
});
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 59ee87c4a02..21e63533c66 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -6,7 +6,7 @@ import * as actions from '~/self_monitor/store/actions';
import * as types from '~/self_monitor/store/mutation_types';
import createState from '~/self_monitor/store/state';
-describe('self monitor actions', () => {
+describe('self-monitor actions', () => {
let state;
let mock;
@@ -129,7 +129,7 @@ describe('self monitor actions', () => {
payload: {
actionName: 'viewSelfMonitorProject',
actionText: 'View project',
- message: 'Self monitoring project successfully created.',
+ message: 'Self-monitoring project successfully created.',
},
},
{ type: types.SET_SHOW_ALERT, payload: true },
@@ -236,7 +236,7 @@ describe('self monitor actions', () => {
payload: {
actionName: 'createProject',
actionText: 'Undo',
- message: 'Self monitoring project successfully deleted.',
+ message: 'Self-monitoring project successfully deleted.',
},
},
{ type: types.SET_SHOW_ALERT, payload: true },
diff --git a/spec/frontend/self_monitor/store/mutations_spec.js b/spec/frontend/self_monitor/store/mutations_spec.js
index 5282ae3b2f5..315450f3aef 100644
--- a/spec/frontend/self_monitor/store/mutations_spec.js
+++ b/spec/frontend/self_monitor/store/mutations_spec.js
@@ -1,7 +1,7 @@
import mutations from '~/self_monitor/store/mutations';
import createState from '~/self_monitor/store/state';
-describe('self monitoring mutations', () => {
+describe('self-monitoring mutations', () => {
let localState;
beforeEach(() => {
diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
new file mode 100644
index 00000000000..843ac1da4bb
--- /dev/null
+++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
@@ -0,0 +1,93 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
+import { __ } from '~/locale';
+import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
+import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
+
+describe('MilestoneDropdown component', () => {
+ let wrapper;
+
+ const propsData = {
+ attrWorkspacePath: 'full/path',
+ issuableType: IssuableType.Issue,
+ workspaceType: WorkspaceType.project,
+ };
+
+ const findHiddenInput = () => wrapper.find('input');
+ const findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(MilestoneDropdown, { propsData: { ...propsData, ...props } });
+ };
+
+ it('renders SidebarDropdown', () => {
+ createComponent();
+
+ expect(findSidebarDropdown().props()).toMatchObject({
+ attrWorkspacePath: propsData.attrWorkspacePath,
+ issuableAttribute: MilestoneDropdown.issuableAttribute,
+ issuableType: propsData.issuableType,
+ workspaceType: propsData.workspaceType,
+ });
+ });
+
+ it('renders hidden input', () => {
+ createComponent();
+
+ expect(findHiddenInput().attributes()).toEqual({
+ type: 'hidden',
+ name: 'update[milestone_id]',
+ value: undefined,
+ });
+ });
+
+ describe('when milestone ID and title is provided', () => {
+ it('is used in the dropdown and hidden input', () => {
+ const milestone = {
+ id: 'gid://gitlab/Milestone/52',
+ title: __('Milestone 52'),
+ };
+ createComponent({ milestoneId: milestone.id, milestoneTitle: milestone.title });
+
+ expect(findSidebarDropdown().props('currentAttribute')).toEqual(milestone);
+ expect(findHiddenInput().attributes('value')).toBe(
+ getIdFromGraphQLId(milestone.id).toString(),
+ );
+ });
+ });
+
+ describe('when SidebarDropdown emits `change` event', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when valid milestone is emitted', () => {
+ it('updates the hidden input value', async () => {
+ const milestone = {
+ id: 'gid://gitlab/Milestone/52',
+ title: __('Milestone 52'),
+ };
+
+ findSidebarDropdown().vm.$emit('change', milestone);
+ await nextTick();
+
+ expect(findHiddenInput().attributes('value')).toBe(
+ getIdFromGraphQLId(milestone.id).toString(),
+ );
+ });
+ });
+
+ describe('when null milestone is emitted', () => {
+ it('updates the hidden input value to `0`', async () => {
+ const milestone = { id: null };
+
+ findSidebarDropdown().vm.$emit('change', milestone);
+ await nextTick();
+
+ expect(findHiddenInput().attributes('value')).toBe('0');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js
new file mode 100644
index 00000000000..277ef6d9561
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_inputs_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import SidebarReviewersInputs from '~/sidebar/components/reviewers/sidebar_reviewers_inputs.vue';
+import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
+
+let wrapper;
+
+function factory() {
+ wrapper = shallowMount(SidebarReviewersInputs);
+}
+
+describe('Sidebar reviewers inputs component', () => {
+ it('renders hidden input', () => {
+ state.issuable.reviewers = {
+ nodes: [
+ {
+ id: 1,
+ avatarUrl: '',
+ name: 'root',
+ username: 'root',
+ mergeRequestInteraction: { canMerge: true },
+ },
+ {
+ id: 2,
+ avatarUrl: '',
+ name: 'root',
+ username: 'root',
+ mergeRequestInteraction: { canMerge: true },
+ },
+ ],
+ };
+
+ factory();
+
+ expect(wrapper.findAll('input[type="hidden"]').length).toBe(2);
+ });
+});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
new file mode 100644
index 00000000000..83bc8cf7002
--- /dev/null
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
@@ -0,0 +1,285 @@
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlFormInput,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { IssuableType } from '~/issues/constants';
+import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
+import { IssuableAttributeType } from '~/sidebar/constants';
+import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import {
+ emptyProjectMilestonesResponse,
+ mockIssue,
+ mockProjectMilestonesResponse,
+ noCurrentMilestoneResponse,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('SidebarDropdown component', () => {
+ let wrapper;
+
+ const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } };
+ const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData });
+
+ 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 findAttributeItems = () => wrapper.findByTestId('milestone-items');
+ const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item');
+ const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown');
+
+ const toggleDropdown = async () => {
+ wrapper.vm.$refs.dropdown.show();
+ findDropdown().vm.$emit('show');
+
+ await nextTick();
+ jest.runOnlyPendingTimers();
+ await waitForPromises();
+ };
+
+ const createComponentWithApollo = ({
+ requestHandlers = [],
+ projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse),
+ currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse),
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ wrapper = mountExtended(SidebarDropdown, {
+ apolloProvider: createMockApollo([
+ [projectMilestonesQuery, projectMilestonesSpy],
+ [projectIssueMilestoneQuery, currentMilestoneSpy],
+ ...requestHandlers,
+ ]),
+ propsData: {
+ attrWorkspacePath: mockIssue.projectPath,
+ currentAttribute: {},
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ },
+ attachTo: document.body,
+ });
+ };
+
+ const createComponent = ({
+ props = {},
+ data = {},
+ mutationPromise = mutationSuccess,
+ queries = {},
+ } = {}) => {
+ wrapper = mountExtended(SidebarDropdown, {
+ propsData: {
+ attrWorkspacePath: mockIssue.projectPath,
+ currentAttribute: {},
+ issuableType: IssuableType.Issue,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ mocks: {
+ $apollo: {
+ mutate: mutationPromise(),
+ queries: {
+ currentAttribute: { loading: false },
+ attributesList: { loading: false },
+ ...queries,
+ },
+ },
+ },
+ });
+ };
+
+ 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({
+ props: { currentAttribute: { id, title } },
+ data: { attributesList: [{ 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(findAllDropdownItems('No milestone').at(0).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({
+ props: { currentAttribute: { id: 'id', title: 'title' } },
+ data: { attributesList: [{ id: 'id', title: 'title' }] },
+ });
+
+ await toggleDropdown();
+
+ findDropdownItemWithText('title').vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+ });
+
+ describe('when a user is searching', () => {
+ describe('when search result is not found', () => {
+ describe('when milestone', () => {
+ it('renders "No milestone found"', async () => {
+ createComponent();
+
+ await toggleDropdown();
+
+ findSearchBox().vm.$emit('input', 'non existing milestones');
+ await nextTick();
+
+ expect(findDropdownText().text()).toBe('No milestone found');
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('with mock apollo', () => {
+ describe("when issuable type is 'issue'", () => {
+ describe('when dropdown is expanded and user can edit', () => {
+ it('renders the dropdown on clicking edit', async () => {
+ createComponentWithApollo();
+
+ await toggleDropdown();
+
+ expect(findDropdown().isVisible()).toBe(true);
+ });
+
+ it('focuses on the input when dropdown is shown', async () => {
+ createComponentWithApollo();
+
+ await toggleDropdown();
+
+ expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element);
+ });
+
+ describe('milestones', () => {
+ it('should call createAlert if milestones query fails', async () => {
+ createComponentWithApollo({
+ projectMilestonesSpy: jest.fn().mockRejectedValue(new Error()),
+ });
+
+ await toggleDropdown();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: wrapper.vm.i18n.listFetchError,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+
+ it('only fetches attributes when dropdown is opened', async () => {
+ const projectMilestonesSpy = jest
+ .fn()
+ .mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ createComponentWithApollo({ projectMilestonesSpy });
+
+ expect(projectMilestonesSpy).not.toHaveBeenCalled();
+
+ await toggleDropdown();
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, {
+ fullPath: mockIssue.projectPath,
+ state: 'active',
+ title: '',
+ });
+ });
+
+ describe('when a user is searching', () => {
+ it('sends a projectMilestones query with the entered search term "foo"', async () => {
+ const mockSearchTerm = 'foobar';
+ const projectMilestonesSpy = jest
+ .fn()
+ .mockResolvedValueOnce(emptyProjectMilestonesResponse);
+ createComponentWithApollo({ projectMilestonesSpy });
+
+ await toggleDropdown();
+
+ findSearchBox().vm.$emit('input', mockSearchTerm);
+ await nextTick();
+ jest.runOnlyPendingTimers(); // Account for debouncing
+
+ expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
+ fullPath: mockIssue.projectPath,
+ state: 'active',
+ title: mockSearchTerm,
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 8ab4d8ea051..cf5e220a705 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -1,12 +1,4 @@
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlLink,
- GlSearchBoxByType,
- GlFormInput,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlDropdown, GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -19,6 +11,7 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
+import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { IssuableAttributeType } from '~/sidebar/constants';
@@ -32,7 +25,6 @@ import {
noCurrentMilestoneResponse,
mockMilestoneMutationResponse,
mockMilestone2,
- emptyProjectMilestonesResponse,
} from '../mock_data';
jest.mock('~/flash');
@@ -55,20 +47,11 @@ describe('SidebarDropdownWidget', () => {
const findGlLink = () => wrapper.findComponent(GlLink);
const findDateTooltip = () => getBinding(findGlLink().element, 'gl-tooltip');
- 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 findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown);
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
@@ -167,6 +150,8 @@ describe('SidebarDropdownWidget', () => {
}),
);
+ wrapper.vm.$refs.dropdown.show = jest.fn();
+
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
@@ -261,86 +246,7 @@ describe('SidebarDropdownWidget', () => {
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) => {
@@ -350,7 +256,7 @@ describe('SidebarDropdownWidget', () => {
{ id: '123', title: '123' },
{ id: 'id', title: 'title' },
],
- currentAttribute: '123',
+ currentAttribute: { id: '123' },
},
mutationPromise: mutationResp,
});
@@ -366,7 +272,7 @@ describe('SidebarDropdownWidget', () => {
await toggleDropdown();
- findDropdownItemWithText('title').vm.$emit('click');
+ findSidebarDropdown().vm.$emit('change', { id: 'error' });
});
it(`calls createAlert with "${expectedMsg}"`, async () => {
@@ -382,24 +288,6 @@ describe('SidebarDropdownWidget', () => {
});
});
});
-
- describe('when a user is searching', () => {
- describe('when search result is not found', () => {
- describe('when milestone', () => {
- it('renders "No milestone found"', async () => {
- createComponent();
-
- await toggleDropdown();
-
- findSearchBox().vm.$emit('input', 'non existing milestones');
-
- await nextTick();
-
- expect(findDropdownText().text()).toBe('No milestone found');
- });
- });
- });
- });
});
});
@@ -424,18 +312,10 @@ describe('SidebarDropdownWidget', () => {
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', () => {
it('calls setIssueAttribute mutation', () => {
- findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
+ findSidebarDropdown().vm.$emit('change', { id: mockMilestone2.id });
expect(milestoneMutationSpy).toHaveBeenCalledWith({
iid: mockIssue.iid,
@@ -443,72 +323,6 @@ describe('SidebarDropdownWidget', () => {
fullPath: mockIssue.projectPath,
});
});
-
- it('sets the value returned from the mutation to currentAttribute', async () => {
- findDropdownItemWithText(mockMilestone2.title).vm.$emit('click');
- await nextTick();
- expect(findSelectedAttribute().text()).toBe(mockMilestone2.title);
- });
- });
- });
-
- describe('milestones', () => {
- let projectMilestonesSpy;
-
- it('should call createAlert if milestones query fails', async () => {
- await createComponentWithApollo({
- projectMilestonesSpy: jest.fn().mockRejectedValue(error),
- });
-
- await clickEdit();
-
- expect(createAlert).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,
- state: 'active',
- title: '',
- });
- });
-
- 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 nextTick();
-
- // Account for debouncing
- jest.runAllTimers();
-
- expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, {
- fullPath: mockIssue.projectPath,
- state: 'active',
- title: mockSearchTerm,
- });
- });
});
});
});
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 6e2908e659f..2eed1e30d0d 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -38,6 +38,10 @@ export const projectsWithScope = {
id: '2',
fullPath: 'root/332268-test',
name: 'root/332268-test',
+ namespace: {
+ id: '1234',
+ fullPath: 'root',
+ },
},
],
},
@@ -68,6 +72,10 @@ export const mockProjects = [
{
id: '1',
name: 'merge-train-stuff',
+ namespace: {
+ id: '1235',
+ fullPath: 'root',
+ },
fullPath: 'root/merge-train-stuff',
isLocked: false,
__typename: 'Project',
@@ -75,6 +83,10 @@ export const mockProjects = [
{
id: '2',
name: 'ci-project',
+ namespace: {
+ id: '1236',
+ fullPath: 'root',
+ },
fullPath: 'root/ci-project',
isLocked: true,
__typename: 'Project',
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js
index c55ac32b6a6..ea1d9db515a 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/token_access_spec.js
@@ -19,7 +19,8 @@ import {
} from './mock_data';
const projectPath = 'root/my-repo';
-const error = new Error('Error');
+const message = 'An error occurred';
+const error = new Error(message);
Vue.use(VueApollo);
@@ -144,7 +145,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message });
});
});
@@ -187,7 +188,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message });
});
});
});
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index 3bda0d0b530..0fa1a2453f7 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -1,5 +1,5 @@
import { GlTable, GlButton } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import TokenProjectsTable from '~/token_access/components/token_projects_table.vue';
import { mockProjects } from './mock_data';
@@ -7,7 +7,7 @@ describe('Token projects table', () => {
let wrapper;
const createComponent = () => {
- wrapper = mount(TokenProjectsTable, {
+ wrapper = mountExtended(TokenProjectsTable, {
provide: {
fullPath: 'root/ci-project',
},
@@ -18,9 +18,11 @@ describe('Token projects table', () => {
};
const findTable = () => wrapper.findComponent(GlTable);
- const findAllTableRows = () => wrapper.findAll('[data-testid="projects-token-table-row"]');
const findDeleteProjectBtn = () => wrapper.findComponent(GlButton);
const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton);
+ const findAllTableRows = () => wrapper.findAllByTestId('projects-token-table-row');
+ const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name');
+ const findProjectNamespaceCell = () => wrapper.findByTestId('token-access-project-namespace');
beforeEach(() => {
createComponent();
@@ -48,4 +50,9 @@ describe('Token projects table', () => {
// currently two mock projects with one being a locked project
expect(findAllDeleteProjectBtn()).toHaveLength(1);
});
+
+ it('displays project and namespace cells', () => {
+ expect(findProjectNameCell().text()).toBe('merge-train-stuff');
+ expect(findProjectNamespaceCell().text()).toBe('root');
+ });
});
diff --git a/spec/frontend/users_select/utils_spec.js b/spec/frontend/users_select/utils_spec.js
index a09935d8a04..7a080ddaf0f 100644
--- a/spec/frontend/users_select/utils_spec.js
+++ b/spec/frontend/users_select/utils_spec.js
@@ -1,21 +1,10 @@
-import $ from 'jquery';
-import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from '~/users_select/utils';
+import { getAjaxUsersSelectParams } from '~/users_select/utils';
const options = {
fooBar: 'baz',
activeUserId: 1,
};
-describe('getAjaxUsersSelectOptions', () => {
- it('returns options built from select data attributes', () => {
- const $select = $('<select />', { 'data-foo-bar': 'baz', 'data-user-id': 1 });
-
- expect(
- getAjaxUsersSelectOptions($select, { fooBar: 'fooBar', activeUserId: 'user-id' }),
- ).toEqual(options);
- });
-});
-
describe('getAjaxUsersSelectParams', () => {
it('returns query parameters built from provided options', () => {
expect(
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
index 4e3e918f7fb..8dadb0c65d0 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
@@ -20,7 +20,7 @@ describe('MrWidgetContainer', () => {
it('has layout', () => {
factory();
- expect(wrapper.classes()).toContain('mr-widget-heading');
+ expect(wrapper.classes()).toEqual(['mr-section-container', 'mr-widget-workflow']);
expect(wrapper.find('.mr-widget-content').exists()).toBe(true);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
index 7f0173b7445..144e176b0f0 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
@@ -222,6 +222,7 @@ describe('MRWidgetPipeline', () => {
beforeEach(() => {
({ pipeline } = JSON.parse(JSON.stringify(mockData)));
+ pipeline.details.event_type_name = 'Pipeline';
pipeline.details.name = 'Pipeline';
pipeline.merge_request_event_type = undefined;
pipeline.ref.tag = false;
@@ -263,6 +264,7 @@ describe('MRWidgetPipeline', () => {
describe('for a detached merge request pipeline', () => {
it('renders a pipeline widget that reads "Merge request pipeline <ID> <status> for <SHA>"', () => {
+ pipeline.details.event_type_name = 'Merge request pipeline';
pipeline.details.name = 'Merge request pipeline';
pipeline.merge_request_event_type = 'detached';
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
index 05c259de370..7b52773e92d 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
@@ -8,7 +8,7 @@ jest.mock('~/vue_shared/plugins/global_toast');
let wrapper;
-function createWrapper(propsData, mergeRequestWidgetGraphql) {
+function createWrapper(propsData) {
wrapper = mount(WidgetRebase, {
propsData,
data() {
@@ -22,7 +22,6 @@ function createWrapper(propsData, mergeRequestWidgetGraphql) {
},
};
},
- provide: { glFeatures: { mergeRequestWidgetGraphql } },
mocks: {
$apollo: {
queries: {
@@ -43,276 +42,244 @@ describe('Merge request widget rebase component', () => {
wrapper.destroy();
wrapper = null;
});
-
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
- describe('while rebasing', () => {
- it('should show progress message', () => {
- createWrapper(
- {
- mr: { rebaseInProgress: true },
- service: {},
- },
- mergeRequestWidgetGraphql,
- );
-
- expect(findRebaseMessageText()).toContain('Rebase in progress');
- });
+ describe('while rebasing', () => {
+ it('should show progress message', () => {
+ createWrapper({
+ mr: { rebaseInProgress: true },
+ service: {},
});
- describe('with permissions', () => {
- const rebaseMock = jest.fn().mockResolvedValue();
- const pollMock = jest.fn().mockResolvedValue({});
+ expect(findRebaseMessageText()).toContain('Rebase in progress');
+ });
+ });
+
+ describe('with permissions', () => {
+ const rebaseMock = jest.fn().mockResolvedValue();
+ const pollMock = jest.fn().mockResolvedValue({});
- it('renders the warning message', () => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
+ it('renders the warning message', () => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ });
- const text = findRebaseMessageText();
+ const text = findRebaseMessageText();
- expect(text).toContain('Merge blocked');
- expect(text.replace(/\s\s+/g, ' ')).toContain(
- 'the source branch must be rebased onto the target branch',
- );
- });
+ expect(text).toContain('Merge blocked');
+ expect(text.replace(/\s\s+/g, ' ')).toContain(
+ 'the source branch must be rebased onto the target branch',
+ );
+ });
- it('renders an error message when rebasing has failed', async () => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
+ it('renders an error message when rebasing has failed', async () => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ });
+
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({ rebasingError: 'Something went wrong!' });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ rebasingError: 'Something went wrong!' });
+ await nextTick();
+ expect(findRebaseMessageText()).toContain('Something went wrong!');
+ });
- await nextTick();
- expect(findRebaseMessageText()).toContain('Something went wrong!');
+ describe('Rebase buttons', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
});
+ });
- describe('Rebase buttons', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ it('renders both buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
- it('renders both buttons', () => {
- expect(findRebaseWithoutCiButton().exists()).toBe(true);
- expect(findStandardRebaseButton().exists()).toBe(true);
- });
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
- it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ await nextTick();
- await nextTick();
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
- });
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
- it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
- findRebaseWithoutCiButton().vm.$emit('click');
+ await nextTick();
- await nextTick();
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
- });
+ describe('Rebase when pipelines must succeed is enabled', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
});
+ });
- describe('Rebase when pipelines must succeed is enabled', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ it('renders only the rebase button', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(false);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
- it('renders only the rebase button', () => {
- expect(findRebaseWithoutCiButton().exists()).toBe(false);
- expect(findStandardRebaseButton().exists()).toBe(true);
- });
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
- it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ await nextTick();
- await nextTick();
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+ });
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
- });
+ describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: true,
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
});
+ });
- describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- allowMergeOnSkippedPipeline: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ it('renders both rebase buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
- it('renders both rebase buttons', () => {
- expect(findRebaseWithoutCiButton().exists()).toBe(true);
- expect(findStandardRebaseButton().exists()).toBe(true);
- });
+ await nextTick();
- it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
- await nextTick();
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
- });
+ await nextTick();
- it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
- findRebaseWithoutCiButton().vm.$emit('click');
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+ });
- await nextTick();
+ describe('without permissions', () => {
+ const exampleTargetBranch = 'fake-branch-to-test-with';
- expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
- });
+ describe('UI text', () => {
+ beforeEach(() => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ },
+ service: {},
});
});
- describe('without permissions', () => {
- const exampleTargetBranch = 'fake-branch-to-test-with';
-
- describe('UI text', () => {
- beforeEach(() => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: false,
- targetBranch: exampleTargetBranch,
- },
- service: {},
- },
- mergeRequestWidgetGraphql,
- );
- });
-
- it('renders a message explaining user does not have permissions', () => {
- const text = findRebaseMessageText();
-
- expect(text).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
- expect(text).toContain('the source branch must be rebased');
- });
-
- it('renders the correct target branch name', () => {
- const elem = findRebaseMessage();
-
- expect(elem.text()).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
- });
- });
+ it('renders a message explaining user does not have permissions', () => {
+ const text = findRebaseMessageText();
- it('does render the "Rebase without pipeline" button', () => {
- createWrapper(
- {
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: false,
- targetBranch: exampleTargetBranch,
- },
- service: {},
- },
- mergeRequestWidgetGraphql,
- );
+ expect(text).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ expect(text).toContain('the source branch must be rebased');
+ });
- expect(findRebaseWithoutCiButton().exists()).toBe(true);
- });
+ it('renders the correct target branch name', () => {
+ const elem = findRebaseMessage();
+
+ expect(elem.text()).toContain(
+ 'Merge blocked: the source branch must be rebased onto the target branch.',
+ );
+ });
+ });
+
+ it('does render the "Rebase without pipeline" button', () => {
+ createWrapper({
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ },
+ service: {},
});
- describe('methods', () => {
- it('checkRebaseStatus', async () => {
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- createWrapper(
- {
- mr: {},
- service: {
- rebase() {
- return Promise.resolve();
- },
- poll() {
- return Promise.resolve({
- data: {
- rebase_in_progress: false,
- should_be_rebased: false,
- merge_error: null,
- },
- });
- },
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ });
+ });
+
+ describe('methods', () => {
+ it('checkRebaseStatus', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ createWrapper({
+ mr: {},
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ should_be_rebased: false,
+ merge_error: null,
},
- },
- mergeRequestWidgetGraphql,
- );
+ });
+ },
+ },
+ });
- wrapper.vm.rebase();
+ wrapper.vm.rebase();
- // Wait for the rebase request
- await nextTick();
- // Wait for the polling request
- await nextTick();
- // Wait for the eventHub to be called
- await nextTick();
+ // Wait for the rebase request
+ await nextTick();
+ // Wait for the polling request
+ await nextTick();
+ // Wait for the eventHub to be called
+ await nextTick();
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
- expect(toast).toHaveBeenCalledWith('Rebase completed');
- });
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
+ expect(toast).toHaveBeenCalledWith('Rebase completed');
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
index 5f383c468d8..bd40a968392 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
+exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
<div
- class="mr-widget-body media"
+ class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
>
<div
class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
@@ -44,210 +44,18 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
class="gl-display-flex gl-w-full"
>
<div
- class="media-body gl-display-flex"
+ class="media-body gl-display-flex gl-align-items-center"
>
<h4
class="gl-mr-3"
data-testid="statusText"
>
- Set by
- <a
- class="author-link inline"
- >
- <img
- class="avatar avatar-inline s16"
- src="no_avatar.png"
- />
-
- <span
- class="author"
- >
-
- </span>
- </a>
- to be merged automatically when the pipeline succeeds
- </h4>
-
- <div
- class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
- >
- <div
- class="gl-display-flex gl-align-items-flex-start"
- >
- <div
- class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
- lazy=""
- no-caret=""
- title="Options"
- >
- <!---->
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="dropdown-icon gl-icon s16"
- data-testid="ellipsis_v-icon"
- role="img"
- >
- <use
- href="#ellipsis_v"
- />
- </svg>
-
- <span
- class="gl-new-dropdown-button-text gl-sr-only"
- >
-
- </span>
-
- <svg
- aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
- data-testid="chevron-down-icon"
- role="img"
- >
- <use
- href="#chevron-down"
- />
- </svg>
- </button>
- <ul
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
- </div>
-
- <button
- class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Cancel auto-merge
-
- </span>
- </button>
- </div>
- </div>
- </div>
-
- <div
- class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
- >
- <button
- class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
- title="Collapse merge details"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="chevron-lg-up-icon"
- role="img"
- >
- <use
- href="#chevron-lg-up"
- />
- </svg>
-
- <!---->
- </button>
- </div>
- </div>
-</div>
-`;
-
-exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
-<div
- class="mr-widget-body media"
->
- <div
- class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
- >
- <div
- class="gl-display-flex gl-m-auto"
- >
- <div
- class="gl-mr-3 gl-p-2 gl-m-0! gl-text-blue-500 gl-w-6 gl-p-2"
- >
- <div
- class="gl-rounded-full gl-relative gl-display-flex mr-widget-extension-icon"
- >
- <div
- class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto"
- >
- <div
- class="gl-display-flex gl-m-auto gl-translate-y-n50"
- >
- <svg
- aria-label="Scheduled "
- class="gl-display-block gl-icon s12"
- data-qa-selector="status_scheduled_icon"
- data-testid="status-scheduled-icon"
- role="img"
- >
- <use
- href="#status-scheduled"
- />
- </svg>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-w-full"
- >
- <div
- class="media-body gl-display-flex"
- >
-
- <h4
- class="gl-mr-3"
- data-testid="statusText"
- >
- Set by
- <a
- class="author-link inline"
- >
- <img
- class="avatar avatar-inline s16"
- src="no_avatar.png"
- />
-
- <span
- class="author"
- >
-
- </span>
- </a>
- to be merged automatically when the pipeline succeeds
+ Set by to be merged automatically when the pipeline succeeds
</h4>
<div
- class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
+ class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3"
>
<div
class="gl-display-flex gl-align-items-flex-start"
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index d85574262fe..8eeba4d6274 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import MergeChecksFailed from '~/vue_merge_request_widget/components/states/merge_checks_failed.vue';
+import { DETAILED_MERGE_STATUS } from '~/vue_merge_request_widget/constants';
let wrapper;
@@ -15,9 +16,10 @@ describe('Merge request widget merge checks failed state component', () => {
});
it.each`
- mrState | displayText
- ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
- ${{ detailedMergeStatus: 'BLOCKED_STATUS' }} | ${'blockingMergeRequests'}
+ mrState | displayText
+ ${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
+ ${{ detailedMergeStatus: DETAILED_MERGE_STATUS.BLOCKED_STATUS }} | ${'blockingMergeRequests'}
+ ${{ detailedMergeStatus: DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS }} | ${'externalStatusChecksFailed'}
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 28182793683..5b9f30dfb86 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -9,7 +9,6 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
let wrapper;
-let mergeRequestWidgetGraphqlEnabled = false;
function convertPropsToGraphqlState(props) {
return {
@@ -30,12 +29,6 @@ function convertPropsToGraphqlState(props) {
}
function factory(propsData, stateOverride = {}) {
- let state = {};
-
- if (mergeRequestWidgetGraphqlEnabled) {
- state = { ...convertPropsToGraphqlState(propsData), ...stateOverride };
- }
-
wrapper = extendedWrapper(
mount(autoMergeEnabledComponent, {
propsData: {
@@ -43,9 +36,8 @@ function factory(propsData, stateOverride = {}) {
service: new MRWidgetService({}),
},
data() {
- return { state };
+ return { ...convertPropsToGraphqlState(propsData), ...stateOverride };
},
- provide: { glFeatures: { mergeRequestWidgetGraphql: mergeRequestWidgetGraphqlEnabled } },
mocks: {
$apollo: {
queries: {
@@ -95,130 +87,88 @@ describe('MRWidgetAutoMergeEnabled', () => {
wrapper = null;
});
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
- beforeEach(() => {
- mergeRequestWidgetGraphqlEnabled = mergeRequestWidgetGraphql;
+ describe('computed', () => {
+ describe('cancelButtonText', () => {
+ it('should return "Cancel" if MWPS is selected', () => {
+ factory({
+ ...defaultMrProps(),
+ autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ });
+
+ expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe('Cancel auto-merge');
});
+ });
+ });
- describe('computed', () => {
- describe('cancelButtonText', () => {
- it('should return "Cancel" if MWPS is selected', () => {
- factory({
- ...defaultMrProps(),
- autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ describe('methods', () => {
+ describe('cancelAutomaticMerge', () => {
+ it('should set flag and call service then tell main component to update the widget with data', async () => {
+ factory({
+ ...defaultMrProps(),
+ });
+ const mrObj = {
+ is_new_mr_data: true,
+ };
+ jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue(
+ new Promise((resolve) => {
+ resolve({
+ data: mrObj,
});
+ }),
+ );
- expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe(
- 'Cancel auto-merge',
- );
- });
- });
- });
+ wrapper.vm.cancelAutomaticMerge();
- describe('methods', () => {
- describe('cancelAutomaticMerge', () => {
- it('should set flag and call service then tell main component to update the widget with data', async () => {
- factory({
- ...defaultMrProps(),
- });
- const mrObj = {
- is_new_mr_data: true,
- };
- jest.spyOn(wrapper.vm.service, 'cancelAutomaticMerge').mockReturnValue(
- new Promise((resolve) => {
- resolve({
- data: mrObj,
- });
- }),
- );
-
- wrapper.vm.cancelAutomaticMerge();
-
- await waitForPromises();
-
- expect(wrapper.vm.isCancellingAutoMerge).toBe(true);
- if (mergeRequestWidgetGraphql) {
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- } else {
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- }
- });
- });
+ await waitForPromises();
- describe('removeSourceBranch', () => {
- it('should set flag and call service then request main component to update the widget', async () => {
- factory({
- ...defaultMrProps(),
- });
- jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(
- Promise.resolve({
- data: {
- status: MWPS_MERGE_STRATEGY,
- },
- }),
- );
-
- wrapper.vm.removeSourceBranch();
-
- await waitForPromises();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- expect(wrapper.vm.service.merge).toHaveBeenCalledWith({
- sha,
- auto_merge_strategy: MWPS_MERGE_STRATEGY,
- should_remove_source_branch: true,
- });
- });
- });
+ expect(wrapper.vm.isCancellingAutoMerge).toBe(true);
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
});
+ });
+ });
- describe('template', () => {
- it('should have correct elements', () => {
- factory({
- ...defaultMrProps(),
- });
+ describe('template', () => {
+ it('should have correct elements', () => {
+ factory({
+ ...defaultMrProps(),
+ });
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
+ });
- it('should disable cancel auto merge button when the action is in progress', async () => {
- factory({
- ...defaultMrProps(),
- });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isCancellingAutoMerge: true,
- });
+ it('should disable cancel auto merge button when the action is in progress', async () => {
+ factory({
+ ...defaultMrProps(),
+ });
+ // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({
+ isCancellingAutoMerge: true,
+ });
- await nextTick();
+ await nextTick();
- expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true);
- });
+ expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true);
+ });
- it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
- factory({
- ...defaultMrProps(),
- autoMergeStrategy: MWPS_MERGE_STRATEGY,
- });
+ it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
+ factory({
+ ...defaultMrProps(),
+ autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ });
- expect(getStatusText()).toContain(
- 'to be merged automatically when the pipeline succeeds',
- );
- });
+ expect(getStatusText()).toContain('to be merged automatically when the pipeline succeeds');
+ });
- it('should render the cancel button as "Cancel" if MWPS is selected', () => {
- factory({
- ...defaultMrProps(),
- autoMergeStrategy: MWPS_MERGE_STRATEGY,
- });
+ it('should render the cancel button as "Cancel" if MWPS is selected', () => {
+ factory({
+ ...defaultMrProps(),
+ autoMergeStrategy: MWPS_MERGE_STRATEGY,
+ });
- const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
+ const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
- expect(cancelButtonText).toBe('Cancel auto-merge');
- });
- });
+ expect(cancelButtonText).toBe('Cancel auto-merge');
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
index 398a3912882..826f708069c 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -9,18 +9,11 @@ describe('MRWidgetAutoMergeFailed', () => {
const mergeError = 'This is the merge error';
const findButton = () => wrapper.findComponent(GlButton);
- const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
+ const createComponent = (props = {}) => {
wrapper = mount(AutoMergeFailedComponent, {
propsData: { ...props },
data() {
- if (mergeRequestWidgetGraphql) {
- return { mergeError: props.mr?.mergeError };
- }
-
- return {};
- },
- provide: {
- glFeatures: { mergeRequestWidgetGraphql },
+ return { mergeError: props.mr?.mergeError };
},
});
};
@@ -29,40 +22,33 @@ describe('MRWidgetAutoMergeFailed', () => {
wrapper.destroy();
});
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
- beforeEach(() => {
- createComponent(
- {
- mr: { mergeError },
- },
- mergeRequestWidgetGraphql,
- );
- });
+ beforeEach(() => {
+ createComponent({
+ mr: { mergeError },
+ });
+ });
- it('renders failed message', () => {
- expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
- });
+ it('renders failed message', () => {
+ expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
+ });
- it('renders merge error provided', () => {
- expect(wrapper.text()).toContain(mergeError);
- });
+ it('renders merge error provided', () => {
+ expect(wrapper.text()).toContain(mergeError);
+ });
- it('render refresh button', () => {
- expect(findButton().text()).toBe('Refresh');
- });
+ it('render refresh button', () => {
+ expect(findButton().text()).toBe('Refresh');
+ });
- it('emits event and shows loading icon when button is clicked', async () => {
- jest.spyOn(eventHub, '$emit');
- findButton().vm.$emit('click');
+ it('emits event and shows loading icon when button is clicked', async () => {
+ jest.spyOn(eventHub, '$emit');
+ findButton().vm.$emit('click');
- expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
+ expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
- await nextTick();
+ await nextTick();
- expect(findButton().attributes('disabled')).toBe('disabled');
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
+ expect(findButton().attributes('disabled')).toBe('disabled');
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
index 7a9fd5b002d..a16e4d4a6ea 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
@@ -7,7 +7,6 @@ import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_
describe('MRWidgetConflicts', () => {
let wrapper;
- let mergeRequestWidgetGraphql = null;
const path = '/conflicts';
const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button');
@@ -25,10 +24,17 @@ describe('MRWidgetConflicts', () => {
wrapper = extendedWrapper(
mount(ConflictsComponent, {
propsData,
- provide: {
- glFeatures: {
- mergeRequestWidgetGraphql,
- },
+ data() {
+ return {
+ userPermissions: {
+ canMerge: propsData.mr.canMerge,
+ pushToSourceBranch: propsData.mr.canPushToSourceBranch,
+ },
+ state: {
+ shouldBeRebased: propsData.mr.shouldBeRebased,
+ sourceBranchProtected: propsData.mr.sourceBranchProtected,
+ },
+ };
},
mocks: {
$apollo: {
@@ -41,212 +47,188 @@ describe('MRWidgetConflicts', () => {
}),
);
- if (mergeRequestWidgetGraphql) {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- userPermissions: {
- canMerge: propsData.mr.canMerge,
- pushToSourceBranch: propsData.mr.canPushToSourceBranch,
- },
- stateData: {
- shouldBeRebased: propsData.mr.shouldBeRebased,
- sourceBranchProtected: propsData.mr.sourceBranchProtected,
- },
- });
- }
-
await nextTick();
}
afterEach(() => {
- mergeRequestWidgetGraphql = null;
wrapper.destroy();
});
- [false, true].forEach((featureEnabled) => {
- describe(`with GraphQL feature flag ${featureEnabled ? 'enabled' : 'disabled'}`, () => {
- beforeEach(() => {
- mergeRequestWidgetGraphql = featureEnabled;
+ // There are two permissions we need to consider:
+ //
+ // 1. Is the user allowed to merge to the target branch?
+ // 2. Is the user allowed to push to the source branch?
+ //
+ // This yields 4 possible permutations that we need to test, and
+ // we test them below. A user who can push to the source
+ // branch should be allowed to resolve conflicts. This is
+ // consistent with what the backend does.
+ describe('when allowed to merge but not allowed to push to source branch', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: false,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
});
+ });
- // There are two permissions we need to consider:
- //
- // 1. Is the user allowed to merge to the target branch?
- // 2. Is the user allowed to push to the source branch?
- //
- // This yields 4 possible permutations that we need to test, and
- // we test them below. A user who can push to the source
- // branch should be allowed to resolve conflicts. This is
- // consistent with what the backend does.
- describe('when allowed to merge but not allowed to push to source branch', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: false,
- conflictResolutionPath: path,
- conflictsDocsPath: '',
- },
- });
- });
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(wrapper.text()).toContain(mergeConflictsText);
+ expect(wrapper.text()).not.toContain(userCannotMergeText);
+ });
- it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
- });
+ it('should not allow you to resolve the conflicts', () => {
+ expect(wrapper.text()).not.toContain(resolveConflictsBtnText);
+ });
- it('should not allow you to resolve the conflicts', () => {
- expect(wrapper.text()).not.toContain(resolveConflictsBtnText);
- });
+ it('should have merge buttons', () => {
+ expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
+ });
+ });
- it('should have merge buttons', () => {
- expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
- });
+ describe('when not allowed to merge but allowed to push to source branch', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
});
+ });
- describe('when not allowed to merge but allowed to push to source branch', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: false,
- canPushToSourceBranch: true,
- conflictResolutionPath: path,
- conflictsDocsPath: '',
- },
- });
- });
-
- it('should tell you about conflicts', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).toContain(userCannotMergeText);
- });
-
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
- expect(findResolveButton().attributes('href')).toEqual(path);
- });
-
- it('should not have merge buttons', () => {
- expect(wrapper.text()).not.toContain(mergeLocallyBtnText);
- });
+ it('should tell you about conflicts', () => {
+ expect(wrapper.text()).toContain(mergeConflictsText);
+ expect(wrapper.text()).toContain(userCannotMergeText);
+ });
+
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
+ expect(findResolveButton().attributes('href')).toEqual(path);
+ });
+
+ it('should not have merge buttons', () => {
+ expect(wrapper.text()).not.toContain(mergeLocallyBtnText);
+ });
+ });
+
+ describe('when allowed to merge and push to source branch', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: path,
+ conflictsDocsPath: '',
+ },
});
+ });
- describe('when allowed to merge and push to source branch', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: true,
- conflictResolutionPath: path,
- conflictsDocsPath: '',
- },
- });
- });
-
- it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
- });
-
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
- expect(findResolveButton().attributes('href')).toEqual(path);
- });
-
- it('should have merge buttons', () => {
- expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
- });
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(wrapper.text()).toContain(mergeConflictsText);
+ expect(wrapper.text()).not.toContain(userCannotMergeText);
+ });
+
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
+ expect(findResolveButton().attributes('href')).toEqual(path);
+ });
+
+ it('should have merge buttons', () => {
+ expect(findMergeLocalButton().text()).toContain(mergeLocallyBtnText);
+ });
+ });
+
+ describe('when user does not have permission to push to source branch', () => {
+ it('should show proper message', async () => {
+ await createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: false,
+ conflictsDocsPath: '',
+ },
});
- describe('when user does not have permission to push to source branch', () => {
- it('should show proper message', async () => {
- await createComponent({
- mr: {
- canMerge: false,
- canPushToSourceBranch: false,
- conflictsDocsPath: '',
- },
- });
+ expect(wrapper.text().trim().replace(/\s\s+/g, ' ')).toContain(userCannotMergeText);
+ });
- expect(wrapper.text().trim().replace(/\s\s+/g, ' ')).toContain(userCannotMergeText);
- });
+ it('should not have action buttons', async () => {
+ await createComponent({
+ mr: {
+ canMerge: false,
+ canPushToSourceBranch: false,
+ conflictsDocsPath: '',
+ },
+ });
- it('should not have action buttons', async () => {
- await createComponent({
- mr: {
- canMerge: false,
- canPushToSourceBranch: false,
- conflictsDocsPath: '',
- },
- });
-
- expect(findResolveButton().exists()).toBe(false);
- expect(findMergeLocalButton().exists()).toBe(false);
- });
-
- it('should not have resolve button when no conflict resolution path', async () => {
- await createComponent({
- mr: {
- canMerge: true,
- conflictResolutionPath: null,
- conflictsDocsPath: '',
- },
- });
+ expect(findResolveButton().exists()).toBe(false);
+ expect(findMergeLocalButton().exists()).toBe(false);
+ });
- expect(findResolveButton().exists()).toBe(false);
- });
+ it('should not have resolve button when no conflict resolution path', async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ conflictResolutionPath: null,
+ conflictsDocsPath: '',
+ },
});
- describe('when fast-forward or semi-linear merge enabled', () => {
- it('should tell you to rebase locally', async () => {
- await createComponent({
- mr: {
- shouldBeRebased: true,
- conflictsDocsPath: '',
- },
- });
+ expect(findResolveButton().exists()).toBe(false);
+ });
+ });
- expect(removeBreakLine(wrapper.text()).trim()).toContain(fastForwardMergeText);
- });
+ describe('when fast-forward or semi-linear merge enabled', () => {
+ it('should tell you to rebase locally', async () => {
+ await createComponent({
+ mr: {
+ shouldBeRebased: true,
+ conflictsDocsPath: '',
+ },
});
- describe('when source branch protected', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: true,
- conflictResolutionPath: TEST_HOST,
- sourceBranchProtected: true,
- conflictsDocsPath: '',
- },
- });
- });
+ expect(removeBreakLine(wrapper.text()).trim()).toContain(fastForwardMergeText);
+ });
+ });
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().exists()).toBe(true);
- });
+ describe('when source branch protected', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: TEST_HOST,
+ sourceBranchProtected: true,
+ conflictsDocsPath: '',
+ },
});
+ });
- describe('when source branch not protected', () => {
- beforeEach(async () => {
- await createComponent({
- mr: {
- canMerge: true,
- canPushToSourceBranch: true,
- conflictResolutionPath: TEST_HOST,
- sourceBranchProtected: false,
- conflictsDocsPath: '',
- },
- });
- });
+ it('should not allow you to resolve the conflicts', () => {
+ expect(findResolveButton().exists()).toBe(false);
+ });
+ });
- it('should allow you to resolve the conflicts', () => {
- expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
- expect(findResolveButton().attributes('href')).toEqual(TEST_HOST);
- });
+ describe('when source branch not protected', () => {
+ beforeEach(async () => {
+ await createComponent({
+ mr: {
+ canMerge: true,
+ canPushToSourceBranch: true,
+ conflictResolutionPath: TEST_HOST,
+ sourceBranchProtected: false,
+ conflictsDocsPath: '',
+ },
});
});
+
+ it('should allow you to resolve the conflicts', () => {
+ expect(findResolveButton().text()).toContain(resolveConflictsBtnText);
+ expect(findResolveButton().attributes('href')).toEqual(TEST_HOST);
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
index ddce07954ab..f29cf55f7ce 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
@@ -1,26 +1,17 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import MissingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
let wrapper;
-async function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
+function factory(sourceBranchRemoved) {
wrapper = shallowMount(MissingBranchComponent, {
propsData: {
mr: { sourceBranchRemoved },
},
- provide: {
- glFeatures: { mergeRequestWidgetGraphql },
+ data() {
+ return { state: { sourceBranchExists: !sourceBranchRemoved } };
},
});
-
- if (mergeRequestWidgetGraphql) {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } });
- }
-
- await nextTick();
}
describe('MRWidgetMissingBranch', () => {
@@ -28,22 +19,16 @@ describe('MRWidgetMissingBranch', () => {
wrapper.destroy();
});
- [true, false].forEach((mergeRequestWidgetGraphql) => {
- describe(`widget GraphQL feature flag is ${
- mergeRequestWidgetGraphql ? 'enabled' : 'disabled'
- }`, () => {
- it.each`
- sourceBranchRemoved | branchName
- ${true} | ${'source'}
- ${false} | ${'target'}
- `(
- 'should set missing branch name as $branchName when sourceBranchRemoved is $sourceBranchRemoved',
- async ({ sourceBranchRemoved, branchName }) => {
- await factory(sourceBranchRemoved, mergeRequestWidgetGraphql);
+ it.each`
+ sourceBranchRemoved | branchName
+ ${true} | ${'source'}
+ ${false} | ${'target'}
+ `(
+ 'should set missing branch name as $branchName when sourceBranchRemoved is $sourceBranchRemoved',
+ ({ sourceBranchRemoved, branchName }) => {
+ factory(sourceBranchRemoved);
- expect(wrapper.find('[data-testid="widget-content"]').text()).toContain(branchName);
- },
- );
- });
- });
+ expect(wrapper.find('[data-testid="widget-content"]').text()).toContain(branchName);
+ },
+ );
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 48d3f15560b..407bd60b2b7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -60,6 +60,11 @@ const createTestMr = (customConfig) => {
translateStateToMachine: () => this.transitionStateMachine(),
state: 'open',
canMerge: true,
+ mergeable: true,
+ userPermissions: {
+ removeSourceBranch: true,
+ canMerge: true,
+ },
};
Object.assign(mr, customConfig.mr);
@@ -68,7 +73,7 @@ const createTestMr = (customConfig) => {
};
const createTestService = () => ({
- merge: jest.fn(),
+ merge: jest.fn().mockResolvedValue(),
poll: jest.fn().mockResolvedValue(),
});
@@ -87,21 +92,24 @@ const createReadyToMergeResponse = (customMr) => {
});
};
-const createComponent = (
- customConfig = {},
- mergeRequestWidgetGraphql = false,
- restructuredMrWidget = true,
-) => {
+const createComponent = (customConfig = {}, createState = true) => {
wrapper = shallowMount(ReadyToMerge, {
propsData: {
mr: createTestMr(customConfig),
service: createTestService(),
},
- provide: {
- glFeatures: {
- mergeRequestWidgetGraphql,
- restructuredMrWidget,
- },
+ data() {
+ if (createState) {
+ return {
+ loading: false,
+ state: {
+ ...createTestMr(customConfig),
+ },
+ };
+ }
+ return {
+ loading: true,
+ };
},
stubs: {
CommitEdit,
@@ -136,7 +144,7 @@ describe('ReadyToMerge', () => {
describe('computed', () => {
describe('isAutoMergeAvailable', () => {
it('should return true when at least one merge strategy is available', () => {
- createComponent();
+ createComponent({});
expect(wrapper.vm.isAutoMergeAvailable).toBe(true);
});
@@ -168,14 +176,14 @@ describe('ReadyToMerge', () => {
});
it('returns pending when pipeline is active', () => {
- createComponent({ mr: { pipeline: {}, isPipelineActive: true } });
+ createComponent({ mr: { pipeline: { active: true }, isPipelineActive: true } });
expect(wrapper.vm.status).toEqual('pending');
});
it('returns failed when pipeline is failed', () => {
createComponent({
- mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] },
+ mr: { pipeline: { status: 'FAILED' }, availableAutoMergeStrategies: [], hasCI: true },
});
expect(wrapper.vm.status).toEqual('failed');
@@ -185,7 +193,7 @@ describe('ReadyToMerge', () => {
describe('Merge Button Variant', () => {
it('defaults to confirm class', () => {
createComponent({
- mr: { availableAutoMergeStrategies: [] },
+ mr: { availableAutoMergeStrategies: [], mergeable: true },
});
expect(findMergeButton().attributes('variant')).toBe('confirm');
@@ -194,19 +202,19 @@ describe('ReadyToMerge', () => {
describe('status icon', () => {
it('defaults to tick icon', () => {
- createComponent();
+ createComponent({ mr: { mergeable: true } });
expect(wrapper.vm.iconClass).toEqual('success');
});
it('shows tick for success status', () => {
- createComponent({ mr: { pipeline: true } });
+ createComponent({ mr: { pipeline: { status: 'SUCCESS' }, mergeable: true } });
expect(wrapper.vm.iconClass).toEqual('success');
});
it('shows tick for pending status', () => {
- createComponent({ mr: { pipeline: {}, isPipelineActive: true } });
+ createComponent({ mr: { pipeline: { active: true }, mergeable: true } });
expect(wrapper.vm.iconClass).toEqual('success');
});
@@ -266,7 +274,7 @@ describe('ReadyToMerge', () => {
describe('isMergeButtonDisabled', () => {
it('should return false with initial data', () => {
- createComponent({ mr: { isMergeAllowed: true } });
+ createComponent({ mr: { isMergeAllowed: true, mergeable: false } });
expect(wrapper.vm.isMergeButtonDisabled).toBe(false);
});
@@ -283,6 +291,7 @@ describe('ReadyToMerge', () => {
isMergeAllowed: false,
availableAutoMergeStrategies: [],
onlyAllowMergeIfPipelineSucceeds: true,
+ mergeable: false,
},
});
@@ -544,7 +553,15 @@ describe('ReadyToMerge', () => {
describe('Remove source branch checkbox', () => {
describe('when user can merge but cannot delete branch', () => {
it('should be disabled in the rendered output', () => {
- createComponent();
+ createComponent({
+ mr: {
+ mergeable: true,
+ userPermissions: {
+ removeSourceBranch: false,
+ canMerge: true,
+ },
+ },
+ });
expect(wrapper.find('#remove-source-branch-input').exists()).toBe(false);
});
@@ -553,7 +570,7 @@ describe('ReadyToMerge', () => {
describe('when user can merge and can delete branch', () => {
beforeEach(() => {
createComponent({
- mr: { canRemoveSourceBranch: true },
+ mr: { canRemoveSourceBranch: true, mergeable: true },
});
});
@@ -567,7 +584,7 @@ describe('ReadyToMerge', () => {
describe('squash checkbox', () => {
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
createComponent({
- mr: { commitsCount: 2, enableSquashBeforeMerge: true },
+ mr: { commitsCount: 2, enableSquashBeforeMerge: true, mergeable: true },
});
expect(findCheckboxElement().exists()).toBe(true);
@@ -665,6 +682,7 @@ describe('ReadyToMerge', () => {
squashIsSelected: true,
enableSquashBeforeMerge: true,
commitsCount: 2,
+ mergeRequestsFfOnlyEnabled: true,
},
});
@@ -795,7 +813,9 @@ describe('ReadyToMerge', () => {
});
it('shows the diverged commits text when the source branch is behind the target', () => {
- createComponent({ mr: { divergedCommitsCount: 9001, canMerge: false } });
+ createComponent({
+ mr: { divergedCommitsCount: 9001, userPermissions: { canMerge: false }, canMerge: false },
+ });
expect(wrapper.text()).toEqual(
expect.stringContaining('The source branch is 9001 commits behind the target branch'),
@@ -807,7 +827,7 @@ describe('ReadyToMerge', () => {
describe('Merge button when pipeline has failed', () => {
beforeEach(() => {
createComponent({
- mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] },
+ mr: { headPipeline: { status: 'FAILED' }, availableAutoMergeStrategies: [], hasCI: true },
});
});
@@ -830,7 +850,7 @@ describe('ReadyToMerge', () => {
const USER_COMMIT_MESSAGE = 'Merge message provided manually by user';
const createDefaultGqlComponent = () =>
- createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, true);
+ createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, false);
beforeEach(() => {
readyToMergeResponseSpy = jest
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
index 7259f210b6e..82aeac1a47d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
@@ -1,101 +1,42 @@
-import Vue, { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
+import { mount } from '@vue/test-utils';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
-import toast from '~/vue_shared/plugins/global_toast';
-import eventHub from '~/vue_merge_request_widget/event_hub';
-jest.mock('~/vue_shared/plugins/global_toast');
-
-const createComponent = () => {
- const Component = Vue.extend(WorkInProgress);
- const mr = {
- title: 'The best MR ever',
- removeWIPPath: '/path/to/remove/wip',
- };
- const service = {
- removeWIP() {},
- };
- return new Component({
- el: document.createElement('div'),
- propsData: { mr, service },
+let wrapper;
+
+const createComponent = (updateMergeRequest = true) => {
+ wrapper = mount(WorkInProgress, {
+ propsData: {
+ mr: {},
+ },
+ data() {
+ return {
+ userPermissions: {
+ updateMergeRequest,
+ },
+ };
+ },
});
};
-describe('Wip', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr, service } = WorkInProgress.props;
-
- expect(mr.type instanceof Object).toBe(true);
- expect(mr.required).toBe(true);
-
- expect(service.type instanceof Object).toBe(true);
- expect(service.required).toBe(true);
- });
- });
-
- describe('data', () => {
- it('should have default data', () => {
- const vm = createComponent();
-
- expect(vm.isMakingRequest).toBe(false);
- });
- });
-
- describe('methods', () => {
- const mrObj = {
- is_new_mr_data: true,
- };
-
- describe('handleRemoveDraft', () => {
- it('should make a request to service and handle response', async () => {
- const vm = createComponent();
-
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(vm.service, 'removeWIP').mockReturnValue(
- new Promise((resolve) => {
- resolve({
- data: mrObj,
- });
- }),
- );
-
- vm.handleRemoveDraft();
-
- await waitForPromises();
-
- expect(vm.isMakingRequest).toBe(true);
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
- expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.');
- });
- });
+describe('Merge request widget draft state component', () => {
+ afterEach(() => {
+ wrapper.destroy();
});
describe('template', () => {
- let vm;
- let el;
-
- beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
- });
-
it('should have correct elements', () => {
- expect(el.classList.contains('mr-widget-body')).toBe(true);
- expect(el.innerText).toContain(
+ createComponent(true);
+
+ expect(wrapper.text()).toContain(
"Merge blocked: merge request must be marked as ready. It's still marked as draft.",
);
- expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain(
- 'Mark as ready',
- );
+ expect(wrapper.find('[data-testid="removeWipButton"]').text()).toContain('Mark as ready');
});
- it('should not show removeWIP button is user cannot update MR', async () => {
- vm.mr.removeWIPPath = '';
-
- await nextTick();
+ it('should not show removeWIP button is user cannot update MR', () => {
+ createComponent(false);
- expect(el.querySelector('.js-remove-draft')).toBeNull();
+ expect(wrapper.find('[data-testid="removeWipButton"]').exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
index 08424077269..e9a34453930 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue renders given data 1`] = `
-"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\">
+"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\" helppopover=\\"[object Object]\\" actionbuttons=\\"\\">
<div class=\\"gl-display-flex gl-flex-direction-column\\">
<div>
<p class=\\"gl-mb-0\\">Main text for the row</p>
<gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
<!---->
- <gl-badge-stub size=\\"md\\" variant=\\"info\\">
+ <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
Badge is optional. Text to be displayed inside badge
</gl-badge-stub>
<actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
@@ -15,7 +15,7 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
</div>
<ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\">
<li>
- <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" data-qa-selector=\\"child_content\\">
+ <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" actionbuttons=\\"\\" data-qa-selector=\\"child_content\\">
<div class=\\"gl-display-flex gl-flex-direction-column\\">
<div>
<p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
index b7753a58747..527e800ddcf 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -25,6 +25,10 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
header: ['This is a header', 'This is a subheader'],
text: 'Main text for the row',
subtext: 'Optional: Smaller sub-text to be displayed below the main text',
+ helpPopover: {
+ options: { title: 'Widget help popover title' },
+ content: { text: 'Widget help popover content' },
+ },
icon: {
name: EXTENSION_ICONS.success,
},
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
index 9eddd091ad0..e4bee6b8652 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_row_spec.js
@@ -1,11 +1,15 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue';
+import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue', () => {
let wrapper;
const findStatusIcon = () => wrapper.findComponent(StatusIcon);
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
+ const findActionButtons = () => wrapper.findComponent(ActionButtons);
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(WidgetContentRow, {
@@ -61,5 +65,38 @@ describe('~/vue_merge_request_widget/components/widget/widget_content_row.vue',
createComponent({ propsData: { header: '<b role="header">this is a header</b>' } });
expect(wrapper.findByText('<b role="header">this is a header</b>').exists()).toBe(true);
});
+
+ it('renders a help popover', () => {
+ createComponent({
+ propsData: {
+ helpPopover: {
+ options: { title: 'Help popover title' },
+ content: { text: 'Help popover content', learnMorePath: '/path/to/docs' },
+ },
+ },
+ });
+
+ expect(findHelpPopover().props('options')).toEqual({ title: 'Help popover title' });
+ expect(wrapper.findByText('Help popover content').exists()).toBe(true);
+ expect(wrapper.findByText('Learn more').attributes('href')).toBe('/path/to/docs');
+ expect(wrapper.findByText('Learn more').attributes('target')).toBe('_blank');
+ });
+
+ it('does not render help popover when it is not provided', () => {
+ createComponent({});
+ expect(findHelpPopover().exists()).toBe(false);
+ });
+
+ it('does not display action buttons if actionButtons is not provided', () => {
+ createComponent({});
+ expect(findActionButtons().exists()).toBe(false);
+ });
+
+ it('does display action buttons if actionButtons is provided', () => {
+ const actionButtons = [{ text: 'click-me', href: '#' }];
+
+ createComponent({ propsData: { actionButtons } });
+ expect(findActionButtons().props('tertiaryButtons')).toEqual(actionButtons);
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 4826fecf98d..9635e050e4d 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -1,12 +1,21 @@
import { nextTick } from 'vue';
import * as Sentry from '@sentry/browser';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+jest.mock('~/vue_merge_request_widget/components/extensions/telemetry', () => ({
+ createTelemetryHub: jest.fn().mockReturnValue({
+ viewed: jest.fn(),
+ expanded: jest.fn(),
+ fullReportClicked: jest.fn(),
+ }),
+}));
+
describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
let wrapper;
@@ -14,13 +23,15 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const findExpandedSection = () => wrapper.findByTestId('widget-extension-collapsed-section');
const findActionButtons = () => wrapper.findComponent(ActionButtons);
const findToggleButton = () => wrapper.findByTestId('toggle-button');
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
const createComponent = ({ propsData, slots } = {}) => {
wrapper = shallowMountExtended(Widget, {
propsData: {
isCollapsible: false,
loadingText: 'Loading widget',
- widgetName: 'MyWidget',
+ widgetName: 'WidgetTest',
+ fetchCollapsedData: () => Promise.resolve([]),
value: {
collapsed: null,
expanded: null,
@@ -30,6 +41,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
slots,
stubs: {
StatusIcon,
+ ActionButtons,
ContentRow: WidgetContentRow,
},
});
@@ -52,8 +64,9 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
it('sets the error text when fetch method fails', async () => {
- const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
- createComponent({ propsData: { fetchCollapsedData } });
+ createComponent({
+ propsData: { fetchCollapsedData: jest.fn().mockRejectedValue('Something went wrong') },
+ });
await waitForPromises();
expect(wrapper.findByText('Failed to load').exists()).toBe(true);
expect(findStatusIcon().props()).toMatchObject({ iconName: 'failed', isLoading: false });
@@ -79,12 +92,24 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
it('displays the loading text', async () => {
- const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject());
- createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
+ createComponent({
+ propsData: {
+ statusIconName: 'warning',
+ },
+ });
+
expect(wrapper.text()).not.toContain('Loading');
await nextTick();
expect(wrapper.text()).toContain('Loading');
});
+
+ it('validates widget name', () => {
+ expect(() => {
+ createComponent({
+ propsData: { widgetName: 'InvalidWidgetName' },
+ });
+ }).toThrow();
+ });
});
describe('fetch', () => {
@@ -136,7 +161,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
summary: 'Hello world',
- fetchCollapsedData: () => Promise.resolve(),
},
});
@@ -145,28 +169,22 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it.todo('displays content property when content slot is not provided');
- it('displays the summary slot when provided', () => {
+ it('displays the summary slot when provided', async () => {
createComponent({
- propsData: {
- fetchCollapsedData: () => Promise.resolve(),
- },
slots: {
summary: '<b>More complex summary</b>',
},
});
+ await waitForPromises();
+
expect(wrapper.findByTestId('widget-extension-top-level-summary').text()).toBe(
'More complex summary',
);
});
it('does not display action buttons if actionButtons is not provided', () => {
- createComponent({
- propsData: {
- fetchCollapsedData: () => Promise.resolve(),
- },
- });
-
+ createComponent();
expect(findActionButtons().exists()).toBe(false);
});
@@ -175,7 +193,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
- fetchCollapsedData: () => Promise.resolve(),
actionButtons,
},
});
@@ -184,12 +201,34 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
});
+ describe('help popover', () => {
+ it('renders a help popover', () => {
+ createComponent({
+ propsData: {
+ helpPopover: {
+ options: { title: 'My help popover title' },
+ content: { text: 'Help popover content', learnMorePath: '/path/to/docs' },
+ },
+ },
+ });
+
+ expect(findHelpPopover().props('options')).toEqual({ title: 'My help popover title' });
+ expect(wrapper.findByText('Help popover content').exists()).toBe(true);
+ expect(wrapper.findByText('Learn more').attributes('href')).toBe('/path/to/docs');
+ expect(wrapper.findByText('Learn more').attributes('target')).toBe('_blank');
+ });
+
+ it('does not render help popover when it is not provided', () => {
+ createComponent();
+ expect(findHelpPopover().exists()).toBe(false);
+ });
+ });
+
describe('handle collapse toggle', () => {
it('displays the toggle button correctly', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve(),
},
slots: {
content: '<b>More complex content</b>',
@@ -204,7 +243,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve(),
},
slots: {
content: '<b>More complex content</b>',
@@ -221,7 +259,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: false,
- fetchCollapsedData: () => Promise.resolve(),
},
});
@@ -278,7 +315,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve([]),
fetchExpandedData,
},
});
@@ -306,7 +342,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
createComponent({
propsData: {
isCollapsible: true,
- fetchCollapsedData: () => Promise.resolve([]),
fetchExpandedData,
},
});
@@ -323,4 +358,54 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(wrapper.findByText('Failed to load').exists()).toBe(false);
});
});
+
+ describe('telemetry - enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ actionButtons: [
+ {
+ fullReport: true,
+ href: '#',
+ target: '_blank',
+ id: 'full-report-button',
+ text: 'Full Report',
+ },
+ ],
+ },
+ });
+ });
+
+ it('should call create a telemetry hub', () => {
+ expect(wrapper.vm.telemetryHub).not.toBe(null);
+ });
+
+ it('should call the viewed state', async () => {
+ await nextTick();
+ expect(wrapper.vm.telemetryHub.viewed).toHaveBeenCalledTimes(1);
+ });
+
+ it('when full report is clicked it should call the respective telemetry event', async () => {
+ expect(wrapper.vm.telemetryHub.fullReportClicked).not.toHaveBeenCalled();
+ wrapper.findByText('Full Report').vm.$emit('click');
+ await nextTick();
+ expect(wrapper.vm.telemetryHub.fullReportClicked).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('telemetry - disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ telemetry: false,
+ },
+ });
+ });
+
+ it('should not call create a telemetry hub', () => {
+ expect(wrapper.vm.telemetryHub).toBe(null);
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
index 58dadb2c679..41df485b0de 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
@@ -23,11 +23,7 @@ import {
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('DeploymentAction component', () => {
let wrapper;
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js
new file mode 100644
index 00000000000..c6b73f63301
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_info_spec.js
@@ -0,0 +1,42 @@
+import { mount } from '@vue/test-utils';
+import { GlTruncate, GlLink } from '@gitlab/ui';
+import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
+import { deploymentMockData } from './deployment_mock_data';
+
+// This component is well covered in ./deployment_spec.js
+// more component-specific tests are added below
+describe('Deployment Info component', () => {
+ let wrapper;
+
+ const defaultDeploymentInfoOptions = {
+ computedDeploymentStatus: 'computed deployment status',
+ deployment: deploymentMockData,
+ showMetrics: false,
+ };
+
+ const factory = (options = {}) => {
+ const componentProps = { ...defaultDeploymentInfoOptions, ...options };
+ const componentOptions = { propsData: componentProps };
+ wrapper = mount(DeploymentInfo, componentOptions);
+ };
+
+ beforeEach(() => {
+ factory();
+ });
+
+ it('should render gl-truncate for environment name', () => {
+ const envNameComponent = wrapper.findComponent(GlTruncate);
+ expect(envNameComponent.exists()).toBe(true, 'We should use gl-truncate for environment name');
+ expect(envNameComponent.props()).toEqual({
+ text: deploymentMockData.name,
+ withTooltip: true,
+ position: 'middle',
+ });
+ });
+
+ it('should have a link with a correct href to deployed environment', () => {
+ const envLink = wrapper.findComponent(GlLink);
+ expect(envLink.exists()).toBe(true, 'We should have gl-link pointing to deployed environment');
+ expect(envLink.attributes().href).toBe(deploymentMockData.url);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 6622749da92..0f4637d18d9 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -4,6 +4,8 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
+import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
+import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
@@ -22,6 +24,10 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
+import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
+import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
+import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql';
+import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import mockData from './mock_data';
import {
@@ -83,7 +89,39 @@ describe('MrWidgetOptions', () => {
propsData: {
mrData: { ...mrData },
},
+ data() {
+ return { loading: false };
+ },
+
...options,
+ apolloProvider: createMockApollo([
+ [
+ getStateQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ ...getStateQueryResponse.data.project,
+ mergeRequest: {
+ ...getStateQueryResponse.data.project.mergeRequest,
+ mergeError: mrData.mergeError || null,
+ },
+ },
+ },
+ }),
+ ],
+ [readyToMergeQuery, jest.fn().mockResolvedValue(readyToMergeResponse)],
+ [
+ userPermissionsQuery,
+ jest.fn().mockResolvedValue({
+ data: { project: { mergeRequest: { userPermissions: {} } } },
+ }),
+ ],
+ [
+ conflictsStateQuery,
+ jest.fn().mockResolvedValue({ data: { project: { mergeRequest: {} } } }),
+ ],
+ ...(options.apolloMock || []),
+ ]),
});
return axios.waitForAll();
@@ -563,21 +601,6 @@ describe('MrWidgetOptions', () => {
});
});
- describe('code quality widget', () => {
- beforeEach(() => {
- jest.spyOn(document, 'dispatchEvent');
- });
- it('renders the component when refactorCodeQualityExtension is false', () => {
- createComponent(mockData, {}, { refactorCodeQualityExtension: false });
- expect(wrapper.find('.js-codequality-widget').exists()).toBe(true);
- });
-
- it('does not render the component when refactorCodeQualityExtension is true', () => {
- createComponent(mockData, {}, { refactorCodeQualityExtension: true });
- expect(wrapper.find('.js-codequality-widget').exists()).toBe(true);
- });
- });
-
describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => {
beforeEach(() => {
@@ -784,12 +807,12 @@ describe('MrWidgetOptions', () => {
mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
return createComponent(mrData, {
- apolloProvider: createMockApollo([
+ apolloMock: [
[
securityReportMergeRequestDownloadPathsQuery,
async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
],
- ]),
+ ],
});
};
@@ -852,8 +875,10 @@ describe('MrWidgetOptions', () => {
${'closed'} | ${false} | ${'hides'}
${'merged'} | ${true} | ${'shows'}
${'open'} | ${true} | ${'shows'}
- `('$showText merge error when state is $state', ({ state, show }) => {
- createComponent({ ...mockData, state, merge_error: 'Error!' });
+ `('$showText merge error when state is $state', async ({ state, show }) => {
+ createComponent({ ...mockData, state, mergeError: 'Error!' });
+
+ await waitForPromises();
expect(wrapper.find('[data-testid="merge_error"]').exists()).toBe(show);
});
@@ -917,8 +942,7 @@ describe('MrWidgetOptions', () => {
});
it('extension polling is not called if enablePolling flag is not passed', () => {
- // called one time due to parent component polling (mount)
- expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollRequest).toHaveBeenCalledTimes(0);
});
});
@@ -1004,7 +1028,7 @@ describe('MrWidgetOptions', () => {
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollRequest).toHaveBeenCalledTimes(1);
});
});
@@ -1042,7 +1066,7 @@ describe('MrWidgetOptions', () => {
registerExtension(pollingErrorExtension);
await createComponent();
- expect(pollRequest).toHaveBeenCalledTimes(2);
+ expect(pollRequest).toHaveBeenCalledTimes(1);
});
it('captures sentry error and displays error when poll has failed', async () => {
@@ -1085,7 +1109,7 @@ describe('MrWidgetOptions', () => {
await nextTick();
await waitForPromises();
- expect(Sentry.captureException).toHaveBeenCalledTimes(1);
+ expect(Sentry.captureException).toHaveBeenCalledTimes(2);
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error'));
expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
});
diff --git a/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
index 3cdb4265ef0..37df041210c 100644
--- a/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/mr_widget_store_spec.js
@@ -21,22 +21,9 @@ describe('MergeRequestStore', () => {
});
describe('setData', () => {
- it('should set isSHAMismatch when the diff SHA changes', () => {
- store.setData({ ...mockData, diff_head_sha: 'a-different-string' });
-
- expect(store.isSHAMismatch).toBe(true);
- });
-
- it('should not set isSHAMismatch when other data changes', () => {
- store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
-
- expect(store.isSHAMismatch).toBe(false);
- });
-
it('should update cached sha after rebasing', () => {
store.setData({ ...mockData, diff_head_sha: 'abc123' }, true);
- expect(store.isSHAMismatch).toBe(false);
expect(store.sha).toBe('abc123');
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index d309432bc63..3bc191d988f 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -30,7 +30,7 @@ describe('AlertDetails', () => {
const projectPath = 'root/alerts';
const projectIssuesPath = 'root/alerts/-/issues';
const projectId = '1';
- const $router = { replace: jest.fn() };
+ const $router = { push: jest.fn() };
function mountComponent({
data,
@@ -352,7 +352,7 @@ describe('AlertDetails', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentTabIndex: index });
- expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
+ expect($router.push).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
});
});
});
diff --git a/spec/frontend/vue_shared/alert_details/router_spec.js b/spec/frontend/vue_shared/alert_details/router_spec.js
new file mode 100644
index 00000000000..e3efc104862
--- /dev/null
+++ b/spec/frontend/vue_shared/alert_details/router_spec.js
@@ -0,0 +1,35 @@
+import createRouter from '~/vue_shared/alert_details/router';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const BASE_PATH = '/-/alert_management/1/details';
+const EMPTY_HASH = '';
+const NOOP = () => {};
+
+describe('AlertDetails router', () => {
+ const originalLocation = window.location.href;
+ let router;
+
+ beforeEach(() => {
+ setWindowLocation(originalLocation);
+ router = createRouter(BASE_PATH);
+ });
+
+ describe('redirects hash route mode URLs to history route mode', () => {
+ it.each`
+ hashPath | historyPath
+ ${'/#/overview'} | ${'/overview'}
+ ${'#/overview'} | ${'/overview'}
+ ${'/#/'} | ${'/'}
+ ${'#/'} | ${'/'}
+ ${'/#'} | ${'/'}
+ ${'#'} | ${'/'}
+ ${'/'} | ${'/'}
+ ${'/overview'} | ${'/overview'}
+ `('should redirect "$hashPath" to "$historyPath"', ({ hashPath, historyPath }) => {
+ router.push(hashPath, NOOP);
+
+ expect(window.location.hash).toBe(EMPTY_HASH);
+ expect(window.location.pathname).toBe(BASE_PATH + historyPath);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index f5a545891d5..c3a71d7fda3 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -106,7 +106,7 @@ describe('File row component', () => {
level: 2,
});
- expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('32px');
+ expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('16px');
});
it('renders header for file', () => {
diff --git a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js b/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
deleted file mode 100644
index 38f28837cc1..00000000000
--- a/spec/frontend/vue_shared/components/gitlab_version_check_spec.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { GlBadge } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { mockTracking } from 'helpers/tracking_helper';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import axios from '~/lib/utils/axios_utils';
-import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
-
-describe('GitlabVersionCheck', () => {
- let wrapper;
- let mock;
-
- const UPGRADE_DOCS_URL = helpPagePath('update/index');
-
- const defaultResponse = {
- code: 200,
- res: { severity: 'success' },
- };
-
- const createComponent = (mockResponse) => {
- const response = {
- ...defaultResponse,
- ...mockResponse,
- };
-
- mock = new MockAdapter(axios);
- mock.onGet().replyOnce(response.code, response.res);
-
- wrapper = shallowMountExtended(GitlabVersionCheck);
- };
-
- const dummyGon = {
- relative_url_root: '/',
- };
-
- let originalGon;
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- window.gon = originalGon;
- });
-
- const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
- const findGlBadge = () => wrapper.findComponent(GlBadge);
-
- describe.each`
- root | description
- ${'/'} | ${'not used (uses its own (sub)domain)'}
- ${'/gitlab'} | ${'custom path'}
- ${'/service/gitlab'} | ${'custom path with 2 depth'}
- `('path for version_check.json', ({ root, description }) => {
- describe(`when relative url is ${description}: ${root}`, () => {
- beforeEach(async () => {
- originalGon = window.gon;
- window.gon = { ...dummyGon };
- window.gon.relative_url_root = root;
- createComponent(defaultResponse);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it('reflects the relative url setting', () => {
- expect(mock.history.get.length).toBe(1);
-
- const pathRegex = new RegExp(`^${root}`);
- expect(mock.history.get[0].url).toMatch(pathRegex);
- });
- });
- });
-
- describe('template', () => {
- describe.each`
- description | mockResponse | renders
- ${'successful but null'} | ${{ code: 200, res: null }} | ${false}
- ${'successful and valid'} | ${{ code: 200, res: { severity: 'success' } }} | ${true}
- ${'an error'} | ${{ code: 500, res: null }} | ${false}
- `('version_check.json response', ({ description, mockResponse, renders }) => {
- describe(`is ${description}`, () => {
- beforeEach(async () => {
- createComponent(mockResponse);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it(`does${renders ? '' : ' not'} render Badge Click Wrapper and GlBadge`, () => {
- expect(findGlBadgeClickWrapper().exists()).toBe(renders);
- expect(findGlBadge().exists()).toBe(renders);
- });
- });
- });
-
- describe.each`
- mockResponse | expectedUI
- ${{ code: 200, res: { severity: 'success' } }} | ${{ title: 'Up to date', variant: 'success' }}
- ${{ code: 200, res: { severity: 'warning' } }} | ${{ title: 'Update available', variant: 'warning' }}
- ${{ code: 200, res: { severity: 'danger' } }} | ${{ title: 'Update ASAP', variant: 'danger' }}
- `('badge ui', ({ mockResponse, expectedUI }) => {
- describe(`when response is ${mockResponse.res.severity}`, () => {
- let trackingSpy;
-
- beforeEach(async () => {
- createComponent(mockResponse);
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await waitForPromises(); // Ensure we wrap up the axios call
- });
-
- it(`title is ${expectedUI.title}`, () => {
- expect(findGlBadge().text()).toBe(expectedUI.title);
- });
-
- it(`variant is ${expectedUI.variant}`, () => {
- expect(findGlBadge().attributes('variant')).toBe(expectedUI.variant);
- });
-
- it(`tracks rendered_version_badge with label ${expectedUI.title}`, () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'rendered_version_badge', {
- label: expectedUI.title,
- });
- });
-
- it(`link is ${UPGRADE_DOCS_URL}`, () => {
- expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL);
- });
-
- it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => {
- await findGlBadgeClickWrapper().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_version_badge', {
- label: expectedUI.title,
- });
- });
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/group_select/group_select_spec.js b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
new file mode 100644
index 00000000000..f959d2225fa
--- /dev/null
+++ b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
@@ -0,0 +1,202 @@
+import { nextTick } from 'vue';
+import { GlListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import { createAlert } from '~/flash';
+import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
+import {
+ TOGGLE_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+ QUERY_TOO_SHORT_MESSAGE,
+} from '~/vue_shared/components/group_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/flash');
+
+describe('GroupSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Mocks
+ const groupMock = {
+ full_name: 'selectedGroup',
+ id: '1',
+ };
+ const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
+
+ // Props
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlListbox);
+ const findInput = () => wrapper.findByTestId('input');
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupSelect, {
+ propsData: {
+ inputName,
+ inputId,
+ ...props,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+ const search = (searchString) => findListbox().vm.$emit('search', searchString);
+ const createComponentWithGroups = () => {
+ mock.onGet('/api/undefined/groups.json').reply(200, [groupMock]);
+ createComponent();
+ openListbox();
+ return waitForPromises();
+ };
+ const selectGroup = () => {
+ findListbox().vm.$emit('select', groupMock.id);
+ return nextTick();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on mount', () => {
+ it('fetches groups when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ });
+
+ describe('with an initial selection', () => {
+ it('if the selected group is not part of the fetched list, fetches it individually', async () => {
+ mock.onGet(groupEndpoint).reply(200, groupMock);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it('show an error if fetching the individual group fails', async () => {
+ mock
+ .onGet('/api/undefined/groups.json')
+ .reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
+ mock.onGet(groupEndpoint).reply(500);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FETCH_GROUP_ERROR,
+ error: expect.any(Error),
+ parent: wrapper.vm.$el,
+ });
+ });
+ });
+ });
+
+ it('shows an error when fetching groups fails', async () => {
+ mock.onGet('/api/undefined/groups.json').reply(500);
+ createComponent();
+ openListbox();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FETCH_GROUPS_ERROR,
+ error: expect.any(Error),
+ parent: wrapper.vm.$el,
+ });
+ });
+
+ describe('selection', () => {
+ it('uses the default toggle text while no group is selected', async () => {
+ await createComponentWithGroups();
+
+ expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
+ });
+
+ describe('once a group is selected', () => {
+ it(`uses the selected group's name as the toggle text`, async () => {
+ await createComponentWithGroups();
+ await selectGroup();
+
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it(`uses the selected group's ID as the listbox' and input value`, async () => {
+ await createComponentWithGroups();
+ await selectGroup();
+
+ expect(findListbox().attributes('selected')).toBe(groupMock.id);
+ expect(findInput().attributes('value')).toBe(groupMock.id);
+ });
+
+ it(`on reset, falls back to the default toggle text`, async () => {
+ await createComponentWithGroups();
+ await selectGroup();
+
+ findListbox().vm.$emit('reset');
+ await nextTick();
+
+ expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
+ });
+ });
+ });
+
+ describe('search', () => {
+ it('sets `searching` to `true` when first opening the dropdown', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ openListbox();
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('sets `searching` to `true` while searching', async () => {
+ await createComponentWithGroups();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ search('foo');
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('fetches groups matching the search string', async () => {
+ const searchString = 'searchString';
+ await createComponentWithGroups();
+
+ expect(mock.history.get).toHaveLength(1);
+
+ search(searchString);
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(2);
+ expect(mock.history.get[1].params).toStrictEqual({ search: searchString });
+ });
+
+ it('shows a notice if the search query is too short', async () => {
+ const searchString = 'a';
+ await createComponentWithGroups();
+ search(searchString);
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 6fd5ae0e946..77c03dc0c3c 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -96,6 +96,20 @@ describe('HelpPopover', () => {
});
});
+ describe('with alternative icon', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ icon: 'information-o',
+ },
+ });
+ });
+
+ it('uses the given icon', () => {
+ expect(findQuestionButton().props('icon')).toBe('information-o');
+ });
+ });
+
describe('with custom slots', () => {
const titleSlot = '<h1>title</h1>';
const defaultSlot = '<strong>content</strong>';
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index f7e93f45148..625e67c7cc1 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -27,7 +27,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const formFieldAriaLabel = 'Edit your content';
let mock;
- const buildWrapper = ({ propsData = {}, attachTo } = {}) => {
+ const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
wrapper = mountExtended(MarkdownEditor, {
attachTo,
propsData: {
@@ -45,6 +45,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
},
stubs: {
BubbleMenu: stubComponent(BubbleMenu),
+ ...stubs,
},
});
};
@@ -138,9 +139,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('input')).toEqual([[newValue]]);
});
- describe('when initOnAutofocus is true', () => {
+ describe('when autofocus is true', () => {
beforeEach(async () => {
- buildWrapper({ attachTo: document.body, propsData: { initOnAutofocus: true } });
+ buildWrapper({ attachTo: document.body, propsData: { autofocus: true } });
await nextTick();
});
@@ -171,7 +172,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
markdown: value,
- autofocus: 'end',
}),
);
});
@@ -204,10 +204,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
});
- describe('when initOnAutofocus is true', () => {
+ describe('when autofocus is true', () => {
beforeEach(() => {
- buildWrapper({ propsData: { initOnAutofocus: true } });
- findLocalStorageSync().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ buildWrapper({
+ propsData: { autofocus: true },
+ stubs: { ContentEditor: stubComponent(ContentEditor) },
+ });
});
it('sets the content editor autofocus property to end', () => {
@@ -247,19 +249,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('updates localStorage value', () => {
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
});
-
- it('sets the textarea as the activeElement in the document', async () => {
- // The component should be rebuilt to attach it to the document body
- buildWrapper({ attachTo: document.body });
- await findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
-
- expect(findContentEditor().exists()).toBe(true);
-
- await findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
- await findSegmentedControl().vm.$emit('change', EDITING_MODE_MARKDOWN_FIELD);
-
- expect(document.activeElement).toBe(findTextarea().element);
- });
});
describe('when content editor emits loading event', () => {
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
new file mode 100644
index 00000000000..8edcb905096
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -0,0 +1,205 @@
+import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MarkdownDrawer, { cache } from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
+import { getRenderedMarkdown } from '~/vue_shared/components/markdown_drawer/utils/fetch';
+import { contentTop } from '~/lib/utils/common_utils';
+
+jest.mock('~/vue_shared/components/markdown_drawer/utils/fetch', () => ({
+ getRenderedMarkdown: jest.fn().mockReturnValue({
+ title: 'test title test',
+ body: `<div id="content-body">
+ <div class="documentation md gl-mt-3">
+ test body
+ </div>
+ </div>`,
+ }),
+}));
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ contentTop: jest.fn(),
+}));
+
+describe('MarkdownDrawer', () => {
+ let wrapper;
+ const defaultProps = {
+ documentPath: 'user/search/global_search/advanced_search_syntax.json',
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(MarkdownDrawer, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ Object.keys(cache).forEach((key) => delete cache[key]);
+ });
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
+ const findDrawerTitle = () => wrapper.findComponent('[data-testid="title-element"]');
+ const findDrawerBody = () => wrapper.findComponent({ ref: 'content-element' });
+
+ describe('component', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders correctly', () => {
+ expect(findDrawer().exists()).toBe(true);
+ expect(findDrawerTitle().text()).toBe('test title test');
+ expect(findDrawerBody().text()).toBe('test body');
+ });
+ });
+
+ describe.each`
+ hasNavbar | navbarHeight
+ ${false} | ${0}
+ ${true} | ${100}
+ `('computes offsetTop', ({ hasNavbar, navbarHeight }) => {
+ beforeEach(() => {
+ global.document.querySelector = jest.fn(() =>
+ hasNavbar
+ ? {
+ dataset: {
+ page: 'test',
+ },
+ }
+ : undefined,
+ );
+ contentTop.mockReturnValue(navbarHeight);
+ createComponent();
+ });
+
+ afterEach(() => {
+ contentTop.mockClear();
+ });
+
+ it(`computes offsetTop ${hasNavbar ? 'with' : 'without'} .navbar-gitlab`, () => {
+ expect(findDrawer().attributes('headerheight')).toBe(`${navbarHeight}px`);
+ });
+ });
+
+ describe('watcher', () => {
+ let renderGLFMSpy;
+ let fetchMarkdownSpy;
+
+ beforeEach(async () => {
+ renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
+ fetchMarkdownSpy = jest.spyOn(MarkdownDrawer.methods, 'fetchMarkdown');
+ global.document.querySelector = jest.fn(() => ({
+ getBoundingClientRect: jest.fn(() => ({ bottom: 100 })),
+ dataset: {
+ page: 'test',
+ },
+ }));
+ createComponent();
+ await nextTick();
+ });
+
+ afterEach(() => {
+ renderGLFMSpy.mockClear();
+ fetchMarkdownSpy.mockClear();
+ });
+
+ it('for documentPath triggers fetch', async () => {
+ expect(fetchMarkdownSpy).toHaveBeenCalledTimes(1);
+
+ await wrapper.setProps({ documentPath: '/test/me' });
+ await nextTick();
+
+ expect(fetchMarkdownSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('for open triggers renderGLFM', async () => {
+ wrapper.vm.fetchMarkdown();
+ wrapper.vm.openDrawer();
+ await nextTick();
+ expect(renderGLFMSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('Markdown fetching', () => {
+ let renderGLFMSpy;
+
+ beforeEach(async () => {
+ renderGLFMSpy = jest.spyOn(MarkdownDrawer.methods, 'renderGLFM');
+ createComponent();
+ await nextTick();
+ });
+
+ afterEach(() => {
+ renderGLFMSpy.mockClear();
+ });
+
+ it('fetches the Markdown and caches it', async () => {
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
+ expect(Object.keys(cache)).toHaveLength(1);
+ });
+
+ it('when the document changes, fetches it and caches it as well', async () => {
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
+ expect(Object.keys(cache)).toHaveLength(1);
+
+ await wrapper.setProps({ documentPath: '/test/me2' });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+ });
+
+ it('when re-using an already fetched document, gets it from the cache', async () => {
+ await wrapper.setProps({ documentPath: '/test/me2' });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+
+ await wrapper.setProps({ documentPath: defaultProps.documentPath });
+ await nextTick();
+
+ expect(getRenderedMarkdown).toHaveBeenCalledTimes(2);
+ expect(Object.keys(cache)).toHaveLength(2);
+ });
+ });
+
+ describe('Markdown fetching returns error', () => {
+ beforeEach(async () => {
+ getRenderedMarkdown.mockReturnValue({
+ hasFetchError: true,
+ });
+
+ createComponent();
+ await nextTick();
+ });
+ afterEach(() => {
+ getRenderedMarkdown.mockClear();
+ });
+ it('shows alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('While Markdown is fetching', () => {
+ beforeEach(async () => {
+ getRenderedMarkdown.mockReturnValue(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ getRenderedMarkdown.mockClear();
+ });
+
+ it('shows skeleton', async () => {
+ expect(findSkeleton().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js b/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js
new file mode 100644
index 00000000000..53b40407556
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/mock_data.js
@@ -0,0 +1,42 @@
+export const MOCK_HTML = `<!DOCTYPE html>
+<html>
+<body>
+ <div id="content-body">
+ <h1>test title <strong>test</strong></h1>
+ <div class="documentation md gl-mt-3">
+ <a href="../advanced_search.md">Advanced Search</a>
+ <a href="../advanced_search2.md">Advanced Search2</a>
+ <h2>test header h2</h2>
+ <table class="testClass">
+ <tr>
+ <td>Emil</td>
+ <td>Tobias</td>
+ <td>Linus</td>
+ </tr>
+ <tr>
+ <td>16</td>
+ <td>14</td>
+ <td>10</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+</body>
+</html>`.replace(/\n/g, '');
+
+export const MOCK_DRAWER_DATA = {
+ hasFetchError: false,
+ title: 'test title test',
+ body: ` <div id="content-body"> <div class="documentation md gl-mt-3"> <a href="../advanced_search.md">Advanced Search</a> <a href="../advanced_search2.md">Advanced Search2</a> <h2>test header h2</h2> <table class="testClass"> <tbody><tr> <td>Emil</td> <td>Tobias</td> <td>Linus</td> </tr> <tr> <td>16</td> <td>14</td> <td>10</td> </tr> </tbody></table> </div> </div>`,
+};
+
+export const MOCK_DRAWER_DATA_ERROR = {
+ hasFetchError: true,
+};
+
+export const MOCK_TABLE_DATA_BEFORE = `<head></head><body><h1>test</h1></test><table><tbody><tr><td></td></tr></tbody></table></body>`;
+
+export const MOCK_HTML_DATA_AFTER = {
+ body: '<table><tbody><tr><td></td></tr></tbody></table>',
+ title: 'test',
+};
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
new file mode 100644
index 00000000000..ff07b2cf838
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
@@ -0,0 +1,43 @@
+import MockAdapter from 'axios-mock-adapter';
+import {
+ getRenderedMarkdown,
+ splitDocument,
+} from '~/vue_shared/components/markdown_drawer/utils/fetch';
+import axios from '~/lib/utils/axios_utils';
+import {
+ MOCK_HTML,
+ MOCK_DRAWER_DATA,
+ MOCK_DRAWER_DATA_ERROR,
+ MOCK_TABLE_DATA_BEFORE,
+ MOCK_HTML_DATA_AFTER,
+} from '../mock_data';
+
+describe('utils/fetch', () => {
+ let mock;
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe.each`
+ axiosMock | type | toExpect
+ ${{ code: 200, res: { html: MOCK_HTML } }} | ${'success'} | ${MOCK_DRAWER_DATA}
+ ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
+ `('process markdown data', ({ axiosMock, type, toExpect }) => {
+ describe(`if api fetch responds with ${type}`, () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet().reply(axiosMock.code, axiosMock.res);
+ });
+ it(`should update drawer correctly`, async () => {
+ expect(await getRenderedMarkdown('/any/path')).toStrictEqual(toExpect);
+ });
+ });
+ });
+
+ describe('splitDocument', () => {
+ it(`should update tables correctly`, () => {
+ expect(splitDocument(MOCK_TABLE_DATA_BEFORE)).toStrictEqual(MOCK_HTML_DATA_AFTER);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/namespace_select/mock_data.js b/spec/frontend/vue_shared/components/namespace_select/mock_data.js
deleted file mode 100644
index cfd521c67cb..00000000000
--- a/spec/frontend/vue_shared/components/namespace_select/mock_data.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export const groupNamespaces = [
- { id: 1, name: 'Group 1', humanName: 'Group 1' },
- { id: 2, name: 'Subgroup 1', humanName: 'Group 1 / Subgroup 1' },
-];
-
-export const userNamespaces = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
deleted file mode 100644
index d930ef63dad..00000000000
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_deprecated_spec.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import { nextTick } from 'vue';
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlIntersectionObserver,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import NamespaceSelect, {
- i18n,
- EMPTY_NAMESPACE_ID,
-} from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
-import { userNamespaces, groupNamespaces } from './mock_data';
-
-const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
-const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
-const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
-
-describe('NamespaceSelectDeprecated', () => {
- let wrapper;
-
- const createComponent = (props = {}) =>
- shallowMountExtended(NamespaceSelect, {
- propsData: {
- userNamespaces,
- groupNamespaces,
- ...props,
- },
- stubs: {
- // We have to "full" mount GlDropdown so that slot children will render
- GlDropdown,
- },
- });
-
- const wrappersText = (arr) => arr.wrappers.map((w) => w.text());
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownText = () => findDropdown().props('text');
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findGroupDropdownItems = () =>
- wrapper.findByTestId('namespace-list-groups').findAllComponents(GlDropdownItem);
- const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
- const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const search = (term) => findSearchBox().vm.$emit('input', term);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('default', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
- it('renders the dropdown', () => {
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('renders each dropdown item', () => {
- expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName));
- });
-
- it('renders default dropdown text', () => {
- expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT);
- });
-
- it('splits group and user namespaces', () => {
- const headers = findSectionHeaders();
- expect(wrappersText(headers)).toEqual([i18n.USERS, i18n.GROUPS]);
- });
-
- it('does not render wrapper as full width', () => {
- expect(findDropdown().attributes('block')).toBeUndefined();
- });
- });
-
- it('with defaultText, it overrides dropdown text', () => {
- const textOverride = 'Select an option';
-
- wrapper = createComponent({ defaultText: textOverride });
-
- expect(findDropdownText()).toBe(textOverride);
- });
-
- it('with includeHeaders=false, hides group/user headers', () => {
- wrapper = createComponent({ includeHeaders: false });
-
- expect(findSectionHeaders()).toHaveLength(0);
- });
-
- it('with fullWidth=true, sets the dropdown to full width', () => {
- wrapper = createComponent({ fullWidth: true });
-
- expect(findDropdown().attributes('block')).toBe('true');
- });
-
- describe('with search', () => {
- it.each`
- term | includeEmptyNamespace | shouldFilterNamespaces | expectedItems
- ${''} | ${false} | ${true} | ${[...userNamespaces, ...groupNamespaces]}
- ${'sub'} | ${false} | ${true} | ${[groupNamespaces[1]]}
- ${'User'} | ${false} | ${true} | ${[...userNamespaces]}
- ${'User'} | ${true} | ${true} | ${[...userNamespaces]}
- ${'namespace'} | ${true} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
- ${'sub'} | ${false} | ${false} | ${[...userNamespaces, ...groupNamespaces]}
- `(
- 'with term=$term, includeEmptyNamespace=$includeEmptyNamespace, and shouldFilterNamespaces=$shouldFilterNamespaces should show $expectedItems.length',
- async ({ term, includeEmptyNamespace, shouldFilterNamespaces, expectedItems }) => {
- wrapper = createComponent({
- includeEmptyNamespace,
- emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
- shouldFilterNamespaces,
- });
-
- search(term);
-
- await nextTick();
-
- const expected = expectedItems.map((x) => x.humanName);
-
- expect(findDropdownItemsTexts()).toEqual(expected);
- },
- );
- });
-
- describe('when search is typed in', () => {
- it('emits `search` event', async () => {
- wrapper = createComponent();
-
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
-
- await nextTick();
-
- expect(wrapper.emitted('search')).toEqual([['foo']]);
- });
- });
-
- describe('with a selected namespace', () => {
- const selectedGroupIndex = 1;
- const selectedItem = groupNamespaces[selectedGroupIndex];
-
- beforeEach(() => {
- wrapper = createComponent();
-
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
- findGroupDropdownItems().at(selectedGroupIndex).vm.$emit('click');
- });
-
- it('sets the dropdown text', () => {
- expect(findDropdownText()).toBe(selectedItem.humanName);
- });
-
- it('emits the `select` event when a namespace is selected', () => {
- const args = [selectedItem];
- expect(wrapper.emitted('select')).toEqual([args]);
- });
-
- it('clears search', () => {
- expect(wrapper.findComponent(GlSearchBoxByType).props('value')).toBe('');
- });
- });
-
- describe('with an empty namespace option', () => {
- beforeEach(() => {
- wrapper = createComponent({
- includeEmptyNamespace: true,
- emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
- });
- });
-
- it('includes the empty namespace', () => {
- const first = findDropdownItems().at(0);
-
- expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE);
- });
-
- it('emits the `select` event when a namespace is selected', () => {
- findDropdownItems().at(0).vm.$emit('click');
-
- expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
- });
-
- it.each`
- desc | term | shouldShow
- ${'should hide empty option'} | ${'group'} | ${false}
- ${'should show empty option'} | ${'Empty'} | ${true}
- `('when search for $term, $desc', async ({ term, shouldShow }) => {
- search(term);
-
- await nextTick();
-
- expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
- });
- });
-
- describe('when `hasNextPageOfGroups` prop is `true`', () => {
- it('renders `GlIntersectionObserver` and emits `load-more-groups` event when bottom is reached', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true });
-
- const intersectionObserver = wrapper.findComponent(GlIntersectionObserver);
-
- intersectionObserver.vm.$emit('appear');
-
- expect(intersectionObserver.exists()).toBe(true);
- expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
- });
-
- describe('when `isLoading` prop is `true`', () => {
- it('renders a loading icon', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true, isLoading: true });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
- });
-
- describe('when `isSearchLoading` prop is `true`', () => {
- it('sets `isLoading` prop to `true`', () => {
- wrapper = createComponent({ isSearchLoading: true });
-
- expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
- });
- });
-
- describe('when dropdown is opened', () => {
- it('emits `show` event', () => {
- wrapper = createComponent();
-
- findDropdown().vm.$emit('show');
-
- expect(wrapper.emitted('show')).toEqual([[]]);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
index 9b1316677d7..d531147c0e6 100644
--- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js
@@ -37,6 +37,7 @@ const mockProps = {
dropdownButtonTitle: 'Move issuable',
dropdownHeaderTitle: 'Move issuable',
moveInProgress: false,
+ disabled: false,
};
const mockEvent = {
@@ -44,20 +45,21 @@ const mockEvent = {
preventDefault: jest.fn(),
};
-const createComponent = (propsData = mockProps) =>
- shallowMount(IssuableMoveDropdown, {
- propsData,
- });
-
describe('IssuableMoveDropdown', () => {
let mock;
let wrapper;
- beforeEach(() => {
- mock = new MockAdapter(axios);
- wrapper = createComponent();
+ const createComponent = (propsData = mockProps) => {
+ wrapper = shallowMount(IssuableMoveDropdown, {
+ propsData,
+ });
wrapper.vm.$refs.dropdown.hide = jest.fn();
wrapper.vm.$refs.searchInput.focusInput = jest.fn();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent();
});
afterEach(() => {
@@ -194,6 +196,12 @@ describe('IssuableMoveDropdown', () => {
expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true);
});
+ it('renders disabled dropdown when `disabled` is true', () => {
+ createComponent({ ...mockProps, disabled: true });
+
+ expect(findDropdownEl().attributes('disabled')).toBe('true');
+ });
+
it('renders header element', () => {
const headerEl = findDropdownEl().find('[data-testid="header"]');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
index b58c44645d6..74ddd07d041 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/labels_select_root_spec.js
@@ -49,7 +49,6 @@ describe('LabelsSelectRoot', () => {
issuableType = IssuableType.Issue,
queryHandler = successfulQueryHandler,
mutationHandler = successfulMutationHandler,
- isRealtimeEnabled = false,
} = {}) => {
const mockApollo = createMockApollo([
[issueLabelsQuery, queryHandler],
@@ -74,9 +73,6 @@ describe('LabelsSelectRoot', () => {
allowLabelEdit: true,
allowLabelCreate: true,
labelsManagePath: 'test',
- glFeatures: {
- realtimeLabels: isRealtimeEnabled,
- },
},
});
};
@@ -204,17 +200,10 @@ describe('LabelsSelectRoot', () => {
});
});
- it('does not emit `updateSelectedLabels` event when the subscription is triggered and FF is disabled', async () => {
+ it('emits `updateSelectedLabels` event when the subscription is triggered', async () => {
createComponent();
await waitForPromises();
- expect(wrapper.emitted('updateSelectedLabels')).toBeUndefined();
- });
-
- it('emits `updateSelectedLabels` event when the subscription is triggered and FF is enabled', async () => {
- createComponent({ isRealtimeEnabled: true });
- await waitForPromises();
-
expect(wrapper.emitted('updateSelectedLabels')).toEqual([
[
{
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index 8dc3348acfa..d720574ce6d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -2,6 +2,9 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
+import { scrollToElement } from '~/lib/utils/common_utils';
+
+jest.mock('~/lib/utils/common_utils');
const DEFAULT_PROPS = {
chunkIndex: 2,
@@ -13,11 +16,17 @@ const DEFAULT_PROPS = {
blamePath: 'blame/file.js',
};
+const hash = '#L142';
+
describe('Chunk component', () => {
let wrapper;
+ let idleCallbackSpy;
const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(Chunk, { propsData: { ...DEFAULT_PROPS, ...props } });
+ wrapper = shallowMountExtended(Chunk, {
+ mocks: { $route: { hash } },
+ propsData: { ...DEFAULT_PROPS, ...props },
+ });
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
@@ -26,6 +35,7 @@ describe('Chunk component', () => {
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
createComponent();
});
@@ -51,18 +61,30 @@ describe('Chunk component', () => {
});
describe('rendering', () => {
+ it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ jest.clearAllMocks();
+ createComponent({ isFirstChunk: true });
+
+ expect(window.requestIdleCallback).not.toHaveBeenCalled();
+ expect(findContent().exists()).toBe(true);
+ });
+
it('does not render a Chunk Line component if isHighlighted is false', () => {
expect(findChunkLines().length).toBe(0);
});
+ it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ idleCallbackSpy.mockRestore();
+ createComponent();
+
+ expect(findLineNumbers()).toHaveLength(0);
+ expect(findContent().exists()).toBe(false);
+ });
+
it('renders simplified line numbers and content if isHighlighted is false', () => {
expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
- expect(findLineNumbers().at(0).attributes()).toMatchObject({
- 'data-line-number': `${DEFAULT_PROPS.startingFrom + 1}`,
- href: `#L${DEFAULT_PROPS.startingFrom + 1}`,
- id: `L${DEFAULT_PROPS.startingFrom + 1}`,
- });
+ expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
expect(findContent().text()).toBe(DEFAULT_PROPS.content);
});
@@ -80,5 +102,14 @@ describe('Chunk component', () => {
blamePath: DEFAULT_PROPS.blamePath,
});
});
+
+ it('does not scroll to route hash if last chunk is not loaded', () => {
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+
+ it('scrolls to route hash if last chunk is loaded', () => {
+ createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
+ expect(scrollToElement).toHaveBeenCalledWith(hash, { behavior: 'auto' });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
index 375b1307616..a7b55d7332f 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/link_dependencies_spec.js
@@ -1,10 +1,26 @@
import packageJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/package_json_linker';
+import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
import gemspecLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker';
+import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker';
+import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker';
+import composerJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker';
import linkDependencies from '~/vue_shared/components/source_viewer/plugins/link_dependencies';
-import { PACKAGE_JSON_FILE_TYPE, PACKAGE_JSON_CONTENT, GEMSPEC_FILE_TYPE } from './mock_data';
+import {
+ PACKAGE_JSON_FILE_TYPE,
+ PACKAGE_JSON_CONTENT,
+ GEMSPEC_FILE_TYPE,
+ GODEPS_JSON_FILE_TYPE,
+ GEMFILE_FILE_TYPE,
+ PODSPEC_JSON_FILE_TYPE,
+ COMPOSER_JSON_FILE_TYPE,
+} from './mock_data';
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/package_json_linker');
jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemspec_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker');
+jest.mock('~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker');
describe('Highlight.js plugin for linking dependencies', () => {
const hljsResultMock = { value: 'test' };
@@ -18,4 +34,24 @@ describe('Highlight.js plugin for linking dependencies', () => {
linkDependencies(hljsResultMock, GEMSPEC_FILE_TYPE);
expect(gemspecLinker).toHaveBeenCalled();
});
+
+ it('calls godepsJsonLinker for godeps_json file types', () => {
+ linkDependencies(hljsResultMock, GODEPS_JSON_FILE_TYPE);
+ expect(godepsJsonLinker).toHaveBeenCalled();
+ });
+
+ it('calls gemfileLinker for gemfile file types', () => {
+ linkDependencies(hljsResultMock, GEMFILE_FILE_TYPE);
+ expect(gemfileLinker).toHaveBeenCalled();
+ });
+
+ it('calls podspecJsonLinker for podspec_json file types', () => {
+ linkDependencies(hljsResultMock, PODSPEC_JSON_FILE_TYPE);
+ expect(podspecJsonLinker).toHaveBeenCalled();
+ });
+
+ it('calls composerJsonLinker for composer_json file types', () => {
+ linkDependencies(hljsResultMock, COMPOSER_JSON_FILE_TYPE);
+ expect(composerJsonLinker).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
index aa874c9c081..5455479ec71 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/mock_data.js
@@ -1,4 +1,34 @@
export const PACKAGE_JSON_FILE_TYPE = 'package_json';
+
export const PACKAGE_JSON_CONTENT = '{ "dependencies": { "@babel/core": "^7.18.5" } }';
+export const COMPOSER_JSON_EXAMPLES = {
+ packagist: '{ "require": { "composer/installers": "^1.2" } }',
+ drupal: '{ "require": { "drupal/bootstrap": "3.x-dev" } }',
+ withoutLink: '{ "require": { "drupal/erp_common": "dev-master" } }',
+};
+
export const GEMSPEC_FILE_TYPE = 'gemspec';
+
+export const GODEPS_JSON_FILE_TYPE = 'godeps_json';
+
+export const GEMFILE_FILE_TYPE = 'gemfile';
+
+export const PODSPEC_JSON_FILE_TYPE = 'podspec_json';
+
+export const PODSPEC_JSON_CONTENT = `{
+ "dependencies": {
+ "MyCheckCore": [
+ ]
+ },
+ "subspecs": [
+ {
+ "dependencies": {
+ "AFNetworking/Security": [
+ ]
+ }
+ }
+ ]
+ }`;
+
+export const COMPOSER_JSON_FILE_TYPE = 'composer_json';
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js
new file mode 100644
index 00000000000..3ecb16ddcd0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/composer_json_linker_spec.js
@@ -0,0 +1,38 @@
+import composerJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/composer_json_linker';
+import { COMPOSER_JSON_EXAMPLES } from '../mock_data';
+
+describe('Highlight.js plugin for linking composer.json dependencies', () => {
+ it('mutates the input value by wrapping dependency names and versions in anchors', () => {
+ const inputValue =
+ '<span class="hljs-attr">&quot;drupal/erp_common"&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;dev-master&quot;</span>';
+ const outputValue =
+ '<span class="hljs-attr">&quot;drupal/erp_common"&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;dev-master&quot;</span>';
+ const hljsResultMock = { value: inputValue };
+
+ const output = composerJsonLinker(hljsResultMock, COMPOSER_JSON_EXAMPLES.withoutLink);
+ expect(output).toBe(outputValue);
+ });
+});
+
+const getInputValue = (dependencyString, version) =>
+ `<span class="hljs-attr">&quot;${dependencyString}&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;${version}&quot;</span>`;
+const getOutputValue = (dependencyString, version, expectedHref) =>
+ `<span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${dependencyString}</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${version}</a>&quot;</span>`;
+
+describe('Highlight.js plugin for linking Godeps.json dependencies', () => {
+ it.each`
+ type | dependency | version | expectedHref
+ ${'packagist'} | ${'composer/installers'} | ${'^1.2'} | ${'https://packagist.org/packages/composer/installers'}
+ ${'drupal'} | ${'drupal/bootstrap'} | ${'3.x-dev'} | ${'https://www.drupal.org/project/bootstrap'}
+ `(
+ 'mutates the input value by wrapping dependency names in anchors and altering path when needed',
+ ({ type, dependency, version, expectedHref }) => {
+ const inputValue = getInputValue(dependency, version);
+ const outputValue = getOutputValue(dependency, version, expectedHref);
+ const hljsResultMock = { value: inputValue };
+
+ const output = composerJsonLinker(hljsResultMock, COMPOSER_JSON_EXAMPLES[type]);
+ expect(output).toBe(outputValue);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
index e4ce07ec668..66e2020da27 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util_spec.js
@@ -1,13 +1,15 @@
import {
createLink,
generateHLJSOpenTag,
+ getObjectKeysByKeyName,
} from '~/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util';
+import { PODSPEC_JSON_CONTENT } from '../mock_data';
describe('createLink', () => {
it('generates a link with the correct attributes', () => {
const href = 'http://test.com';
const innerText = 'testing';
- const result = `<a href="${href}" rel="nofollow noreferrer noopener">${innerText}</a>`;
+ const result = `<a href="${href}" target="_blank" rel="nofollow noreferrer noopener">${innerText}</a>`;
expect(createLink(href, innerText)).toBe(result);
});
@@ -18,7 +20,7 @@ describe('createLink', () => {
const escapedHref = '&lt;script&gt;XSS&lt;/script&gt;';
const href = `http://test.com/${unescapedXSS}`;
const innerText = `testing${unescapedXSS}`;
- const result = `<a href="http://test.com/${escapedHref}" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
+ const result = `<a href="http://test.com/${escapedHref}" target="_blank" rel="nofollow noreferrer noopener">testing${escapedPackageName}</a>`;
expect(createLink(href, innerText)).toBe(result);
});
@@ -32,3 +34,11 @@ describe('generateHLJSOpenTag', () => {
expect(generateHLJSOpenTag(type)).toBe(result);
});
});
+
+describe('getObjectKeysByKeyName method', () => {
+ it('gets all object keys within specified key', () => {
+ const acc = [];
+ const keys = getObjectKeysByKeyName(JSON.parse(PODSPEC_JSON_CONTENT), 'dependencies', acc);
+ expect(keys).toEqual(['MyCheckCore', 'AFNetworking/Security']);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js
new file mode 100644
index 00000000000..4e188c9af7e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemfile_linker_spec.js
@@ -0,0 +1,13 @@
+import gemfileLinker from '~/vue_shared/components/source_viewer/plugins/utils/gemfile_linker';
+
+describe('Highlight.js plugin for linking gemfile dependencies', () => {
+ it('mutates the input value by wrapping dependency names in anchors', () => {
+ const inputValue = 'gem </span><span class="hljs-string">&#39;paranoia&#39;';
+ const outputValue =
+ 'gem </span><span class="hljs-string">&#39;<a href="https://rubygems.org/gems/paranoia" target="_blank" rel="nofollow noreferrer noopener">paranoia</a>&#39;';
+ const hljsResultMock = { value: inputValue };
+
+ const output = gemfileLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
index 3f74bfa117f..4b104b0bf43 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/gemspec_linker_spec.js
@@ -5,7 +5,7 @@ describe('Highlight.js plugin for linking gemspec dependencies', () => {
const inputValue =
's.add_dependency(<span class="hljs-string">&#x27;rugged&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
const outputValue =
- 's.add_dependency(<span class="hljs-string linked">&#x27;<a href="https://rubygems.org/gems/rugged" rel="nofollow noreferrer noopener">rugged</a>&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
+ 's.add_dependency(<span class="hljs-string linked">&#x27;<a href="https://rubygems.org/gems/rugged" target="_blank" rel="nofollow noreferrer noopener">rugged</a>&#x27;</span>, <span class="hljs-string">&#x27;~&gt; 0.24.0&#x27;</span>)';
const hljsResultMock = { value: inputValue };
const output = gemspecLinker(hljsResultMock);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js
new file mode 100644
index 00000000000..ea7e3936846
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker_spec.js
@@ -0,0 +1,27 @@
+import godepsJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker';
+
+const getInputValue = (dependencyString) =>
+ `<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">&quot;${dependencyString}&quot;</span>`;
+const getOutputValue = (dependencyString, expectedHref) =>
+ `<span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-attr">&quot;<a href="${expectedHref}" target="_blank" rel="nofollow noreferrer noopener">${dependencyString}</a>&quot;</span>`;
+
+describe('Highlight.js plugin for linking Godeps.json dependencies', () => {
+ it.each`
+ dependency | expectedHref
+ ${'gitlab.com/group/project/path'} | ${'https://gitlab.com/group/project/_/tree/master/path'}
+ ${'gitlab.com/group/subgroup/project.git/path'} | ${'https://gitlab.com/group/subgroup/_/tree/master/project.git/path'}
+ ${'github.com/docker/docker/pkg/homedir'} | ${'https://github.com/docker/docker/tree/master/pkg/homedir'}
+ ${'golang.org/x/net/http2'} | ${'https://godoc.org/golang.org/x/net/http2'}
+ ${'gopkg.in/yaml.v1'} | ${'https://gopkg.in/yaml.v1'}
+ `(
+ 'mutates the input value by wrapping dependency names in anchors and altering path when needed',
+ ({ dependency, expectedHref }) => {
+ const inputValue = getInputValue(dependency);
+ const outputValue = getOutputValue(dependency, expectedHref);
+ const hljsResultMock = { value: inputValue };
+
+ const output = godepsJsonLinker(hljsResultMock);
+ expect(output).toBe(outputValue);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
index e83c129818c..170a44f8ee2 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/package_json_linker_spec.js
@@ -6,7 +6,7 @@ describe('Highlight.js plugin for linking package.json dependencies', () => {
const inputValue =
'<span class="hljs-attr">&quot;@babel/core&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^7.18.5&quot;</span>';
const outputValue =
- '<span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">@babel/core</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" rel="nofollow noreferrer noopener">^7.18.5</a>&quot;</span>';
+ '<span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" target="_blank" rel="nofollow noreferrer noopener">@babel/core</a>&quot;</span>: <span class="hljs-attr">&quot;<a href="https://npmjs.com/package/@babel/core" target="_blank" rel="nofollow noreferrer noopener">^7.18.5</a>&quot;</span>';
const hljsResultMock = { value: inputValue };
const output = packageJsonLinker(hljsResultMock, PACKAGE_JSON_CONTENT);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js
new file mode 100644
index 00000000000..0ef63de68c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker_spec.js
@@ -0,0 +1,14 @@
+import podspecJsonLinker from '~/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker';
+import { PODSPEC_JSON_CONTENT } from '../mock_data';
+
+describe('Highlight.js plugin for linking podspec_json dependencies', () => {
+ it('mutates the input value by wrapping dependency names in anchors', () => {
+ const inputValue =
+ '<span class="hljs-attr">&quot;AFNetworking/Security&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">[';
+ const outputValue =
+ '<span class="hljs-attr">&quot;<a href="https://cocoapods.org/pods/AFNetworking" target="_blank" rel="nofollow noreferrer noopener">AFNetworking/Security</a>&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">[';
+ const hljsResultMock = { value: inputValue };
+ const output = podspecJsonLinker(hljsResultMock, PODSPEC_JSON_CONTENT);
+ expect(output).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
index bc6df1a2565..8d072c8c8de 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
@@ -8,13 +8,14 @@ describe('Highlight.js plugin for wrapping _emitter nodes', () => {
children: [
{ kind: 'string', children: ['Text 1'] },
{ kind: 'string', children: ['Text 2', { kind: 'comment', children: ['Text 3'] }] },
+ { kind: undefined, sublanguage: true, children: ['Text 3 (sublanguage)'] },
'Text4\nText5',
],
},
},
};
- const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text4</span>\n<span class="">Text5</span>`;
+ const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text 3 (sublanguage)</span><span class="">Text4</span>\n<span class="">Text5</span>`;
wrapChildNodes(hljsResultMock);
expect(hljsResultMock.value).toBe(outputValue);
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 6d319b37b02..33f370efdfa 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -10,6 +10,7 @@ import {
EVENT_LABEL_VIEWER,
EVENT_LABEL_FALLBACK,
ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
} from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
@@ -121,6 +122,7 @@ describe('Source Viewer component', () => {
it('highlights the first chunk', () => {
expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
+ expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
});
describe('auto-detects if a language cannot be loaded', () => {
@@ -133,45 +135,27 @@ describe('Source Viewer component', () => {
});
describe('rendering', () => {
- it('renders the first chunk', async () => {
- const firstChunk = findChunks().at(0);
-
- expect(firstChunk.props('content')).toContain(chunk1);
-
- expect(firstChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 0,
+ it.each`
+ chunkIndex | chunkContent | totalChunks
+ ${0} | ${chunk1} | ${0}
+ ${1} | ${chunk2} | ${3}
+ ${2} | ${chunk3Result} | ${3}
+ `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
+ const chunk = findChunks().at(chunkIndex);
+
+ expect(chunk.props('content')).toContain(chunkContent.trim());
+
+ expect(chunk.props()).toMatchObject({
+ totalLines: LINES_PER_CHUNK,
+ startingFrom: LINES_PER_CHUNK * chunkIndex,
+ totalChunks,
});
});
- it('renders the second chunk', async () => {
- const secondChunk = findChunks().at(1);
-
- expect(secondChunk.props('content')).toContain(chunk2.trim());
-
- expect(secondChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 70,
- });
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
});
-
- it('renders the third chunk', async () => {
- const thirdChunk = findChunks().at(2);
-
- expect(thirdChunk.props('content')).toContain(chunk3Result.trim());
-
- expect(chunk3Result).toEqual(chunk3.replace(/\r?\n/g, '\n'));
-
- expect(thirdChunk.props()).toMatchObject({
- totalLines: 70,
- startingFrom: 140,
- });
- });
- });
-
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
});
describe('LineHighlighter', () => {
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index f55d3156581..e1c6020686c 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -543,24 +543,6 @@ describe('IssuableItem', () => {
});
});
- describe('when issuable was created within the past 24 hours', () => {
- it('renders issuable card with a recently-created style', () => {
- wrapper = createComponent({
- issuable: { ...mockIssuable, createdAt: '2020-12-10T12:34:56' },
- });
-
- expect(wrapper.classes()).toContain('today');
- });
- });
-
- describe('when issuable was created earlier than the past 24 hours', () => {
- it('renders issuable card without a recently-created style', () => {
- wrapper = createComponent({ issuable: { ...mockIssuable, createdAt: '2020-12-09' } });
-
- expect(wrapper.classes()).not.toContain('today');
- });
- });
-
describe('scoped labels', () => {
describe.each`
description | labelPosition | hasScopedLabelsFeature | scoped
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 0c53f599d55..371844e66f4 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -322,6 +322,18 @@ describe('IssuableListRoot', () => {
});
});
+ describe('showFilteredSearchFriendlyText prop', () => {
+ describe.each([true, false])('when %s', (showFilteredSearchFriendlyText) => {
+ it('passes its value to FilteredSearchBar', () => {
+ wrapper = createComponent({ props: { showFilteredSearchFriendlyText } });
+
+ expect(findFilteredSearchBar().props('showFriendlyText')).toBe(
+ showFilteredSearchFriendlyText,
+ );
+ });
+ });
+ });
+
describe('alert', () => {
const error = 'oopsie!';
diff --git a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
new file mode 100644
index 00000000000..3dbff024a6b
--- /dev/null
+++ b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
@@ -0,0 +1,453 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Webhook push events form editor component Different push events rules when editing existing hook with "all_branches" strategy selected 1`] = `
+<gl-form-radio-group-stub
+ checked="all_branches"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing existing hook with "regex" strategy selected 1`] = `
+<gl-form-radio-group-stub
+ checked="regex"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value="foo"
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Regex such as %{REGEX_CODE} is supported."
+ />
+ </p>
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing existing hook with "wildcard" strategy selected 1`] = `
+<gl-form-radio-group-stub
+ checked="wildcard"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value="foo"
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported."
+ />
+ </p>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing new hook all_branches should be selected by default 1`] = `
+<gl-form-radio-group-stub
+ checked="all_branches"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing new hook should be able to set regex rule 1`] = `
+<gl-form-radio-group-stub
+ checked="regex"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value=""
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Regex such as %{REGEX_CODE} is supported."
+ />
+ </p>
+</gl-form-radio-group-stub>
+`;
+
+exports[`Webhook push events form editor component Different push events rules when editing new hook should be able to set wildcard rule 1`] = `
+<gl-form-radio-group-stub
+ checked="wildcard"
+ disabledfield="disabled"
+ htmlfield="html"
+ name="hook[branch_filter_strategy]"
+ options=""
+ textfield="text"
+ valuefield="value"
+>
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_all_branches"
+ value="all_branches"
+ >
+ <div
+ data-qa-selector="strategy_radio_all"
+ >
+ All branches
+ </div>
+ </gl-form-radio-stub>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_wildcard"
+ value="wildcard"
+ >
+ <div
+ data-qa-selector="strategy_radio_wildcard"
+ >
+
+ Wildcard pattern
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <gl-form-input-stub
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ name="hook[push_events_branch_filter]"
+ value=""
+ />
+ </div>
+
+ <p
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf-stub
+ message="Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported."
+ />
+ </p>
+
+ <gl-form-radio-stub
+ class="gl-mt-2 branch-filter-strategy-radio"
+ data-testid="rule_regex"
+ value="regex"
+ >
+ <div
+ data-qa-selector="strategy_radio_regex"
+ >
+
+ Regular expression
+
+ </div>
+ </gl-form-radio-stub>
+
+ <div
+ class="gl-ml-6"
+ >
+ <!---->
+ </div>
+
+ <!---->
+</gl-form-radio-group-stub>
+`;
diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js
index 16e0a3f549e..45a39d2dd58 100644
--- a/spec/frontend/webhooks/components/form_url_app_spec.js
+++ b/spec/frontend/webhooks/components/form_url_app_spec.js
@@ -1,10 +1,14 @@
import { nextTick } from 'vue';
-import { GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { scrollToElement } from '~/lib/utils/common_utils';
import FormUrlApp from '~/webhooks/components/form_url_app.vue';
import FormUrlMaskItem from '~/webhooks/components/form_url_mask_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/common_utils');
describe('FormUrlApp', () => {
let wrapper;
@@ -26,8 +30,11 @@ describe('FormUrlApp', () => {
const findAllUrlMaskItems = () => wrapper.findAllComponents(FormUrlMaskItem);
const findAddItem = () => wrapper.findComponent(GlLink);
const findFormUrl = () => wrapper.findByTestId('form-url');
+ const findFormUrlGroup = () => wrapper.findAllComponents(GlFormGroup).at(0);
const findFormUrlPreview = () => wrapper.findByTestId('form-url-preview');
const findUrlMaskSection = () => wrapper.findByTestId('url-mask-section');
+ const findFormEl = () => document.querySelector('.js-webhook-form');
+ const submitForm = () => findFormEl().dispatchEvent(new Event('submit'));
describe('template', () => {
it('renders radio buttons for URL masking', () => {
@@ -60,8 +67,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const firstItem = findAllUrlMaskItems().at(0);
- expect(firstItem.props('itemKey')).toBeNull();
- expect(firstItem.props('itemValue')).toBeNull();
+ expect(firstItem.props()).toMatchObject({
+ itemKey: null,
+ itemValue: null,
+ });
});
});
@@ -90,12 +99,18 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(2);
const firstItem = findAllUrlMaskItems().at(0);
- expect(firstItem.props('itemKey')).toBe(mockItem1.key);
- expect(firstItem.props('itemValue')).toBe(mockItem1.value);
+ expect(firstItem.props()).toMatchObject({
+ itemKey: mockItem1.key,
+ itemValue: mockItem1.value,
+ isEditing: true,
+ });
const secondItem = findAllUrlMaskItems().at(1);
- expect(secondItem.props('itemKey')).toBe(mockItem2.key);
- expect(secondItem.props('itemValue')).toBe(mockItem2.value);
+ expect(secondItem.props()).toMatchObject({
+ itemKey: mockItem2.key,
+ itemValue: mockItem2.value,
+ isEditing: true,
+ });
});
describe('on mask item input', () => {
@@ -106,8 +121,10 @@ describe('FormUrlApp', () => {
firstItem.vm.$emit('input', mockInput);
await nextTick();
- expect(firstItem.props('itemKey')).toBe(mockInput.key);
- expect(firstItem.props('itemValue')).toBe(mockInput.value);
+ expect(firstItem.props()).toMatchObject({
+ itemKey: mockInput.key,
+ itemValue: mockInput.value,
+ });
});
});
@@ -119,8 +136,10 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(3);
const lastItem = findAllUrlMaskItems().at(-1);
- expect(lastItem.props('itemKey')).toBeNull();
- expect(lastItem.props('itemValue')).toBeNull();
+ expect(lastItem.props()).toMatchObject({
+ itemKey: null,
+ itemValue: null,
+ });
});
});
@@ -133,8 +152,88 @@ describe('FormUrlApp', () => {
expect(findAllUrlMaskItems()).toHaveLength(1);
const newFirstItem = findAllUrlMaskItems().at(0);
- expect(newFirstItem.props('itemKey')).toBe(mockItem2.key);
- expect(newFirstItem.props('itemValue')).toBe(mockItem2.value);
+ expect(newFirstItem.props()).toMatchObject({
+ itemKey: mockItem2.key,
+ itemValue: mockItem2.value,
+ });
+ });
+ });
+ });
+
+ describe('validations', () => {
+ const inputRequiredText = FormUrlApp.i18n.inputRequired;
+
+ beforeEach(() => {
+ setHTMLFixture('<form class="js-webhook-form"></form>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it.each`
+ url | state | scrollToElementCalls
+ ${null} | ${undefined} | ${1}
+ ${''} | ${undefined} | ${1}
+ ${'https://example.com/'} | ${'true'} | ${0}
+ `('when URL is `$url`, state is `$state`', async ({ url, state, scrollToElementCalls }) => {
+ createComponent({
+ props: { initialUrl: url },
+ });
+
+ submitForm();
+ await nextTick();
+
+ expect(findFormUrlGroup().attributes('state')).toBe(state);
+ expect(scrollToElement).toHaveBeenCalledTimes(scrollToElementCalls);
+ expect(findFormUrlGroup().attributes('invalid-feedback')).toBe(inputRequiredText);
+ });
+
+ it.each`
+ key | value | keyInvalidFeedback | valueInvalidFeedback | scrollToElementCalls
+ ${null} | ${null} | ${inputRequiredText} | ${inputRequiredText} | ${1}
+ ${null} | ${'random'} | ${inputRequiredText} | ${FormUrlApp.i18n.valuePartOfUrl} | ${1}
+ ${null} | ${'secret'} | ${inputRequiredText} | ${null} | ${1}
+ ${'key'} | ${null} | ${null} | ${inputRequiredText} | ${1}
+ ${'key'} | ${'secret'} | ${null} | ${null} | ${0}
+ `(
+ 'when key is `$key` and value is `$value`',
+ async ({ key, value, keyInvalidFeedback, valueInvalidFeedback, scrollToElementCalls }) => {
+ createComponent({
+ props: { initialUrl: 'http://example.com?password=secret' },
+ });
+ findRadioGroup().vm.$emit('input', true);
+ await nextTick();
+
+ const maskItem = findAllUrlMaskItems().at(0);
+ const mockInput = { index: 0, key, value };
+ maskItem.vm.$emit('input', mockInput);
+
+ submitForm();
+ await nextTick();
+
+ expect(maskItem.props('keyInvalidFeedback')).toBe(keyInvalidFeedback);
+ expect(maskItem.props('valueInvalidFeedback')).toBe(valueInvalidFeedback);
+ expect(scrollToElement).toHaveBeenCalledTimes(scrollToElementCalls);
+ },
+ );
+
+ describe('when initialUrlVariables is passed', () => {
+ it('does not validate empty values', async () => {
+ const initialUrlVariables = [{ key: 'key' }];
+
+ createComponent({
+ props: { initialUrl: 'url', initialUrlVariables },
+ });
+
+ submitForm();
+ await nextTick();
+
+ const maskItem = findAllUrlMaskItems().at(0);
+
+ expect(maskItem.props('keyInvalidFeedback')).toBeNull();
+ expect(maskItem.props('valueInvalidFeedback')).toBeNull();
+ expect(scrollToElement).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/webhooks/components/form_url_mask_item_spec.js b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
index ab028ef2997..06c743749a6 100644
--- a/spec/frontend/webhooks/components/form_url_mask_item_spec.js
+++ b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
@@ -14,6 +14,7 @@ describe('FormUrlMaskItem', () => {
const mockKey = 'key';
const mockValue = 'value';
const mockInput = 'input';
+ const mockFeedback = 'feedback';
const createComponent = ({ props } = {}) => {
wrapper = shallowMountExtended(FormUrlMaskItem, {
@@ -21,29 +22,80 @@ describe('FormUrlMaskItem', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findMaskItemKey = () => wrapper.findByTestId('mask-item-key');
const findMaskItemValue = () => wrapper.findByTestId('mask-item-value');
const findRemoveButton = () => wrapper.findComponent(GlButton);
describe('template', () => {
it('renders input for key and value', () => {
- createComponent();
+ createComponent({ props: { itemKey: mockKey, itemValue: mockValue } });
const keyInput = findMaskItemKey();
- expect(keyInput.attributes('label')).toBe(FormUrlMaskItem.i18n.keyLabel);
- expect(keyInput.findComponent(GlFormInput).attributes('name')).toBe(
- 'hook[url_variables][][key]',
- );
+ expect(keyInput.attributes()).toMatchObject({
+ label: FormUrlMaskItem.i18n.keyLabel,
+ state: 'true',
+ });
+ expect(keyInput.findComponent(GlFormInput).attributes()).toMatchObject({
+ name: 'hook[url_variables][][key]',
+ value: mockKey,
+ });
const valueInput = findMaskItemValue();
- expect(valueInput.attributes('label')).toBe(FormUrlMaskItem.i18n.valueLabel);
- expect(valueInput.findComponent(GlFormInput).attributes('name')).toBe(
- 'hook[url_variables][][value]',
- );
+ expect(valueInput.attributes()).toMatchObject({
+ label: FormUrlMaskItem.i18n.valueLabel,
+ state: 'true',
+ });
+ expect(valueInput.findComponent(GlFormInput).attributes()).toMatchObject({
+ name: 'hook[url_variables][][value]',
+ value: mockValue,
+ });
+ });
+
+ describe('when isEditing is true', () => {
+ beforeEach(() => {
+ createComponent({ props: { isEditing: true } });
+ });
+
+ it('renders disabled key and value', () => {
+ expect(findMaskItemKey().findComponent(GlFormInput).attributes('disabled')).toBe('true');
+ expect(findMaskItemValue().findComponent(GlFormInput).attributes('disabled')).toBe('true');
+ });
+
+ it('renders disabled remove button', () => {
+ expect(findRemoveButton().attributes('disabled')).toBe('true');
+ });
+
+ it('displays ************ as input value', () => {
+ expect(findMaskItemValue().findComponent(GlFormInput).attributes('value')).toBe(
+ '************',
+ );
+ });
+ });
+
+ describe('when keyInvalidFeedback is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { keyInvalidFeedback: mockFeedback },
+ });
+ });
+
+ it('sets validation message on key', () => {
+ expect(findMaskItemKey().attributes('invalid-feedback')).toBe(mockFeedback);
+ expect(findMaskItemKey().attributes('state')).toBeUndefined();
+ });
+ });
+
+ describe('when valueInvalidFeedback is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { valueInvalidFeedback: mockFeedback },
+ });
+ });
+
+ it('sets validation message on value', () => {
+ expect(findMaskItemValue().attributes('invalid-feedback')).toBe(mockFeedback);
+ expect(findMaskItemValue().attributes('state')).toBeUndefined();
+ });
});
describe('on key input', () => {
diff --git a/spec/frontend/webhooks/components/push_events_spec.js b/spec/frontend/webhooks/components/push_events_spec.js
new file mode 100644
index 00000000000..ccb61c4049a
--- /dev/null
+++ b/spec/frontend/webhooks/components/push_events_spec.js
@@ -0,0 +1,117 @@
+import { nextTick } from 'vue';
+import { GlFormCheckbox, GlFormRadioGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PushEvents from '~/webhooks/components/push_events.vue';
+
+describe('Webhook push events form editor component', () => {
+ let wrapper;
+
+ const findPushEventsCheckBox = (w = wrapper) => w.findComponent(GlFormCheckbox);
+ const findPushEventsIndicator = (w = wrapper) => w.find('input[name="hook[push_events]"]');
+ const findPushEventRulesGroup = (w = wrapper) => w.findComponent(GlFormRadioGroup);
+ const getPushEventsRuleValue = (w = wrapper) => findPushEventRulesGroup(w).vm.$attrs.checked;
+ const findWildcardRuleInput = (w = wrapper) => w.findByTestId('webhook_branch_filter_field');
+ const findRegexRuleInput = (w = wrapper) => w.findByTestId('webhook_branch_filter_field');
+
+ const createComponent = (provides) =>
+ shallowMountExtended(PushEvents, {
+ provide: {
+ isNewHook: true,
+ pushEvents: false,
+ strategy: 'wildcard',
+ pushEventsBranchFilter: '',
+ ...provides,
+ },
+ });
+
+ describe('Renders push events checkbox', () => {
+ it('when it is a new hook', async () => {
+ wrapper = createComponent({
+ isNewHook: true,
+ });
+ await nextTick();
+
+ const checkbox = findPushEventsCheckBox();
+ expect(checkbox.exists()).toBe(true);
+ expect(findPushEventRulesGroup().exists()).toBe(false);
+ expect(findPushEventsIndicator().attributes('value')).toBe('false');
+ });
+
+ it('when it is not a new hook and push events is enabled', async () => {
+ wrapper = createComponent({
+ isNewHook: false,
+ pushEvents: true,
+ });
+ await nextTick();
+
+ expect(findPushEventsCheckBox().exists()).toBe(true);
+ expect(findPushEventRulesGroup().exists()).toBe(true);
+ expect(findPushEventsIndicator().attributes('value')).toBe('true');
+ });
+ });
+
+ describe('Different push events rules', () => {
+ describe('when editing new hook', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ isNewHook: true,
+ });
+ await nextTick();
+ await findPushEventsCheckBox().vm.$emit('input', true);
+ await nextTick();
+ });
+
+ it('all_branches should be selected by default', async () => {
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+ });
+
+ it('should be able to set wildcard rule', async () => {
+ expect(getPushEventsRuleValue()).toBe('all_branches');
+ expect(findWildcardRuleInput().exists()).toBe(false);
+ expect(findRegexRuleInput().exists()).toBe(false);
+
+ await findPushEventRulesGroup(wrapper).vm.$emit('input', 'wildcard');
+ expect(findWildcardRuleInput().exists()).toBe(true);
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+
+ const testVal = 'test-val';
+ findWildcardRuleInput().vm.$emit('input', testVal);
+ await nextTick();
+ expect(findWildcardRuleInput().attributes('value')).toBe(testVal);
+ });
+
+ it('should be able to set regex rule', async () => {
+ expect(getPushEventsRuleValue()).toBe('all_branches');
+ expect(findRegexRuleInput().exists()).toBe(false);
+ expect(findWildcardRuleInput().exists()).toBe(false);
+
+ await findPushEventRulesGroup(wrapper).vm.$emit('input', 'regex');
+ expect(findRegexRuleInput().exists()).toBe(true);
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+
+ const testVal = 'test-val';
+ findRegexRuleInput().vm.$emit('input', testVal);
+ await nextTick();
+ expect(findRegexRuleInput().attributes('value')).toBe(testVal);
+ });
+ });
+
+ describe('when editing existing hook', () => {
+ it.each(['all_branches', 'wildcard', 'regex'])(
+ 'with "%s" strategy selected',
+ async (strategy) => {
+ wrapper = createComponent({
+ isNewHook: false,
+ pushEvents: true,
+ pushEventsBranchFilter: 'foo',
+ strategy,
+ });
+ await nextTick();
+
+ expect(findPushEventsIndicator().attributes('value')).toBe('true');
+ expect(findPushEventRulesGroup().element).toMatchSnapshot();
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 1b204b6fd60..7367212e49f 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -8,7 +8,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
-import { temporaryConfig } from '~/graphql_shared/issuable_client';
+import { config } from '~/graphql_shared/issuable_client';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -86,7 +86,7 @@ describe('WorkItemAssignees component', () => {
],
{},
{
- typePolicies: temporaryConfig.cacheConfig.typePolicies,
+ typePolicies: config.cacheConfig.typePolicies,
},
);
diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
new file mode 100644
index 00000000000..01ab7824975
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
@@ -0,0 +1,108 @@
+import { shallowMount } from '@vue/test-utils';
+import $ from 'jquery';
+import { nextTick } from 'vue';
+import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
+import { descriptionTextWithCheckboxes, descriptionHtmlWithCheckboxes } from '../mock_data';
+
+describe('WorkItemDescription', () => {
+ let wrapper;
+
+ const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
+ const findCheckboxAtIndex = (index) => wrapper.findAll('input[type="checkbox"]').at(index);
+
+ const defaultWorkItemDescription = {
+ description: descriptionTextWithCheckboxes,
+ descriptionHtml: descriptionHtmlWithCheckboxes,
+ };
+
+ const createComponent = ({
+ workItemDescription = defaultWorkItemDescription,
+ canEdit = false,
+ } = {}) => {
+ wrapper = shallowMount(WorkItemDescriptionRendered, {
+ propsData: {
+ workItemDescription,
+ canEdit,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders gfm', async () => {
+ const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
+
+ createComponent();
+
+ await nextTick();
+
+ expect(renderGFMSpy).toHaveBeenCalled();
+ });
+
+ describe('with checkboxes', () => {
+ beforeEach(() => {
+ createComponent({
+ canEdit: true,
+ workItemDescription: {
+ description: `- [x] todo 1\n- [ ] todo 2`,
+ descriptionHtml: `<ul dir="auto" class="task-list" data-sourcepos="1:1-4:0">
+<li class="task-list-item" data-sourcepos="1:1-2:15">
+<input checked="" class="task-list-item-checkbox" type="checkbox"> todo 1</li>
+<li class="task-list-item" data-sourcepos="2:1-2:15">
+<input class="task-list-item-checkbox" type="checkbox"> todo 2</li>
+</ul>`,
+ },
+ });
+ });
+
+ it('checks unchecked checkbox', async () => {
+ findCheckboxAtIndex(1).setChecked();
+
+ await nextTick();
+
+ const updatedDescription = `- [x] todo 1\n- [x] todo 2`;
+ expect(wrapper.emitted('descriptionUpdated')).toEqual([[updatedDescription]]);
+ });
+
+ it('disables checkbox while updating', async () => {
+ findCheckboxAtIndex(1).setChecked();
+
+ await nextTick();
+
+ expect(findCheckboxAtIndex(1).attributes().disabled).toBeDefined();
+ });
+
+ it('unchecks checked checkbox', async () => {
+ findCheckboxAtIndex(0).setChecked(false);
+
+ await nextTick();
+
+ const updatedDescription = `- [ ] todo 1\n- [ ] todo 2`;
+ expect(wrapper.emitted('descriptionUpdated')).toEqual([[updatedDescription]]);
+ });
+ });
+
+ describe('Edit button', () => {
+ it('is not visible when canUpdate = false', async () => {
+ await createComponent({
+ canUpdate: false,
+ });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('toggles edit mode', async () => {
+ createComponent({
+ canEdit: true,
+ });
+
+ findEditButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 0691fe25e0d..c79b049442d 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -8,21 +8,23 @@ import EditedAt from '~/issues/show/components/edited.vue';
import { updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
+import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
updateWorkItemMutationResponse,
+ workItemDescriptionSubscriptionResponse,
workItemResponseFactory,
workItemQueryResponse,
+ projectWorkItemResponse,
} from '../mock_data';
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
- return {
- confirmAction: jest.fn(),
- };
-});
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/lib/utils/autosave');
const workItemId = workItemQueryResponse.data.workItem.id;
@@ -33,12 +35,22 @@ describe('WorkItemDescription', () => {
Vue.use(VueApollo);
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+ const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
+ const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
+ let workItemResponseHandler;
+ let workItemsMvc2;
- const findEditButton = () => wrapper.find('[data-testid="edit-description"]');
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+ const findRenderedDescription = () => wrapper.findComponent(WorkItemDescriptionRendered);
const findEditedAt = () => wrapper.findComponent(EditedAt);
- const editDescription = (newText) => wrapper.find('textarea').setValue(newText);
+ const editDescription = (newText) => {
+ if (workItemsMvc2) {
+ return findMarkdownEditor().vm.$emit('input', newText);
+ }
+ return wrapper.find('textarea').setValue(newText);
+ };
const clickCancel = () => wrapper.find('[data-testid="cancel"]').vm.$emit('click');
const clickSave = () => wrapper.find('[data-testid="save-description"]').vm.$emit('click', {});
@@ -48,18 +60,30 @@ describe('WorkItemDescription', () => {
canUpdate = true,
workItemResponse = workItemResponseFactory({ canUpdate }),
isEditing = false,
+ fetchByIid = false,
} = {}) => {
- const workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
+ workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
[workItemQuery, workItemResponseHandler],
[updateWorkItemMutation, mutationHandler],
+ [workItemDescriptionSubscription, subscriptionHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
]),
propsData: {
workItemId: id,
fullPath: 'test-project-path',
+ queryVariables: {
+ id: workItemId,
+ },
+ fetchByIid,
+ },
+ provide: {
+ glFeatures: {
+ workItemsMvc2,
+ },
},
stubs: {
MarkdownField,
@@ -69,7 +93,7 @@ describe('WorkItemDescription', () => {
await waitForPromises();
if (isEditing) {
- findEditButton().vm.$emit('click');
+ findRenderedDescription().vm.$emit('startEditing');
await nextTick();
}
@@ -79,171 +103,178 @@ describe('WorkItemDescription', () => {
wrapper.destroy();
});
- describe('Edit button', () => {
- it('is not visible when canUpdate = false', async () => {
- await createComponent({
- canUpdate: false,
+ describe.each([true, false])(
+ 'editing description with workItemsMvc2 %workItemsMvc2Enabled',
+ (workItemsMvc2Enabled) => {
+ beforeEach(() => {
+ beforeEach(() => {
+ workItemsMvc2 = workItemsMvc2Enabled;
+ });
});
- expect(findEditButton().exists()).toBe(false);
- });
+ describe('editing description', () => {
+ it('shows edited by text', async () => {
+ const lastEditedAt = '2022-09-21T06:18:42Z';
+ const lastEditedBy = {
+ name: 'Administrator',
+ webPath: '/root',
+ };
- it('toggles edit mode', async () => {
- await createComponent({
- canUpdate: true,
- });
+ await createComponent({
+ workItemResponse: workItemResponseFactory({
+ lastEditedAt,
+ lastEditedBy,
+ }),
+ });
- findEditButton().vm.$emit('click');
+ expect(findEditedAt().props()).toEqual({
+ updatedAt: lastEditedAt,
+ updatedByName: lastEditedBy.name,
+ updatedByPath: lastEditedBy.webPath,
+ });
+ });
- await nextTick();
+ it('does not show edited by text', async () => {
+ await createComponent();
- expect(findMarkdownField().exists()).toBe(true);
- });
- });
+ expect(findEditedAt().exists()).toBe(false);
+ });
- describe('editing description', () => {
- it('shows edited by text', async () => {
- const lastEditedAt = '2022-09-21T06:18:42Z';
- const lastEditedBy = {
- name: 'Administrator',
- webPath: '/root',
- };
-
- await createComponent({
- workItemResponse: workItemResponseFactory({
- lastEditedAt,
- lastEditedBy,
- }),
- });
+ it('cancels when clicking cancel', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- expect(findEditedAt().props()).toEqual({
- updatedAt: lastEditedAt,
- updatedByName: lastEditedBy.name,
- updatedByPath: lastEditedBy.webPath,
- });
- });
+ clickCancel();
- it('does not show edited by text', async () => {
- await createComponent();
+ await nextTick();
- expect(findEditedAt().exists()).toBe(false);
- });
+ expect(confirmAction).not.toHaveBeenCalled();
+ expect(findMarkdownField().exists()).toBe(false);
+ });
- it('cancels when clicking cancel', async () => {
- await createComponent({
- isEditing: true,
- });
+ it('prompts for confirmation when clicking cancel after changes', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- clickCancel();
+ editDescription('updated desc');
- await nextTick();
+ clickCancel();
- expect(confirmAction).not.toHaveBeenCalled();
- expect(findMarkdownField().exists()).toBe(false);
- });
+ await nextTick();
- it('prompts for confirmation when clicking cancel after changes', async () => {
- await createComponent({
- isEditing: true,
- });
+ expect(confirmAction).toHaveBeenCalled();
+ });
- editDescription('updated desc');
+ it('calls update widgets mutation', async () => {
+ const updatedDesc = 'updated desc';
- clickCancel();
+ await createComponent({
+ isEditing: true,
+ });
- await nextTick();
+ editDescription(updatedDesc);
- expect(confirmAction).toHaveBeenCalled();
- });
+ clickSave();
- it('calls update widgets mutation', async () => {
- await createComponent({
- isEditing: true,
- });
+ await waitForPromises();
- editDescription('updated desc');
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemId,
+ descriptionWidget: {
+ description: updatedDesc,
+ },
+ },
+ });
+ });
- clickSave();
+ it('tracks editing description', async () => {
+ await createComponent({
+ isEditing: true,
+ markdownPreviewPath: '/preview',
+ });
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- await waitForPromises();
+ clickSave();
- expect(mutationSuccessHandler).toHaveBeenCalledWith({
- input: {
- id: workItemId,
- descriptionWidget: {
- description: 'updated desc',
- },
- },
- });
- });
+ await waitForPromises();
- it('tracks editing description', async () => {
- await createComponent({
- isEditing: true,
- markdownPreviewPath: '/preview',
- });
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_description',
+ property: 'type_Task',
+ });
+ });
- clickSave();
+ it('emits error when mutation returns error', async () => {
+ const error = 'eror';
- await waitForPromises();
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: {},
+ errors: [error],
+ },
+ },
+ }),
+ });
- expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_description', {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_description',
- property: 'type_Task',
- });
- });
+ editDescription('updated desc');
- it('emits error when mutation returns error', async () => {
- const error = 'eror';
+ clickSave();
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockResolvedValue({
- data: {
- workItemUpdate: {
- workItem: {},
- errors: [error],
- },
- },
- }),
- });
+ await waitForPromises();
- editDescription('updated desc');
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- clickSave();
+ it('emits error when mutation fails', async () => {
+ const error = 'eror';
- await waitForPromises();
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
+ });
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ editDescription('updated desc');
- it('emits error when mutation fails', async () => {
- const error = 'eror';
+ clickSave();
- await createComponent({
- isEditing: true,
- mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
- });
+ await waitForPromises();
- editDescription('updated desc');
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
- clickSave();
+ it('autosaves description', async () => {
+ await createComponent({
+ isEditing: true,
+ });
- await waitForPromises();
+ editDescription('updated desc');
- expect(wrapper.emitted('error')).toEqual([[error]]);
- });
+ expect(updateDraft).toHaveBeenCalled();
+ });
+ });
+
+ it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
+ createComponent({ fetchByIid: false });
+ await waitForPromises();
- it('autosaves description', async () => {
- await createComponent({
- isEditing: true,
+ expect(workItemResponseHandler).toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
});
- editDescription('updated desc');
+ it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
+ createComponent({ fetchByIid: true });
+ await waitForPromises();
- expect(updateDraft).toHaveBeenCalled();
- });
- });
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).toHaveBeenCalled();
+ });
+ },
+ );
});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 6b1ef8971d3..4029e47c390 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -86,6 +86,7 @@ describe('WorkItemDetailModal component', () => {
isModal: true,
workItemId: defaultPropsData.workItemId,
workItemParentId: defaultPropsData.issueGid,
+ iid: null,
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index aae61b11196..26777b57797 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -24,12 +24,13 @@ import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemInformation from '~/work_items/components/work_item_information.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
+import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
-import { temporaryConfig } from '~/graphql_shared/issuable_client';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockParent,
@@ -37,6 +38,8 @@ import {
workItemResponseFactory,
workItemTitleSubscriptionResponse,
workItemAssigneesSubscriptionResponse,
+ workItemMilestoneSubscriptionResponse,
+ projectWorkItemResponse,
} from '../mock_data';
describe('WorkItemDetail component', () => {
@@ -52,8 +55,12 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const successByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
+ const milestoneSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemMilestoneSubscriptionResponse);
const assigneesSubscriptionHandler = jest
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
@@ -85,26 +92,23 @@ describe('WorkItemDetail component', () => {
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
- includeWidgets = false,
workItemsMvc2Enabled = false,
+ fetchByIid = false,
+ iidPathQueryParam = undefined,
} = {}) => {
const handlers = [
[workItemQuery, handler],
[workItemTitleSubscription, subscriptionHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
+ [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [workItemByIidQuery, successByIidHandler],
confidentialityMock,
];
wrapper = shallowMount(WorkItemDetail, {
- apolloProvider: createMockApollo(
- handlers,
- {},
- {
- typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
- },
- ),
- propsData: { isModal, workItemId },
+ apolloProvider: createMockApollo(handlers),
+ propsData: { isModal, workItemId, iid: '1' },
data() {
return {
updateInProgress,
@@ -114,15 +118,24 @@ describe('WorkItemDetail component', () => {
provide: {
glFeatures: {
workItemsMvc2: workItemsMvc2Enabled,
+ useIidInWorkItemsPath: fetchByIid,
},
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
projectNamespace: 'namespace',
+ fullPath: 'group/project',
},
stubs: {
WorkItemWeight: true,
WorkItemIteration: true,
},
+ mocks: {
+ $route: {
+ query: {
+ iid_path: iidPathQueryParam,
+ },
+ },
+ },
});
};
@@ -421,8 +434,9 @@ describe('WorkItemDetail component', () => {
});
describe('subscriptions', () => {
- it('calls the title subscription', () => {
+ it('calls the title subscription', async () => {
createComponent();
+ await waitForPromises();
expect(titleSubscriptionHandler).toHaveBeenCalledWith({
issuableId: workItemQueryResponse.data.workItem.id,
@@ -543,15 +557,41 @@ describe('WorkItemDetail component', () => {
describe('milestone widget', () => {
it.each`
- description | includeWidgets | exists
- ${'renders when widget is returned from API'} | ${true} | ${true}
- ${'does not render when widget is not returned from API'} | ${false} | ${false}
- `('$description', async ({ includeWidgets, exists }) => {
- createComponent({ includeWidgets, workItemsMvc2Enabled: true });
+ description | milestoneWidgetPresent | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', async ({ milestoneWidgetPresent, exists }) => {
+ const response = workItemResponseFactory({ milestoneWidgetPresent });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
expect(findWorkItemMilestone().exists()).toBe(exists);
});
+
+ describe('milestone subscription', () => {
+ describe('when the milestone widget exists', () => {
+ it('calls the milestone subscription', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({
+ issuableId: workItemQueryResponse.data.workItem.id,
+ });
+ });
+ });
+
+ describe('when the assignees widget does not exist', () => {
+ it('does not call the milestone subscription', async () => {
+ const response = workItemResponseFactory({ milestoneWidgetPresent: false });
+ const handler = jest.fn().mockResolvedValue(response);
+ createComponent({ handler });
+ await waitForPromises();
+
+ expect(milestoneSubscriptionHandler).not.toHaveBeenCalled();
+ });
+ });
+ });
});
describe('work item information', () => {
@@ -571,4 +611,35 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemInformationAlert().exists()).toBe(false);
});
});
+
+ it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ id: workItemQueryResponse.data.workItem.id,
+ });
+ expect(successByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is true but there is no `iid_path` parameter in URL', async () => {
+ createComponent({ fetchByIid: true });
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ id: workItemQueryResponse.data.workItem.id,
+ });
+ expect(successByIidHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => {
+ createComponent({ fetchByIid: true, iidPathQueryParam: 'true' });
+ await waitForPromises();
+
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).toHaveBeenCalledWith({
+ fullPath: 'group/project',
+ iid: '1',
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js
index 701406b9588..7ebaf8209c7 100644
--- a/spec/frontend/work_items/components/work_item_due_date_spec.js
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -140,7 +140,7 @@ describe('WorkItemDueDate component', () => {
beforeEach(() => {
createComponent({ canUpdate: true, dueDate: '2022-12-31', startDate: '2022-12-31' });
- datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker.calendar, 'show');
+ datePickerOpenSpy = jest.spyOn(wrapper.vm.$refs.dueDatePicker, 'show');
findStartDatePicker().vm.$emit('input', startDate);
findStartDatePicker().vm.$emit('close');
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index e6ff7e8502d..9f7659b3f8d 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -9,6 +9,7 @@ import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widg
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants';
import {
@@ -18,6 +19,7 @@ import {
workItemResponseFactory,
updateWorkItemMutationResponse,
workItemLabelsSubscriptionResponse,
+ projectWorkItemResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -33,6 +35,7 @@ describe('WorkItemLabels component', () => {
const findLabelsTitle = () => wrapper.findByTestId('labels-title');
const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
@@ -45,12 +48,14 @@ describe('WorkItemLabels component', () => {
workItemQueryHandler = workItemQuerySuccess,
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
+ fetchByIid = false,
} = {}) => {
const apolloProvider = createMockApollo([
[workItemQuery, workItemQueryHandler],
[labelSearchQuery, searchQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
[workItemLabelsSubscription, subscriptionHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
]);
wrapper = mountExtended(WorkItemLabels, {
@@ -58,6 +63,10 @@ describe('WorkItemLabels component', () => {
workItemId,
canUpdate,
fullPath: 'test-project-path',
+ queryVariables: {
+ id: workItemId,
+ },
+ fetchByIid,
},
attachTo: document.body,
apolloProvider,
@@ -226,4 +235,20 @@ describe('WorkItemLabels component', () => {
});
});
});
+
+ it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
+ createComponent({ fetchByIid: false });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
+ createComponent({ fetchByIid: true });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).not.toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index ab3ea623e3e..071d5fb715a 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
-import { GlForm, GlFormInput, GlFormCombobox } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlTokenSelector } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
+import { FORM_TYPES } from '~/work_items/constants';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
@@ -14,6 +15,7 @@ import {
projectWorkItemTypesQueryResponse,
createWorkItemMutationResponse,
updateWorkItemMutationResponse,
+ mockIterationWidgetResponse,
} from '../../mock_data';
Vue.use(VueApollo);
@@ -23,22 +25,35 @@ describe('WorkItemLinksForm', () => {
const updateMutationResolver = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const createMutationResolver = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
+ const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
+
+ const mockParentIteration = mockIterationWidgetResponse;
const createComponent = async ({
- listResponse = availableWorkItemsResponse,
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
hasIterationsFeature = false,
+ workItemsMvc2Enabled = false,
+ parentIteration = null,
+ formType = FORM_TYPES.create,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
- [projectWorkItemsQuery, jest.fn().mockResolvedValue(listResponse)],
+ [projectWorkItemsQuery, availableWorkItemsResolver],
[projectWorkItemTypesQuery, jest.fn().mockResolvedValue(typesResponse)],
[updateWorkItemMutation, updateMutationResolver],
[createWorkItemMutation, createMutationResolver],
]),
- propsData: { issuableGid: 'gid://gitlab/WorkItem/1', parentConfidential },
+ propsData: {
+ issuableGid: 'gid://gitlab/WorkItem/1',
+ parentConfidential,
+ parentIteration,
+ formType,
+ },
provide: {
+ glFeatures: {
+ workItemsMvc2: workItemsMvc2Enabled,
+ },
projectPath: 'project/path',
hasIterationsFeature,
},
@@ -48,89 +63,155 @@ describe('WorkItemLinksForm', () => {
};
const findForm = () => wrapper.findComponent(GlForm);
- const findCombobox = () => wrapper.findComponent(GlFormCombobox);
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findInput = () => wrapper.findComponent(GlFormInput);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
- beforeEach(async () => {
- await createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders form', () => {
- expect(findForm().exists()).toBe(true);
- });
-
- it('creates child task in non confidential parent', async () => {
- findInput().vm.$emit('input', 'Create task test');
+ describe('creating a new work item', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
- findForm().vm.$emit('submit', {
- preventDefault: jest.fn(),
+ it('renders create form', () => {
+ expect(findForm().exists()).toBe(true);
+ expect(findInput().exists()).toBe(true);
+ expect(findAddChildButton().text()).toBe('Create task');
+ expect(findTokenSelector().exists()).toBe(false);
});
- await waitForPromises();
- expect(createMutationResolver).toHaveBeenCalledWith({
- input: {
- title: 'Create task test',
- projectPath: 'project/path',
- workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
- hierarchyWidget: {
- parentId: 'gid://gitlab/WorkItem/1',
+
+ it('creates child task in non confidential parent', async () => {
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create task test',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: false,
},
- confidential: false,
- },
+ });
});
- });
- it('creates child task in confidential parent', async () => {
- await createComponent({ parentConfidential: true });
+ it('creates child task in confidential parent', async () => {
+ await createComponent({ parentConfidential: true });
- findInput().vm.$emit('input', 'Create confidential task');
+ findInput().vm.$emit('input', 'Create confidential task');
- findForm().vm.$emit('submit', {
- preventDefault: jest.fn(),
- });
- await waitForPromises();
- expect(createMutationResolver).toHaveBeenCalledWith({
- input: {
- title: 'Create confidential task',
- projectPath: 'project/path',
- workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
- hierarchyWidget: {
- parentId: 'gid://gitlab/WorkItem/1',
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(createMutationResolver).toHaveBeenCalledWith({
+ input: {
+ title: 'Create confidential task',
+ projectPath: 'project/path',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ confidential: true,
},
- confidential: true,
- },
+ });
});
});
- // Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('selects and add child', async () => {
- findCombobox().vm.$emit('input', availableWorkItemsResponse.data.workspace.workItems.edges[0]);
+ describe('adding an existing work item', () => {
+ beforeEach(async () => {
+ await createComponent({ formType: FORM_TYPES.add });
+ });
- findAddChildButton().vm.$emit('click');
- await waitForPromises();
- expect(updateMutationResolver).toHaveBeenCalled();
- });
+ it('renders add form', () => {
+ expect(findForm().exists()).toBe(true);
+ expect(findTokenSelector().exists()).toBe(true);
+ expect(findAddChildButton().text()).toBe('Add task');
+ expect(findInput().exists()).toBe(false);
+ });
- // eslint-disable-next-line jest/no-disabled-tests
- describe.skip('when typing in combobox', () => {
- beforeEach(async () => {
- findCombobox().vm.$emit('input', 'Task');
+ it('searches for available work items as prop when typing in input', async () => {
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'Task');
await waitForPromises();
- await jest.runOnlyPendingTimers();
+
+ expect(availableWorkItemsResolver).toHaveBeenCalled();
});
- it('passes available work items as prop', () => {
- expect(findCombobox().exists()).toBe(true);
- expect(findCombobox().props('tokenList').length).toBe(2);
+ it('selects and adds children', async () => {
+ findTokenSelector().vm.$emit(
+ 'input',
+ availableWorkItemsResponse.data.workspace.workItems.nodes,
+ );
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ await waitForPromises();
+
+ expect(findAddChildButton().text()).toBe('Add tasks');
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(updateMutationResolver).toHaveBeenCalled();
});
+ });
+
+ describe('associate iteration with task', () => {
+ it('does not update iteration when mvc2 feature flag is not enabled', async () => {
+ await createComponent({
+ hasIterationsFeature: true,
+ parentIteration: mockParentIteration,
+ });
- it('passes action to create task', () => {
- expect(findCombobox().props('actionList').length).toBe(1);
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(updateMutationResolver).not.toHaveBeenCalled();
+ });
+ it('updates when parent has an iteration associated', async () => {
+ await createComponent({
+ workItemsMvc2Enabled: true,
+ hasIterationsFeature: true,
+ parentIteration: mockParentIteration,
+ });
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(updateMutationResolver).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ iterationWidget: {
+ iterationId: mockParentIteration.id,
+ },
+ },
+ });
+ });
+ it('does not update when parent has no iteration associated', async () => {
+ await createComponent({
+ workItemsMvc2Enabled: true,
+ hasIterationsFeature: true,
+ });
+ findInput().vm.$emit('input', 'Create task test');
+
+ findForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ });
+ await waitForPromises();
+ expect(updateMutationResolver).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index 6961996f912..66ce2c1becf 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -8,6 +8,7 @@ import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import { FORM_TYPES } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
@@ -41,6 +42,13 @@ const issueDetailsResponse = (confidential = false) => ({
},
__typename: 'Iteration',
},
+ milestone: {
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/28',
+ title: 'v2.0',
+ __typename: 'Milestone',
+ },
__typename: 'Issue',
},
__typename: 'Project',
@@ -107,7 +115,9 @@ describe('WorkItemLinks', () => {
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findLinksBody = () => wrapper.findByTestId('links-body');
const findEmptyState = () => wrapper.findByTestId('links-empty');
+ const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
+ const findToggleCreateFormButton = () => wrapper.findByTestId('toggle-create-form');
const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
@@ -136,11 +146,27 @@ describe('WorkItemLinks', () => {
});
describe('add link form', () => {
- it('displays form on click add button and hides form on cancel', async () => {
+ it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
+ findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
expect(findAddLinksForm().exists()).toBe(true);
+ expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.add);
+
+ findAddLinksForm().vm.$emit('cancel');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(false);
+ });
+
+ it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => {
+ findToggleFormDropdown().vm.$emit('click');
+ findToggleCreateFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findAddLinksForm().exists()).toBe(true);
+ expect(findAddLinksForm().props('formType')).toBe(FORM_TYPES.create);
findAddLinksForm().vm.$emit('cancel');
await nextTick();
@@ -193,7 +219,7 @@ describe('WorkItemLinks', () => {
});
it('does not display button to toggle Add form', () => {
- expect(findToggleAddFormButton().exists()).toBe(false);
+ expect(findToggleFormDropdown().exists()).toBe(false);
});
it('does not display link menu on children', () => {
@@ -283,6 +309,7 @@ describe('WorkItemLinks', () => {
await createComponent({
issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
});
+ findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index 08cdf62ae52..60ba2b55f76 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -9,7 +9,7 @@ import {
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
-import { resolvers, temporaryConfig } from '~/graphql_shared/issuable_client';
+import { resolvers, config } from '~/graphql_shared/issuable_client';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -22,8 +22,14 @@ import {
mockMilestoneWidgetResponse,
workItemResponseFactory,
updateWorkItemMutationErrorResponse,
+ workItemMilestoneSubscriptionResponse,
+ projectWorkItemResponse,
+ updateWorkItemMutationResponse,
} from 'jest/work_items/mock_data';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
describe('WorkItemMilestone component', () => {
Vue.use(VueApollo);
@@ -47,6 +53,8 @@ describe('WorkItemMilestone component', () => {
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+ const workItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const networkResolvedValue = new Error();
@@ -54,6 +62,12 @@ describe('WorkItemMilestone component', () => {
const successSearchWithNoMatchingMilestones = jest
.fn()
.mockResolvedValue(projectMilestonesResponseWithNoMilestones);
+ const milestoneSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemMilestoneSubscriptionResponse);
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
const showDropdown = () => {
findDropdown().vm.$emit('shown');
@@ -67,12 +81,20 @@ describe('WorkItemMilestone component', () => {
canUpdate = true,
milestone = mockMilestoneWidgetResponse,
searchQueryHandler = successSearchQueryHandler,
+ fetchByIid = false,
+ mutationHandler = successUpdateWorkItemMutationHandler,
} = {}) => {
const apolloProvider = createMockApollo(
- [[projectMilestonesQuery, searchQueryHandler]],
+ [
+ [workItemQuery, workItemQueryHandler],
+ [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [projectMilestonesQuery, searchQueryHandler],
+ [updateWorkItemMutation, mutationHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
+ ],
resolvers,
{
- typePolicies: temporaryConfig.cacheConfig.typePolicies,
+ typePolicies: config.cacheConfig.typePolicies,
},
);
@@ -92,6 +114,10 @@ describe('WorkItemMilestone component', () => {
workItemId,
workItemType,
fullPath,
+ queryVariables: {
+ id: workItemId,
+ },
+ fetchByIid,
},
stubs: {
GlDropdown,
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index ed90b11222a..635a1f326f8 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -41,6 +41,7 @@ export const workItemQueryResponse = {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Test',
state: 'OPEN',
description: 'description',
@@ -113,6 +114,7 @@ export const updateWorkItemMutationResponse = {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -178,6 +180,19 @@ export const mockParent = {
},
};
+export const descriptionTextWithCheckboxes = `- [ ] todo 1\n- [ ] todo 2`;
+
+export const descriptionHtmlWithCheckboxes = `
+ <ul dir="auto" class="task-list" data-sourcepos"1:1-2:12">
+ <li class="task-list-item" data-sourcepos="1:1-1:11">
+ <input class="task-list-item-checkbox" type="checkbox"> todo 1
+ </li>
+ <li class="task-list-item" data-sourcepos="2:1-2:12">
+ <input class="task-list-item-checkbox" type="checkbox"> todo 2
+ </li>
+ </ul>
+`;
+
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
@@ -193,12 +208,14 @@ export const workItemResponseFactory = ({
allowsScopedLabels = false,
lastEditedAt = null,
lastEditedBy = null,
+ withCheckboxes = false,
parent = mockParent.parent,
} = {}) => ({
data: {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: 1,
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -224,9 +241,10 @@ export const workItemResponseFactory = ({
{
__typename: 'WorkItemWidgetDescription',
type: 'DESCRIPTION',
- description: 'some **great** text',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
+ description: withCheckboxes ? descriptionTextWithCheckboxes : 'some **great** text',
+ descriptionHtml: withCheckboxes
+ ? descriptionHtmlWithCheckboxes
+ : '<p data-sourcepos="1:1-1:19" dir="auto">some <strong>great</strong> text</p>',
lastEditedAt,
lastEditedBy,
},
@@ -283,11 +301,12 @@ export const workItemResponseFactory = ({
milestoneWidgetPresent
? {
__typename: 'WorkItemWidgetMilestone',
- dueDate: null,
- expired: false,
- id: 'gid://gitlab/Milestone/30',
- title: 'v4.0',
type: 'MILESTONE',
+ milestone: {
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ },
}
: { type: 'MOCK TYPE' },
{
@@ -312,7 +331,8 @@ export const workItemResponseFactory = ({
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
- id: 'gid://gitlab/WorkItem/1',
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
workItemTypes: {
nodes: [
{ id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
@@ -331,6 +351,7 @@ export const createWorkItemMutationResponse = {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -368,6 +389,7 @@ export const createWorkItemFromTaskMutationResponse = {
__typename: 'WorkItem',
description: 'New description',
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
confidential: false,
@@ -405,6 +427,7 @@ export const createWorkItemFromTaskMutationResponse = {
newWorkItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1000000',
+ iid: '100',
title: 'Updated title',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
@@ -498,6 +521,28 @@ export const workItemTitleSubscriptionResponse = {
},
};
+export const workItemDescriptionSubscriptionResponse = {
+ data: {
+ issuableDescriptionUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetDescription',
+ type: 'DESCRIPTION',
+ description: 'New description',
+ descriptionHtml: '<p>New description</p>',
+ lastEditedAt: '2022-09-21T06:18:42Z',
+ lastEditedBy: {
+ id: 'gid://gitlab/User/2',
+ name: 'Someone else',
+ webPath: '/not-you',
+ },
+ },
+ ],
+ },
+ },
+};
+
export const workItemWeightSubscriptionResponse = {
data: {
issuableWeightUpdated: {
@@ -567,6 +612,25 @@ export const workItemIterationSubscriptionResponse = {
},
};
+export const workItemMilestoneSubscriptionResponse = {
+ data: {
+ issuableMilestoneUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ type: 'MILESTONE',
+ milestone: {
+ id: 'gid://gitlab/Milestone/1125',
+ expired: false,
+ title: 'Milestone title',
+ },
+ },
+ ],
+ },
+ },
+};
+
export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
@@ -776,6 +840,7 @@ export const changeWorkItemParentMutationResponse = {
},
description: null,
id: 'gid://gitlab/WorkItem/2',
+ iid: '2',
state: 'OPEN',
title: 'Foo',
confidential: false,
@@ -809,22 +874,20 @@ export const availableWorkItemsResponse = {
__typename: 'Project',
id: 'gid://gitlab/Project/2',
workItems: {
- edges: [
+ nodes: [
{
- node: {
- id: 'gid://gitlab/WorkItem/458',
- title: 'Task 1',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- },
+ id: 'gid://gitlab/WorkItem/458',
+ title: 'Task 1',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ __typename: 'WorkItem',
},
{
- node: {
- id: 'gid://gitlab/WorkItem/459',
- title: 'Task 2',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- },
+ id: 'gid://gitlab/WorkItem/459',
+ title: 'Task 2',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ __typename: 'WorkItem',
},
],
},
@@ -1072,7 +1135,7 @@ export const groupIterationsResponseWithNoIterations = {
};
export const mockMilestoneWidgetResponse = {
- dueDate: null,
+ state: 'active',
expired: false,
id: 'gid://gitlab/Milestone/30',
title: 'v4.0',
@@ -1122,3 +1185,14 @@ export const projectMilestonesResponseWithNoMilestones = {
},
},
};
+
+export const projectWorkItemResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItemQueryResponse.data.workItem],
+ },
+ },
+ },
+};
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index 15dac25b7d9..387c8a355fa 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -37,12 +37,17 @@ describe('Create work item component', () => {
props = {},
queryHandler = querySuccessHandler,
mutationHandler = createWorkItemSuccessHandler,
+ fetchByIid = false,
} = {}) => {
- fakeApollo = createMockApollo([
- [projectWorkItemTypesQuery, queryHandler],
- [createWorkItemMutation, mutationHandler],
- [createWorkItemFromTaskMutation, mutationHandler],
- ]);
+ fakeApollo = createMockApollo(
+ [
+ [projectWorkItemTypesQuery, queryHandler],
+ [createWorkItemMutation, mutationHandler],
+ [createWorkItemFromTaskMutation, mutationHandler],
+ ],
+ {},
+ { typePolicies: { Project: { merge: true } } },
+ );
wrapper = shallowMount(CreateWorkItem, {
apolloProvider: fakeApollo,
data() {
@@ -61,6 +66,9 @@ describe('Create work item component', () => {
},
provide: {
fullPath: 'full-path',
+ glFeatures: {
+ useIidInWorkItemsPath: fetchByIid,
+ },
},
});
};
@@ -99,7 +107,12 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
- expect(wrapper.vm.$router.push).toHaveBeenCalled();
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ name: 'workItem',
+ params: {
+ id: '1',
+ },
+ });
});
it('adds right margin for create button', () => {
@@ -197,4 +210,18 @@ describe('Create work item component', () => {
'Something went wrong when creating work item. Please try again.',
);
});
+
+ it('performs a correct redirect when `useIidInWorkItemsPath` feature flag is enabled', async () => {
+ createComponent({ fetchByIid: true });
+ findTitleInput().vm.$emit('title-input', 'Test title');
+
+ wrapper.find('form').trigger('submit');
+ await waitForPromises();
+
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ name: 'workItem',
+ params: { id: '1' },
+ query: { iid_path: 'true' },
+ });
+ });
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index d9372f2bcf0..880c4271024 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -55,6 +55,7 @@ describe('Work items root component', () => {
isModal: false,
workItemId: 'gid://gitlab/WorkItem/1',
workItemParentId: null,
+ iid: '1',
});
});
@@ -65,11 +66,15 @@ describe('Work items root component', () => {
deleteWorkItemHandler,
});
- findWorkItemDetail().vm.$emit('deleteWorkItem');
+ findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' });
await waitForPromises();
- expect(deleteWorkItemHandler).toHaveBeenCalled();
+ expect(deleteWorkItemHandler).toHaveBeenCalledWith({
+ input: {
+ id: '1',
+ },
+ });
expect(mockToastShow).toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
});
@@ -81,7 +86,7 @@ describe('Work items root component', () => {
deleteWorkItemHandler,
});
- findWorkItemDetail().vm.$emit('deleteWorkItem');
+ findWorkItemDetail().vm.$emit('deleteWorkItem', { workItemType: 'task', workItemId: '1' });
await waitForPromises();
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 66a917d8052..982f9f71f9e 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -10,6 +10,8 @@ import {
workItemTitleSubscriptionResponse,
workItemWeightSubscriptionResponse,
workItemLabelsSubscriptionResponse,
+ workItemMilestoneSubscriptionResponse,
+ workItemDescriptionSubscriptionResponse,
} from 'jest/work_items/mock_data';
import App from '~/work_items/components/app.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -17,6 +19,8 @@ import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subs
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
+import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
+import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
@@ -34,6 +38,12 @@ describe('Work items router', () => {
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
const labelsSubscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
+ const milestoneSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemMilestoneSubscriptionResponse);
+ const descriptionSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemDescriptionSubscriptionResponse);
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
@@ -47,6 +57,8 @@ describe('Work items router', () => {
[workItemTitleSubscription, titleSubscriptionHandler],
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
[workItemLabelsSubscription, labelsSubscriptionHandler],
+ [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [workItemDescriptionSubscription, descriptionSubscriptionHandler],
];
if (IS_EE) {
@@ -59,10 +71,19 @@ describe('Work items router', () => {
provide: {
fullPath: 'full-path',
issuesListPath: 'full-path/-/issues',
+ hasIssueWeightsFeature: false,
},
});
};
+ beforeEach(() => {
+ window.gon = {
+ features: {
+ workItemsMvc2: false,
+ },
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
window.location.hash = '';
@@ -74,7 +95,14 @@ describe('Work items router', () => {
expect(wrapper.findComponent(WorkItemsRoot).exists()).toBe(true);
});
+ it('does not render create work item page on `/new` route if `workItemsMvc2` feature flag is off', async () => {
+ await createComponent('/new');
+
+ expect(wrapper.findComponent(CreateWorkItem).exists()).toBe(false);
+ });
+
it('renders create work item page on `/new` route', async () => {
+ window.gon.features.workItemsMvc2 = true;
await createComponent('/new');
expect(wrapper.findComponent(CreateWorkItem).exists()).toBe(true);
diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb
index a4a643582f5..a54cb8a7988 100644
--- a/spec/graphql/graphql_triggers_spec.rb
+++ b/spec/graphql/graphql_triggers_spec.rb
@@ -3,76 +3,89 @@
require 'spec_helper'
RSpec.describe GraphqlTriggers do
+ let_it_be(:issuable, refind: true) { create(:work_item) }
+
describe '.issuable_assignees_updated' do
- it 'triggers the issuableAssigneesUpdated subscription' do
- assignees = create_list(:user, 2)
- issue = create(:issue, assignees: assignees)
+ let(:assignees) { create_list(:user, 2) }
+ before do
+ issuable.update!(assignees: assignees)
+ end
+
+ it 'triggers the issuableAssigneesUpdated subscription' do
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableAssigneesUpdated',
- { issuable_id: issue.to_gid },
- issue
+ { issuable_id: issuable.to_gid },
+ issuable
)
- GraphqlTriggers.issuable_assignees_updated(issue)
+ GraphqlTriggers.issuable_assignees_updated(issuable)
end
end
describe '.issuable_title_updated' do
it 'triggers the issuableTitleUpdated subscription' do
- work_item = create(:work_item)
-
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableTitleUpdated',
- { issuable_id: work_item.to_gid },
- work_item
+ { issuable_id: issuable.to_gid },
+ issuable
).and_call_original
- GraphqlTriggers.issuable_title_updated(work_item)
+ GraphqlTriggers.issuable_title_updated(issuable)
end
end
describe '.issuable_description_updated' do
it 'triggers the issuableDescriptionUpdated subscription' do
- work_item = create(:work_item)
-
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableDescriptionUpdated',
- { issuable_id: work_item.to_gid },
- work_item
+ { issuable_id: issuable.to_gid },
+ issuable
).and_call_original
- GraphqlTriggers.issuable_description_updated(work_item)
+ GraphqlTriggers.issuable_description_updated(issuable)
end
end
describe '.issuable_labels_updated' do
- it 'triggers the issuableLabelsUpdated subscription' do
- project = create(:project)
- labels = create_list(:label, 3, project: project)
- issue = create(:issue, labels: labels)
+ let(:labels) { create_list(:label, 3, project: create(:project)) }
+
+ before do
+ issuable.update!(labels: labels)
+ end
+ it 'triggers the issuableLabelsUpdated subscription' do
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableLabelsUpdated',
- { issuable_id: issue.to_gid },
- issue
+ { issuable_id: issuable.to_gid },
+ issuable
)
- GraphqlTriggers.issuable_labels_updated(issue)
+ GraphqlTriggers.issuable_labels_updated(issuable)
end
end
describe '.issuable_dates_updated' do
it 'triggers the issuableDatesUpdated subscription' do
- work_item = create(:work_item)
-
expect(GitlabSchema.subscriptions).to receive(:trigger).with(
'issuableDatesUpdated',
- { issuable_id: work_item.to_gid },
- work_item
+ { issuable_id: issuable.to_gid },
+ issuable
+ ).and_call_original
+
+ GraphqlTriggers.issuable_dates_updated(issuable)
+ end
+ end
+
+ describe '.issuable_milestone_updated' do
+ it 'triggers the issuableMilestoneUpdated subscription' do
+ expect(GitlabSchema.subscriptions).to receive(:trigger).with(
+ 'issuableMilestoneUpdated',
+ { issuable_id: issuable.to_gid },
+ issuable
).and_call_original
- GraphqlTriggers.issuable_dates_updated(work_item)
+ GraphqlTriggers.issuable_milestone_updated(issuable)
end
end
diff --git a/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb b/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb
index f47f1b9869e..2eccfd3409f 100644
--- a/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/bulk_delete_spec.rb
@@ -6,7 +6,6 @@ RSpec.describe Mutations::Ci::Runner::BulkDelete do
include GraphqlHelpers
let_it_be(:admin_user) { create(:user, :admin) }
- let_it_be(:user) { create(:user) }
let(:current_ctx) { { current_user: user } }
@@ -19,24 +18,15 @@ RSpec.describe Mutations::Ci::Runner::BulkDelete do
sync(resolve(described_class, args: mutation_params, ctx: current_ctx))
end
- context 'when the user cannot admin the runner' do
- let(:runner) { create(:ci_runner) }
- let(:mutation_params) do
- { ids: [runner.to_global_id] }
- end
-
- it 'generates an error' do
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) { response }
- end
- end
-
context 'when user can delete runners' do
+ let_it_be(:group) { create(:group) }
+
let(:user) { admin_user }
let!(:runners) do
- create_list(:ci_runner, 2, :instance)
+ create_list(:ci_runner, 2, :group, groups: [group])
end
- context 'when required arguments are missing' do
+ context 'when runner IDs are missing' do
let(:mutation_params) { {} }
context 'when admin mode is enabled', :enable_admin_mode do
@@ -47,43 +37,48 @@ RSpec.describe Mutations::Ci::Runner::BulkDelete do
end
context 'with runners specified by id' do
- let(:mutation_params) do
+ let!(:mutation_params) do
{ ids: runners.map(&:to_global_id) }
end
context 'when admin mode is enabled', :enable_admin_mode do
it 'deletes runners', :aggregate_failures do
- expect_next_instance_of(
- ::Ci::Runners::BulkDeleteRunnersService, { runners: runners }
- ) do |service|
- expect(service).to receive(:execute).once.and_call_original
- end
-
expect { response }.to change { Ci::Runner.count }.by(-2)
expect(response[:errors]).to be_empty
end
+ end
- context 'when runner list is is above limit' do
- before do
- stub_const('::Ci::Runners::BulkDeleteRunnersService::RUNNER_LIMIT', 1)
- end
-
- it 'only deletes up to the defined limit', :aggregate_failures do
- expect { response }.to change { Ci::Runner.count }
- .by(-::Ci::Runners::BulkDeleteRunnersService::RUNNER_LIMIT)
- expect(response[:errors]).to be_empty
- end
+ it 'ignores unknown keys from service response payload', :aggregate_failures do
+ expect_next_instance_of(
+ ::Ci::Runners::BulkDeleteRunnersService, { runners: runners, current_user: user }
+ ) do |service|
+ expect(service).to receive(:execute).once.and_return(
+ ServiceResponse.success(
+ payload: {
+ extra_key: 'extra_value',
+ deleted_count: 10,
+ deleted_ids: (1..10).to_a,
+ errors: []
+ }))
end
+
+ expect(response).not_to include(extra_key: 'extra_value')
end
+ end
+ end
- context 'when admin mode is disabled', :aggregate_failures do
- it 'returns error', :aggregate_failures do
- expect do
- expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
- response
- end
- end.not_to change { Ci::Runner.count }
- end
+ context 'when the user cannot delete the runner' do
+ let(:runner) { create(:ci_runner) }
+ let!(:mutation_params) do
+ { ids: [runner.to_global_id] }
+ end
+
+ context 'when user is admin and admin mode is not enabled' do
+ let(:user) { admin_user }
+
+ it 'returns error', :aggregate_failures do
+ expect { response }.not_to change { Ci::Runner.count }
+ expect(response[:errors]).to match_array("User does not have permission to delete any of the runners")
end
end
end
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
index ee65be1e085..098b7ac6aa4 100644
--- a/spec/graphql/mutations/ci/runner/update_spec.rb
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -7,8 +7,10 @@ RSpec.describe Mutations::Ci::Runner::Update do
let_it_be(:user) { create(:user) }
let_it_be(:project1) { create(:project) }
- let_it_be(:runner) do
- create(:ci_runner, :project, projects: [project1], active: true, locked: false, run_untagged: true)
+ let_it_be(:project2) { create(:project) }
+
+ let(:runner) do
+ create(:ci_runner, :project, projects: [project1, project2], active: true, locked: false, run_untagged: true)
end
let(:current_ctx) { { current_user: user } }
@@ -79,14 +81,14 @@ RSpec.describe Mutations::Ci::Runner::Update do
end
context 'with associatedProjects argument' do
- let_it_be(:project2) { create(:project) }
+ let_it_be(:project3) { create(:project) }
context 'with id set to project runner' do
let(:mutation_params) do
{
id: runner.to_global_id,
description: 'updated description',
- associated_projects: [project2.to_global_id.to_s]
+ associated_projects: [project3.to_global_id.to_s]
}
end
@@ -96,7 +98,7 @@ RSpec.describe Mutations::Ci::Runner::Update do
{
runner: runner,
current_user: admin_user,
- project_ids: [project2.id]
+ project_ids: [project3.id]
}
) do |service|
expect(service).to receive(:execute).and_call_original
@@ -110,7 +112,7 @@ RSpec.describe Mutations::Ci::Runner::Update do
expect(response[:runner]).to be_an_instance_of(Ci::Runner)
expect(response[:runner]).to have_attributes(expected_attributes)
expect(runner.reload).to have_attributes(expected_attributes)
- expect(runner.projects).to match_array([project1, project2])
+ expect(runner.projects).to match_array([project1, project3])
end
context 'with user not allowed to assign runner' do
@@ -124,7 +126,7 @@ RSpec.describe Mutations::Ci::Runner::Update do
{
runner: runner,
current_user: admin_user,
- project_ids: [project2.id]
+ project_ids: [project3.id]
}
) do |service|
expect(service).to receive(:execute).and_call_original
@@ -137,11 +139,39 @@ RSpec.describe Mutations::Ci::Runner::Update do
expect(response[:errors]).to match_array(['user not allowed to assign runner'])
expect(response[:runner]).to be_nil
expect(runner.reload).not_to have_attributes(expected_attributes)
- expect(runner.projects).to match_array([project1])
+ expect(runner.projects).to match_array([project1, project2])
end
end
end
+ context 'with an empty list of projects' do
+ let(:mutation_params) do
+ {
+ id: runner.to_global_id,
+ associated_projects: []
+ }
+ end
+
+ it 'removes project relationships', :aggregate_failures do
+ expect_next_instance_of(
+ ::Ci::Runners::SetRunnerAssociatedProjectsService,
+ {
+ runner: runner,
+ current_user: admin_user,
+ project_ids: []
+ }
+ ) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ response
+
+ expect(response[:errors]).to be_empty
+ expect(response[:runner]).to be_an_instance_of(Ci::Runner)
+ expect(runner.reload.projects).to contain_exactly(project1)
+ end
+ end
+
context 'with id set to instance runner' do
let(:instance_runner) { create(:ci_runner, :instance) }
let(:mutation_params) do
diff --git a/spec/graphql/mutations/commits/create_spec.rb b/spec/graphql/mutations/commits/create_spec.rb
index fd0c2c46b2e..2c452410cca 100644
--- a/spec/graphql/mutations/commits/create_spec.rb
+++ b/spec/graphql/mutations/commits/create_spec.rb
@@ -179,7 +179,7 @@ RSpec.describe Mutations::Commits::Create do
it 'returns errors' do
expect(mutated_commit).to be_nil
- expect(subject[:errors].to_s).to match(/3:UserCommitFiles: empty CommitMessage/)
+ expect(subject[:errors].to_s).to match(/empty CommitMessage/)
end
end
diff --git a/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
index 6bed3a752ed..3198419fb81 100644
--- a/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
+++ b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Mutations::ResolvesGroup do
end
end
- let(:context) { double }
+ let(:context) { {} }
subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
diff --git a/spec/graphql/mutations/container_repositories/destroy_spec.rb b/spec/graphql/mutations/container_repositories/destroy_spec.rb
index 97da7846339..9f3ff8da80b 100644
--- a/spec/graphql/mutations/container_repositories/destroy_spec.rb
+++ b/spec/graphql/mutations/container_repositories/destroy_spec.rb
@@ -20,11 +20,10 @@ RSpec.describe Mutations::ContainerRepositories::Destroy do
end
shared_examples 'destroying the container repository' do
- it 'destroys the container repistory' do
+ it 'marks the repository as delete_scheduled' do
expect(::Packages::CreateEventService)
.to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
- expect(DeleteContainerRepositoryWorker)
- .to receive(:perform_async).with(user.id, container_repository.id)
+ expect(DeleteContainerRepositoryWorker).not_to receive(:perform_async)
expect { subject }.to change { ::Packages::Event.count }.by(1)
expect(container_repository.reload.delete_scheduled?).to be true
@@ -56,6 +55,23 @@ RSpec.describe Mutations::ContainerRepositories::Destroy do
it_behaves_like params[:shared_examples_name]
end
+
+ context 'with container_registry_delete_repository_with_cron_worker disabled' do
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
+ end
+
+ it 'enqueues a removal job' do
+ expect(::Packages::CreateEventService)
+ .to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
+ expect(DeleteContainerRepositoryWorker)
+ .to receive(:perform_async).with(user.id, container_repository.id)
+
+ expect { subject }.to change { ::Packages::Event.count }.by(1)
+ expect(container_repository.reload.delete_scheduled?).to be true
+ end
+ end
end
end
end
diff --git a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb
index 9254d84b29c..aab21776a99 100644
--- a/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb
+++ b/spec/graphql/mutations/incident_management/timeline_event/create_spec.rb
@@ -6,6 +6,9 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
+ let_it_be(:timeline_event_tag) do
+ create(:incident_management_timeline_event_tag, project: project, name: 'Test tag 1')
+ end
let(:args) { { note: 'note', occurred_at: Time.current } }
@@ -39,6 +42,104 @@ RSpec.describe Mutations::IncidentManagement::TimelineEvent::Create do
it_behaves_like 'responding with an incident timeline errors',
errors: ["Occurred at can't be blank and Timeline text can't be blank"]
end
+
+ context 'when timeline event tags are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ timeline_event_tag_names: [timeline_event_tag.name.to_s]
+ }
+ end
+
+ it_behaves_like 'creating an incident timeline event'
+ end
+
+ context 'when predefined tags are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ timeline_event_tag_names: ['Start time']
+ }
+ end
+
+ it_behaves_like 'creating an incident timeline event'
+
+ it 'creates and sets the tag on the event' do
+ timeline_event = resolve[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags.by_names(['Start time']).count).to eq 1
+ end
+ end
+
+ context 'when predefined tags exist' do
+ let_it_be(:end_time_tag) do
+ create(:incident_management_timeline_event_tag, project: project, name: 'End time')
+ end
+
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ timeline_event_tag_names: ['End time']
+ }
+ end
+
+ it 'does not create a new tag' do
+ expect { resolve }.not_to change(IncidentManagement::TimelineEventTag, :count)
+ end
+ end
+
+ context 'when same tags are tried to be assigned to same timeline event' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ timeline_event_tag_names: ['Start time', 'Start time']
+ }
+ end
+
+ it 'only assigns the tag once on the event' do
+ timeline_event = resolve[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags.by_names(['Start time']).count).to eq(1)
+ expect(timeline_event.timeline_event_tags.count).to eq(1)
+ end
+ end
+
+ context 'with case-insentive tags' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ timeline_event_tag_names: ['tESt tAg 1']
+ }
+ end
+
+ it 'sets the tag on the event' do
+ timeline_event = resolve[:timeline_event]
+
+ expect(timeline_event.timeline_event_tags.by_names(['Test tag 1']).count).to eq(1)
+ end
+ end
+
+ context 'when non-existing tags are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ timeline_event_tag_names: ['other time']
+ }
+ end
+
+ it_behaves_like 'responding with an incident timeline errors',
+ errors: ["Following tags don't exist: [\"other time\"]"]
+
+ it 'does not create the timeline event' do
+ expect { resolve }.not_to change(IncidentManagement::TimelineEvent, :count)
+ end
+ end
end
it_behaves_like 'failing to create an incident timeline event'
diff --git a/spec/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb b/spec/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb
new file mode 100644
index 00000000000..57a32f7e4bc
--- /dev/null
+++ b/spec/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::IncidentManagement::TimelineEventTag::Create do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project) }
+
+ let(:args) { { name: 'Test tag 1' } }
+
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_incident_management_timeline_event_tag) }
+
+ describe '#resolve' do
+ subject(:resolve) { mutation_for(project, current_user).resolve(project_path: project.full_path, **args) }
+
+ context 'when user has permission to create timeline event tag' do
+ it 'adds the tag to the project' do
+ expect { resolve }.to change(IncidentManagement::TimelineEventTag, :count).by(1)
+ expect(project.incident_management_timeline_event_tags.by_names(['Test tag 1']).pluck_names)
+ .to match_array(['Test tag 1'])
+ end
+ end
+
+ context 'when TimelineEventTags::CreateService responds with an error' do
+ let(:args) { {} }
+
+ it 'returns errors' do
+ expect(resolve).to eq(timeline_event_tag: nil, errors: ["Name can't be blank and Name is invalid"])
+ end
+ end
+
+ context 'when user has no permissions to create tags on a project' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'raises an error' do
+ expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ private
+
+ def mutation_for(project, user)
+ described_class.new(object: project, context: { current_user: user }, field: nil)
+ end
+end
diff --git a/spec/graphql/mutations/security/ci_configuration/base_security_analyzer_spec.rb b/spec/graphql/mutations/security/ci_configuration/base_security_analyzer_spec.rb
index 668768189df..c081f9f0cd2 100644
--- a/spec/graphql/mutations/security/ci_configuration/base_security_analyzer_spec.rb
+++ b/spec/graphql/mutations/security/ci_configuration/base_security_analyzer_spec.rb
@@ -11,6 +11,6 @@ RSpec.describe Mutations::Security::CiConfiguration::BaseSecurityAnalyzer do
project = create(:project, :public, :repository)
project.add_developer(user)
- expect { mutation.resolve(project_path: project.full_path ) }.to raise_error(NotImplementedError)
+ expect { mutation.resolve(project_path: project.full_path) }.to raise_error(NotImplementedError)
end
end
diff --git a/spec/graphql/mutations/todos/restore_many_spec.rb b/spec/graphql/mutations/todos/restore_many_spec.rb
index d43f1c8a2e9..3235be8486e 100644
--- a/spec/graphql/mutations/todos/restore_many_spec.rb
+++ b/spec/graphql/mutations/todos/restore_many_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe Mutations::Todos::RestoreMany do
end
def restore_mutation(todos)
- mutation.resolve(ids: todos.map { |todo| global_id_of(todo) } )
+ mutation.resolve(ids: todos.map { |todo| global_id_of(todo) })
end
def expect_states_were_not_changed
diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
index ab1f19abaad..d346b7aee93 100644
--- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
let_it_be(:user) { create(:user) }
let_it_be(:unauth_user) { create(:user) }
- let_it_be(:user_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
+ let_it_be(:user_project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let_it_be(:group) { create(:group, :private) }
shared_examples_for 'group and project board list issues resolver' do
diff --git a/spec/graphql/resolvers/board_lists_resolver_spec.rb b/spec/graphql/resolvers/board_lists_resolver_spec.rb
index 2fb7c5c4717..0f6e51ebbd0 100644
--- a/spec/graphql/resolvers/board_lists_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_lists_resolver_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Resolvers::BoardListsResolver do
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:unauth_user) { create(:user) }
- let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
+ let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:project_label) { create(:label, project: project, name: 'Development') }
let_it_be(:group_label) { create(:group_label, group: group, name: 'Development') }
@@ -65,7 +65,7 @@ RSpec.describe Resolvers::BoardListsResolver do
it 'returns empty result if list is not found' do
external_group = create(:group, :private)
- external_board = create(:board, resource_parent: external_group )
+ external_board = create(:board, resource_parent: external_group)
external_label = create(:group_label, group: group)
external_list = create(:list, board: external_board, label: external_label)
diff --git a/spec/graphql/resolvers/board_resolver_spec.rb b/spec/graphql/resolvers/board_resolver_spec.rb
index 51a13850366..e83d2cbfd1f 100644
--- a/spec/graphql/resolvers/board_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_resolver_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Resolvers::BoardResolver do
end
context 'when project boards' do
- let(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
it_behaves_like 'group and project boards resolver'
end
diff --git a/spec/graphql/resolvers/boards_resolver_spec.rb b/spec/graphql/resolvers/boards_resolver_spec.rb
index 07d0902d3ba..aa78f0db188 100644
--- a/spec/graphql/resolvers/boards_resolver_spec.rb
+++ b/spec/graphql/resolvers/boards_resolver_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe Resolvers::BoardsResolver do
end
context 'when project boards' do
- let(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
it_behaves_like 'group and project boards resolver'
end
diff --git a/spec/graphql/resolvers/container_repositories_resolver_spec.rb b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
index 8cbb366f873..df0a98b1536 100644
--- a/spec/graphql/resolvers/container_repositories_resolver_spec.rb
+++ b/spec/graphql/resolvers/container_repositories_resolver_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Resolvers::ContainerRepositoriesResolver do
end
[:created_desc, :updated_asc, :name_desc].each do |order|
- context "#{order}" do
+ context order.to_s do
let(:args) { { sort: order } }
it { is_expected.to eq([sort_repository2, sort_repository]) }
@@ -51,7 +51,7 @@ RSpec.describe Resolvers::ContainerRepositoriesResolver do
end
[:created_asc, :updated_desc, :name_asc].each do |order|
- context "#{order}" do
+ context order.to_s do
let(:args) { { sort: order } }
it { is_expected.to eq([sort_repository, sort_repository2]) }
diff --git a/spec/graphql/resolvers/group_packages_resolver_spec.rb b/spec/graphql/resolvers/group_packages_resolver_spec.rb
index c600f9c9f9a..639d4d93b79 100644
--- a/spec/graphql/resolvers/group_packages_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_packages_resolver_spec.rb
@@ -21,10 +21,10 @@ RSpec.describe 'Resolvers::GroupPackagesResolver' do
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 ) }
+ 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' } }
diff --git a/spec/graphql/resolvers/incident_management/timeline_event_tags_resolver_spec.rb b/spec/graphql/resolvers/incident_management/timeline_event_tags_resolver_spec.rb
new file mode 100644
index 00000000000..8ab34e05e52
--- /dev/null
+++ b/spec/graphql/resolvers/incident_management/timeline_event_tags_resolver_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::IncidentManagement::TimelineEventTagsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:incident) { create(:incident, project: project) }
+
+ let_it_be(:timeline_event) do
+ create(:incident_management_timeline_event, project: project, incident: incident)
+ end
+
+ let_it_be(:timeline_event_with_no_tags) do
+ create(:incident_management_timeline_event, project: project, incident: incident)
+ end
+
+ let_it_be(:timeline_event_tag) do
+ create(:incident_management_timeline_event_tag, project: project)
+ end
+
+ let_it_be(:timeline_event_tag2) do
+ create(:incident_management_timeline_event_tag, project: project, name: 'Test tag 2')
+ end
+
+ let_it_be(:timeline_event_tag_link) do
+ create(:incident_management_timeline_event_tag_link,
+ timeline_event: timeline_event,
+ timeline_event_tag: timeline_event_tag)
+ end
+
+ let(:resolver) { described_class }
+
+ subject(:resolved_timeline_event_tags) do
+ sync(resolve_timeline_event_tags(timeline_event, current_user: current_user).to_a)
+ end
+
+ before do
+ project.add_guest(current_user)
+ end
+
+ specify do
+ expect(resolver).to have_nullable_graphql_type(
+ Types::IncidentManagement::TimelineEventTagType.connection_type
+ )
+ end
+
+ it 'returns timeline event tags', :aggregate_failures do
+ expect(resolved_timeline_event_tags.length).to eq(1)
+ expect(resolved_timeline_event_tags.first).to be_a(::IncidentManagement::TimelineEventTag)
+ end
+
+ context 'when timeline event is nil' do
+ subject(:resolved_timeline_event_tags) do
+ sync(resolve_timeline_event_tags(nil, current_user: current_user).to_a)
+ end
+
+ it 'returns no timeline event tags' do
+ expect(resolved_timeline_event_tags).to be_empty
+ end
+ end
+
+ context 'when there is no timeline event tag link' do
+ subject(:resolved_timeline_event_tags) do
+ sync(resolve_timeline_event_tags(timeline_event_with_no_tags, current_user: current_user).to_a)
+ end
+
+ it 'returns no timeline event tags' do
+ expect(resolved_timeline_event_tags).to be_empty
+ end
+ end
+
+ context 'when user does not have permissions' do
+ let(:non_member) { create(:user) }
+
+ subject(:resolved_timeline_event_tags) do
+ sync(resolve_timeline_event_tags(timeline_event, current_user: non_member).to_a)
+ end
+
+ it 'returns no timeline event tags' do
+ expect(resolved_timeline_event_tags).to be_empty
+ end
+ end
+
+ private
+
+ def resolve_timeline_event_tags(obj, context = { current_user: current_user })
+ resolve(resolver, obj: obj, args: {}, ctx: context, arg_style: :internal_prepared)
+ end
+end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/project_issues_resolver_spec.rb
index a74b2a3f18c..b2796ad9b18 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_issues_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::IssuesResolver do
+RSpec.describe Resolvers::ProjectIssuesResolver do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
@@ -87,7 +87,7 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
- context 'negated filtering' do
+ context 'when using negated filters' do
it 'returns issues matching the searched title after applying a negated filter' do
expect(resolve_issues(milestone_title: ['past milestone'], not: { milestone_wildcard_id: wildcard_upcoming })).to contain_exactly(issue6)
end
@@ -252,7 +252,7 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
- context 'filtering by reaction emoji' do
+ context 'when filtering by reaction emoji' do
let_it_be(:downvoted_issue) { create(:issue, project: project) }
let_it_be(:downvote_award) { create(:award_emoji, :downvote, user: current_user, awardable: downvoted_issue) }
@@ -273,7 +273,7 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
- context 'confidential issues' do
+ context 'when listing confidential issues' do
let_it_be(:confidential_issue1) { create(:issue, project: project, confidential: true) }
let_it_be(:confidential_issue2) { create(:issue, project: other_project, confidential: true) }
@@ -375,13 +375,13 @@ RSpec.describe Resolvers::IssuesResolver do
create(:issue_customer_relations_contact, issue: crm_issue3, contact: contact3)
end
- context 'contact' do
+ context 'when filtering by contact' do
it 'returns only the issues for the contact' do
expect(resolve_issues({ crm_contact_id: contact1.id })).to contain_exactly(crm_issue1)
end
end
- context 'organization' do
+ context 'when filtering by organization' do
it 'returns only the issues for the contact' do
expect(resolve_issues({ crm_organization_id: organization.id })).to contain_exactly(crm_issue1, crm_issue2)
end
diff --git a/spec/graphql/resolvers/projects_resolver_spec.rb b/spec/graphql/resolvers/projects_resolver_spec.rb
index 453fafb9590..77507474170 100644
--- a/spec/graphql/resolvers/projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects_resolver_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe Resolvers::ProjectsResolver do
context 'when no sort is provided' do
it 'returns projects in descending order by id' do
- is_expected.to match_array((visible_projecs + named_projects).sort_by { |p| p[:id] }.reverse )
+ is_expected.to match_array((visible_projecs + named_projects).sort_by { |p| p[:id] }.reverse)
end
end
end
diff --git a/spec/graphql/resolvers/recent_boards_resolver_spec.rb b/spec/graphql/resolvers/recent_boards_resolver_spec.rb
index 1afdcd42b4f..059e4a538fe 100644
--- a/spec/graphql/resolvers/recent_boards_resolver_spec.rb
+++ b/spec/graphql/resolvers/recent_boards_resolver_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Resolvers::RecentBoardsResolver do
end
context 'when project boards' do
- let_it_be(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let_it_be(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
it_behaves_like 'group and project recent boards resolver'
end
diff --git a/spec/graphql/resolvers/users_resolver_spec.rb b/spec/graphql/resolvers/users_resolver_spec.rb
index dda15303676..2ae1b53c40f 100644
--- a/spec/graphql/resolvers/users_resolver_spec.rb
+++ b/spec/graphql/resolvers/users_resolver_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Resolvers::UsersResolver do
context 'when both ids and usernames are passed ' do
it 'generates an error' do
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
- resolve_users( args: { ids: [user1.to_global_id.to_s], usernames: [user1.username] } )
+ resolve_users(args: { ids: [user1.to_global_id.to_s], usernames: [user1.username] })
end
end
end
@@ -31,7 +31,7 @@ RSpec.describe Resolvers::UsersResolver do
context 'when a set of IDs is passed' do
it 'returns those users' do
expect(
- resolve_users( args: { ids: [user1.to_global_id.to_s, user2.to_global_id.to_s] } )
+ resolve_users(args: { ids: [user1.to_global_id.to_s, user2.to_global_id.to_s] })
).to contain_exactly(user1, user2)
end
end
@@ -39,7 +39,7 @@ RSpec.describe Resolvers::UsersResolver do
context 'when a set of usernames is passed' do
it 'returns those users' do
expect(
- resolve_users( args: { usernames: [user1.username, user2.username] } )
+ resolve_users(args: { usernames: [user1.username, user2.username] })
).to contain_exactly(user1, user2)
end
end
@@ -49,16 +49,16 @@ RSpec.describe Resolvers::UsersResolver do
it 'returns only admins' do
expect(
- resolve_users( args: { admins: true }, ctx: { current_user: admin_user } )
+ resolve_users(args: { admins: true }, ctx: { current_user: admin_user })
).to contain_exactly(admin_user)
end
end
context 'when a search term is passed' do
it 'returns all users who match', :aggregate_failures do
- expect(resolve_users( args: { search: "some" } )).to contain_exactly(user1, user2)
- expect(resolve_users( args: { search: "123784" } )).to contain_exactly(user2)
- expect(resolve_users( args: { search: "someperson" } )).to contain_exactly(user1)
+ expect(resolve_users(args: { search: "some" })).to contain_exactly(user1, user2)
+ expect(resolve_users(args: { search: "123784" })).to contain_exactly(user2)
+ expect(resolve_users(args: { search: "someperson" })).to contain_exactly(user1)
end
end
diff --git a/spec/graphql/resolvers/work_item_resolver_spec.rb b/spec/graphql/resolvers/work_item_resolver_spec.rb
index c44ed395102..dacc6ac11d8 100644
--- a/spec/graphql/resolvers/work_item_resolver_spec.rb
+++ b/spec/graphql/resolvers/work_item_resolver_spec.rb
@@ -27,14 +27,6 @@ RSpec.describe Resolvers::WorkItemResolver do
end
end
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it { is_expected.to be_nil }
- end
end
private
diff --git a/spec/graphql/resolvers/work_items/types_resolver_spec.rb b/spec/graphql/resolvers/work_items/types_resolver_spec.rb
index 868f4566ad6..5121a105523 100644
--- a/spec/graphql/resolvers/work_items/types_resolver_spec.rb
+++ b/spec/graphql/resolvers/work_items/types_resolver_spec.rb
@@ -29,16 +29,6 @@ RSpec.describe Resolvers::WorkItems::TypesResolver do
expect(result.to_a).to contain_exactly(WorkItems::Type.default_by_type(:task))
end
end
-
- context 'when work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'returns nil' do
- expect(result).to be_nil
- end
- end
end
describe '#resolve' do
@@ -53,15 +43,5 @@ RSpec.describe Resolvers::WorkItems::TypesResolver do
it_behaves_like 'a work item type resolver'
end
-
- context 'when parent is not a group or project' do
- let(:object) { 'not a project/group' }
-
- it 'returns nil because of feature flag check' do
- result = resolve(described_class, obj: object, args: {})
-
- expect(result).to be_nil
- end
- end
end
end
diff --git a/spec/graphql/types/ci/pipeline_schedule_status_enum_spec.rb b/spec/graphql/types/ci/pipeline_schedule_status_enum_spec.rb
index d271e72b17f..dcf37df5070 100644
--- a/spec/graphql/types/ci/pipeline_schedule_status_enum_spec.rb
+++ b/spec/graphql/types/ci/pipeline_schedule_status_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Types::Ci::PipelineScheduleStatusEnum do
- specify { expect(described_class.graphql_name ).to eq('PipelineScheduleStatus') }
+ specify { expect(described_class.graphql_name).to eq('PipelineScheduleStatus') }
it 'exposes the status of a pipeline schedule' do
expect(described_class.values.keys).to match_array(%w[ACTIVE INACTIVE])
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index 9dee834d05f..5683b3f86c4 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -18,7 +18,10 @@ RSpec.describe Types::Ci::PipelineType do
]
if Gitlab.ee?
- expected_fields += %w[security_report_summary security_report_findings code_quality_reports dast_profile]
+ expected_fields += %w[
+ security_report_summary security_report_findings security_report_finding
+ code_quality_reports dast_profile
+ ]
end
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/commit_signature_interface_spec.rb b/spec/graphql/types/commit_signature_interface_spec.rb
new file mode 100644
index 00000000000..4962131d9b5
--- /dev/null
+++ b/spec/graphql/types/commit_signature_interface_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CommitSignature'] do
+ it 'exposes the expected fields' do
+ expect(described_class).to have_graphql_fields(:verification_status, :commit_sha, :project)
+ end
+
+ describe '.resolve_type' do
+ it 'resolves gpg signatures' do
+ expect(described_class.resolve_type(build(:gpg_signature), {})).to eq(
+ Types::CommitSignatures::GpgSignatureType)
+ end
+
+ it 'resolves x509 signatures' do
+ expect(described_class.resolve_type(build(:x509_commit_signature), {})).to eq(
+ Types::CommitSignatures::X509SignatureType)
+ end
+
+ it 'raises an error when type is not known' do
+ expect { described_class.resolve_type(Class, {}) }.to raise_error('Unsupported commit signature type')
+ end
+ end
+end
diff --git a/spec/graphql/types/commit_signatures/gpg_signature_type_spec.rb b/spec/graphql/types/commit_signatures/gpg_signature_type_spec.rb
new file mode 100644
index 00000000000..0b69ee169f2
--- /dev/null
+++ b/spec/graphql/types/commit_signatures/gpg_signature_type_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['GpgSignature'] do
+ specify { expect(described_class.graphql_name).to eq('GpgSignature') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:download_code) }
+
+ specify { expect(described_class).to include(Types::CommitSignatureInterface) }
+
+ it 'contains attributes related to GPG signatures' do
+ expect(described_class).to have_graphql_fields(
+ :user, :verification_status, :commit_sha, :project,
+ :gpg_key_user_name, :gpg_key_user_email, :gpg_key_primary_keyid
+ )
+ end
+end
diff --git a/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb b/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
new file mode 100644
index 00000000000..cb7ce19c9fc
--- /dev/null
+++ b/spec/graphql/types/commit_signatures/verification_status_enum_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['VerificationStatus'] do
+ specify { expect(described_class.graphql_name).to eq('VerificationStatus') }
+
+ it 'exposes all signature verification states' do
+ expect(described_class.values.keys)
+ .to match_array(%w[
+ UNVERIFIED UNVERIFIED_KEY VERIFIED
+ SAME_USER_DIFFERENT_EMAIL OTHER_USER UNKNOWN_KEY
+ MULTIPLE_SIGNATURES
+ ])
+ end
+end
diff --git a/spec/graphql/types/commit_signatures/x509_signature_type_spec.rb b/spec/graphql/types/commit_signatures/x509_signature_type_spec.rb
new file mode 100644
index 00000000000..e268bd5b3b4
--- /dev/null
+++ b/spec/graphql/types/commit_signatures/x509_signature_type_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['X509Signature'] do
+ specify { expect(described_class.graphql_name).to eq('X509Signature') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:download_code) }
+
+ specify { expect(described_class).to include(Types::CommitSignatureInterface) }
+
+ it 'contains attributes related to X.509 signatures' do
+ expect(described_class).to have_graphql_fields(
+ :user, :verification_status, :commit_sha, :project,
+ :x509_certificate
+ )
+ end
+end
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index fe8df15028d..561d165148b 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Commit'] do
specify { expect(described_class.graphql_name).to eq('Commit') }
- specify { expect(described_class).to require_graphql_authorizations(:download_code) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_code) }
specify { expect(described_class).to include(Types::TodoableInterface) }
@@ -13,7 +13,7 @@ RSpec.describe GitlabSchema.types['Commit'] do
expect(described_class).to have_graphql_fields(
:id, :sha, :short_id, :title, :full_title, :full_title_html, :description, :description_html, :message, :title_html, :authored_date,
:author_name, :author_email, :author_gravatar, :author, :web_url, :web_path,
- :pipelines, :signature_html
+ :pipelines, :signature_html, :signature
)
end
end
diff --git a/spec/graphql/types/deployment_details_type_spec.rb b/spec/graphql/types/deployment_details_type_spec.rb
index 70fdc38019e..7dc0c8f97ac 100644
--- a/spec/graphql/types/deployment_details_type_spec.rb
+++ b/spec/graphql/types/deployment_details_type_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['DeploymentDetails'] do
id iid ref tag tags sha created_at updated_at finished_at status commit job triggerer
]
- expect(described_class).to have_graphql_fields(*expected_fields)
+ expect(described_class).to include_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_deployment) }
diff --git a/spec/graphql/types/incident_management/timeline_event_tag_type_spec.rb b/spec/graphql/types/incident_management/timeline_event_tag_type_spec.rb
new file mode 100644
index 00000000000..831a598ab66
--- /dev/null
+++ b/spec/graphql/types/incident_management/timeline_event_tag_type_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['TimelineEventTagType'] do
+ specify { expect(described_class.graphql_name).to eq('TimelineEventTagType') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_incident_management_timeline_event_tag) }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ id
+ name
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/incident_management/timeline_event_type_spec.rb b/spec/graphql/types/incident_management/timeline_event_type_spec.rb
index 5a6bc461f20..6805e0cdc9b 100644
--- a/spec/graphql/types/incident_management/timeline_event_type_spec.rb
+++ b/spec/graphql/types/incident_management/timeline_event_type_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe GitlabSchema.types['TimelineEventType'] do
occurred_at
created_at
updated_at
+ timeline_event_tags
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/issue_type_enum_spec.rb b/spec/graphql/types/issue_type_enum_spec.rb
index 8f4b6f3bf74..cd1737c3ebb 100644
--- a/spec/graphql/types/issue_type_enum_spec.rb
+++ b/spec/graphql/types/issue_type_enum_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe Types::IssueTypeEnum do
specify { expect(described_class.graphql_name).to eq('IssueType') }
- it 'exposes all the existing issue type values' do
+ it 'exposes all the existing issue type values except key_result' do
expect(described_class.values.keys).to match_array(
- %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK]
+ %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE]
)
end
end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 2a0ae79b2c4..dc444f90627 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
fields = %i[id iid title description state reference author assignees updated_by participants labels milestone due_date
confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
- design_collection alert_management_alert severity current_user_todos moved moved_to
+ design_collection alert_management_alert alert_management_alerts severity current_user_todos moved moved_to
closed_as_duplicate_of create_note_email timelogs project_id customer_relations_contacts escalation_status]
fields.each do |field_name|
diff --git a/spec/graphql/types/permission_types/ci/runner_spec.rb b/spec/graphql/types/permission_types/ci/runner_spec.rb
index e5fbbb346e4..b4685794950 100644
--- a/spec/graphql/types/permission_types/ci/runner_spec.rb
+++ b/spec/graphql/types/permission_types/ci/runner_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Types::PermissionTypes::Ci::Runner do
it do
expected_permissions = [
- :read_runner, :update_runner, :delete_runner
+ :read_runner, :update_runner, :delete_runner, :assign_runner
]
expected_permissions.each do |permission|
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index b435f3ed5ff..30fabb8e9e2 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -36,7 +36,8 @@ RSpec.describe GitlabSchema.types['Project'] do
cluster_agent cluster_agents agent_configurations
ci_template timelogs merge_commit_template squash_commit_template work_item_types
recent_issue_boards ci_config_path_or_default packages_cleanup_policy ci_variables
- timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules
+ timelog_categories fork_targets branch_rules ci_config_variables pipeline_schedules languages
+ incident_management_timeline_event_tags
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -212,8 +213,8 @@ RSpec.describe GitlabSchema.types['Project'] do
it "returns the project's sast configuration for analyzer variables" do
analyzer = subject.dig('data', 'project', 'sastCiConfiguration', 'analyzers', 'nodes').first
- expect(analyzer['name']).to eq('bandit')
- expect(analyzer['label']).to eq('Bandit')
+ expect(analyzer['name']).to eq('brakeman')
+ expect(analyzer['label']).to eq('Brakeman')
expect(analyzer['enabled']).to eq(true)
end
@@ -290,14 +291,14 @@ RSpec.describe GitlabSchema.types['Project'] do
subject { described_class.fields['issue'] }
it { is_expected.to have_graphql_type(Types::IssueType) }
- it { is_expected.to have_graphql_resolver(Resolvers::IssuesResolver.single) }
+ it { is_expected.to have_graphql_resolver(Resolvers::ProjectIssuesResolver.single) }
end
describe 'issues field' do
subject { described_class.fields['issues'] }
it { is_expected.to have_graphql_type(Types::IssueType.connection_type) }
- it { is_expected.to have_graphql_resolver(Resolvers::IssuesResolver) }
+ it { is_expected.to have_graphql_resolver(Resolvers::ProjectIssuesResolver) }
end
describe 'merge_request field' do
@@ -508,6 +509,12 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_resolver(Resolvers::Ci::JobTokenScopeResolver) }
end
+ describe 'incident_management_timeline_event_tags field' do
+ subject { described_class.fields['incidentManagementTimelineEventTags'] }
+
+ it { is_expected.to have_graphql_type(Types::IncidentManagement::TimelineEventTagType) }
+ end
+
describe 'agent_configurations' do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
@@ -731,4 +738,114 @@ RSpec.describe GitlabSchema.types['Project'] do
end
end
end
+
+ describe 'timeline_event_tags' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) do
+ create(:project,
+ :private,
+ :repository,
+ creator_id: user.id,
+ namespace: user.namespace)
+ end
+
+ let_it_be(:tag1) do
+ create(:incident_management_timeline_event_tag,
+ project: project,
+ name: 'Tag 1')
+ end
+
+ let_it_be(:tag2) do
+ create(:incident_management_timeline_event_tag,
+ project: project,
+ name: 'Tag 2')
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ incidentManagementTimelineEventTags {
+ name
+ id
+ }
+ }
+ }
+ )
+ end
+
+ let(:tags) do
+ subject.dig('data', 'project', 'incidentManagementTimelineEventTags')
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'when user has permissions to read project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'contains timeline event tags' do
+ expect(tags.count).to eq(2)
+ expect(tags.first['name']).to eq(tag1.name)
+ expect(tags.last['name']).to eq(tag2.name)
+ end
+ end
+ end
+
+ describe 'languages' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) do
+ create(:project,
+ :private,
+ :repository,
+ creator_id: user.id,
+ namespace: user.namespace)
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ languages {
+ name
+ share
+ color
+ }
+ }
+ }
+ )
+ end
+
+ let(:mock_languages) { [] }
+
+ before do
+ allow_next_instance_of(::Projects::RepositoryLanguagesService) do |service|
+ allow(service).to receive(:execute).and_return(mock_languages)
+ end
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ let(:languages) { subject.dig('data', 'project', 'languages') }
+
+ context "when the languages haven't been detected yet" do
+ it 'returns an empty array' do
+ expect(languages).to eq([])
+ end
+ end
+
+ context 'when the languages were detected before' do
+ let(:mock_languages) do
+ [{ share: 66.69, name: "Ruby", color: "#701516" },
+ { share: 22.98, name: "JavaScript", color: "#f1e05a" },
+ { share: 7.91, name: "HTML", color: "#e34c26" },
+ { share: 2.42, name: "CoffeeScript", color: "#244776" }]
+ end
+
+ it 'returns the repository languages' do
+ expect(languages).to eq(mock_languages.map(&:stringify_keys))
+ end
+ end
+ end
end
diff --git a/spec/graphql/types/projects/branch_rule_type_spec.rb b/spec/graphql/types/projects/branch_rule_type_spec.rb
index 119ecf8a097..54ea4f6857b 100644
--- a/spec/graphql/types/projects/branch_rule_type_spec.rb
+++ b/spec/graphql/types/projects/branch_rule_type_spec.rb
@@ -12,12 +12,13 @@ RSpec.describe GitlabSchema.types['BranchRule'] do
name
isDefault
branch_protection
+ matching_branches_count
created_at
updated_at
]
end
- specify { is_expected.to require_graphql_authorizations(:read_protected_branch) }
+ it { is_expected.to require_graphql_authorizations(:read_protected_branch) }
- specify { is_expected.to have_graphql_fields(fields).at_least }
+ it { is_expected.to have_graphql_fields(fields).at_least }
end
diff --git a/spec/graphql/types/projects/repository_language_type_spec.rb b/spec/graphql/types/projects/repository_language_type_spec.rb
new file mode 100644
index 00000000000..fd3e0ee4e90
--- /dev/null
+++ b/spec/graphql/types/projects/repository_language_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Projects::RepositoryLanguageType do
+ specify { expect(described_class.graphql_name).to eq('RepositoryLanguage') }
+
+ specify do
+ expect(described_class).to have_graphql_fields(
+ :name,
+ :share,
+ :color
+ )
+ end
+end
diff --git a/spec/graphql/types/release_links_type_spec.rb b/spec/graphql/types/release_links_type_spec.rb
index e77c4e3ddd1..5a29050a4a2 100644
--- a/spec/graphql/types/release_links_type_spec.rb
+++ b/spec/graphql/types/release_links_type_spec.rb
@@ -26,31 +26,31 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do
describe 'openedMergeRequestsUrl' do
it 'has valid authorization' do
- expect(fetch_authorizations('openedMergeRequestsUrl')).to include(:download_code)
+ expect(fetch_authorizations('openedMergeRequestsUrl')).to include(:read_code)
end
end
describe 'mergedMergeRequestsUrl' do
it 'has valid authorization' do
- expect(fetch_authorizations('mergedMergeRequestsUrl')).to include(:download_code)
+ expect(fetch_authorizations('mergedMergeRequestsUrl')).to include(:read_code)
end
end
describe 'closedMergeRequestsUrl' do
it 'has valid authorization' do
- expect(fetch_authorizations('closedMergeRequestsUrl')).to include(:download_code)
+ expect(fetch_authorizations('closedMergeRequestsUrl')).to include(:read_code)
end
end
describe 'openedIssuesUrl' do
it 'has valid authorization' do
- expect(fetch_authorizations('openedIssuesUrl')).to include(:download_code)
+ expect(fetch_authorizations('openedIssuesUrl')).to include(:read_code)
end
end
describe 'closedIssuesUrl' do
it 'has valid authorization' do
- expect(fetch_authorizations('closedIssuesUrl')).to include(:download_code)
+ expect(fetch_authorizations('closedIssuesUrl')).to include(:read_code)
end
end
diff --git a/spec/graphql/types/release_source_type_spec.rb b/spec/graphql/types/release_source_type_spec.rb
index 69a1ca30dbc..52f1e3a4ff5 100644
--- a/spec/graphql/types/release_source_type_spec.rb
+++ b/spec/graphql/types/release_source_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ReleaseSource'] do
- it { expect(described_class).to require_graphql_authorizations(:download_code) }
+ it { expect(described_class).to require_graphql_authorizations(:read_code) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb
index 5488d78b720..4ff2cbcad46 100644
--- a/spec/graphql/types/repository_type_spec.rb
+++ b/spec/graphql/types/repository_type_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Repository'] do
specify { expect(described_class.graphql_name).to eq('Repository') }
- specify { expect(described_class).to require_graphql_authorizations(:download_code) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_code) }
specify { expect(described_class).to have_graphql_field(:root_ref) }
diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb
index c23a14deaf3..04f0c72b06f 100644
--- a/spec/graphql/types/subscription_type_spec.rb
+++ b/spec/graphql/types/subscription_type_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['Subscription'] do
issuable_description_updated
issuable_labels_updated
issuable_dates_updated
+ issuable_milestone_updated
merge_request_reviewers_updated
merge_request_merge_status_updated
]
diff --git a/spec/graphql/types/x509_certificate_type_spec.rb b/spec/graphql/types/x509_certificate_type_spec.rb
new file mode 100644
index 00000000000..e59d1f83b28
--- /dev/null
+++ b/spec/graphql/types/x509_certificate_type_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['X509Certificate'] do
+ specify { expect(described_class.graphql_name).to eq('X509Certificate') }
+
+ it 'contains attributes for X.509 certifcates' do
+ expect(described_class).to have_graphql_fields(
+ :certificate_status, :created_at, :email, :id, :serial_number, :subject,
+ :subject_key_identifier, :updated_at, :x509_issuer
+ )
+ end
+end
diff --git a/spec/graphql/types/x509_issuer_type_spec.rb b/spec/graphql/types/x509_issuer_type_spec.rb
new file mode 100644
index 00000000000..5446dcf07c7
--- /dev/null
+++ b/spec/graphql/types/x509_issuer_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['X509Issuer'] do
+ specify { expect(described_class.graphql_name).to eq('X509Issuer') }
+
+ it 'contains attributes for X.509 issuers' do
+ expect(described_class).to have_graphql_fields(
+ :created_at, :crl_url, :id, :subject, :subject_key_identifier, :updated_at
+ )
+ end
+end
diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb
index 2206c1ce2ae..b3afd350397 100644
--- a/spec/helpers/appearances_helper_spec.rb
+++ b/spec/helpers/appearances_helper_spec.rb
@@ -169,4 +169,13 @@ RSpec.describe AppearancesHelper do
expect(helper.brand_title).to eq(helper.default_brand_title)
end
end
+
+ describe '#default_brand_title' do
+ it 'returns the default title' do
+ edition = Gitlab.ee? ? 'Enterprise' : 'Community'
+ expected_default_brand_title = "GitLab #{edition} Edition"
+
+ expect(helper.default_brand_title).to eq _(expected_default_brand_title)
+ end
+ end
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index a4b2c963c74..7f838167bd2 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -253,17 +253,44 @@ RSpec.describe ApplicationHelper do
end
describe '#client_class_list' do
- it 'returns string containing CSS classes representing client browser and platform' do
- class_list = helper.client_class_list
- expect(class_list).to eq('gl-browser-generic gl-platform-other')
+ context 'when browser or platform are unknown' do
+ it 'returns string containing CSS classes representing fallbacks' do
+ class_list = helper.client_class_list
+ expect(class_list).to eq('gl-browser-generic gl-platform-other')
+ end
+ end
+
+ context 'when browser and platform are known' do
+ before do
+ allow(helper.controller).to receive(:browser).and_return(::Browser.new('Google Chrome/Linux'))
+ end
+
+ it 'returns string containing CSS classes representing them' do
+ class_list = helper.client_class_list
+ expect(class_list).to eq('gl-browser-chrome gl-platform-linux')
+ end
end
end
describe '#client_js_flags' do
- it 'returns map containing JS flags representing client browser and platform' do
- flags_list = helper.client_js_flags
- expect(flags_list[:isGeneric]).to eq(true)
- expect(flags_list[:isOther]).to eq(true)
+ context 'when browser or platform are unknown' do
+ it 'returns map containing JS flags representing falllbacks' do
+ flags_list = helper.client_js_flags
+ expect(flags_list[:isGeneric]).to eq(true)
+ expect(flags_list[:isOther]).to eq(true)
+ end
+ end
+
+ context 'when browser and platform are known' do
+ before do
+ allow(helper.controller).to receive(:browser).and_return(::Browser.new('Google Chrome/Linux'))
+ end
+
+ it 'returns map containing JS flags representing client browser and platform' do
+ flags_list = helper.client_js_flags
+ expect(flags_list[:isChrome]).to eq(true)
+ expect(flags_list[:isLinux]).to eq(true)
+ end
end
end
@@ -506,42 +533,24 @@ RSpec.describe ApplicationHelper do
end
describe '#page_class' do
- context 'when logged_out_marketing_header experiment is enabled' do
- let_it_be(:expected_class) { 'logged-out-marketing-header-candidate' }
+ let_it_be(:expected_class) { 'logged-out-marketing-header' }
- let(:current_user) { nil }
- let(:variant) { :candidate }
+ let(:current_user) { nil }
- subject do
- helper.page_class.flatten
- end
-
- before do
- stub_experiments(logged_out_marketing_header: variant)
- allow(helper).to receive(:current_user) { current_user }
- end
-
- context 'when candidate' do
- it { is_expected.to include(expected_class) }
- end
-
- context 'when candidate (:trial_focused variant)' do
- let(:variant) { :trial_focused }
-
- it { is_expected.to include(expected_class) }
- end
+ subject do
+ helper.page_class.flatten
+ end
- context 'when control' do
- let(:variant) { :control }
+ before do
+ allow(helper).to receive(:current_user) { current_user }
+ end
- it { is_expected.not_to include(expected_class) }
- end
+ it { is_expected.to include(expected_class) }
- context 'when a user is logged in' do
- let(:current_user) { create(:user) }
+ context 'when a user is logged in' do
+ let(:current_user) { create(:user) }
- it { is_expected.not_to include(expected_class) }
- end
+ it { is_expected.not_to include(expected_class) }
end
end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index c75e9caa77a..eafdbfa8d0a 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -95,7 +95,7 @@ RSpec.describe ApplicationSettingsHelper do
end
describe '.self_monitoring_project_data' do
- context 'when self monitoring project does not exist' do
+ context 'when self-monitoring project does not exist' do
it 'returns create_self_monitoring_project_path' do
expect(helper.self_monitoring_project_data).to include(
'create_self_monitoring_project_path' =>
@@ -137,7 +137,7 @@ RSpec.describe ApplicationSettingsHelper do
end
end
- context 'when self monitoring project exists' do
+ context 'when self-monitoring project exists' do
let(:project) { build(:project) }
before do
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 93efce6b58b..78c0d0a2b11 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -445,6 +445,19 @@ RSpec.describe DiffHelper do
end
end
+ describe '#params_with_whitespace' do
+ before do
+ controller.params[:protocol] = 'HACKED!'
+ controller.params[:host] = 'HACKED!'
+ end
+
+ subject { helper.params_with_whitespace }
+
+ it "filters with safe_params" do
+ expect(subject).to eq({ 'w' => 1 })
+ end
+ end
+
describe "#render_fork_suggestion" do
subject { helper.render_fork_suggestion }
@@ -471,7 +484,6 @@ RSpec.describe DiffHelper do
describe '#conflicts' do
let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: true) }
- let(:merge_ref_head_diff) { true }
let(:can_be_resolved_in_ui?) { true }
let(:allow_tree_conflicts) { false }
let(:files) { [instance_double(Gitlab::Conflict::File, path: 'a')] }
@@ -479,7 +491,6 @@ RSpec.describe DiffHelper do
before do
allow(helper).to receive(:merge_request).and_return(merge_request)
- allow(helper).to receive(:options).and_return(merge_ref_head_diff: merge_ref_head_diff)
allow_next_instance_of(MergeRequests::Conflicts::ListService, merge_request, allow_tree_conflicts: allow_tree_conflicts) do |svc|
allow(svc).to receive(:can_be_resolved_in_ui?).and_return(can_be_resolved_in_ui?)
@@ -496,14 +507,6 @@ RSpec.describe DiffHelper do
expect(helper.conflicts).to eq('a' => files.first)
end
- context 'when merge_ref_head_diff option is false' do
- let(:merge_ref_head_diff) { false }
-
- it 'returns nil' do
- expect(helper.conflicts).to be_nil
- end
- end
-
context 'when merge request can be merged' do
let(:merge_request) { instance_double(MergeRequest, cannot_be_merged?: false) }
@@ -536,4 +539,42 @@ RSpec.describe DiffHelper do
end
end
end
+
+ describe '#show_only_context_commits?' do
+ let(:params) { {} }
+ let(:merge_request) { build_stubbed(:merge_request) }
+ let(:has_no_commits) { true }
+
+ subject(:result) { helper.show_only_context_commits? }
+
+ before do
+ assign(:merge_request, merge_request)
+ allow(helper).to receive(:params).and_return(params)
+ allow(merge_request).to receive(:has_no_commits?).and_return(has_no_commits)
+ end
+
+ context 'when only_context_commits param is set to true' do
+ let(:params) { { only_context_commits: true } }
+
+ it { is_expected.to be_truthy }
+
+ context 'when merge request has commits' do
+ let(:has_no_commits) { false }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when only_context_commits param is set to false' do
+ let(:params) { { only_context_commits: false } }
+
+ it { is_expected.to be_truthy }
+
+ context 'when merge request has commits' do
+ let(:has_no_commits) { false }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
end
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index c1eaf1b1bcd..cf33f8a4939 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe EnvironmentsHelper do
'default_branch' => 'master',
'project_path' => project_path(project),
'tags_path' => project_tags_path(project),
- 'has_metrics' => "#{environment.has_metrics?}",
+ 'has_metrics' => environment.has_metrics?.to_s,
'external_dashboard_url' => nil,
'environment_state' => environment.state,
'custom_metrics_path' => project_prometheus_metrics_path(project),
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 7005b3dc53e..39901047b0f 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -4,6 +4,30 @@ require 'spec_helper'
RSpec.describe EventsHelper do
include Gitlab::Routing
+ include Banzai::Filter::OutputSafety
+
+ describe '#link_to_author' do
+ let(:user) { create(:user) }
+ let(:event) { create(:event, author: user) }
+
+ it 'returns a link to the author' do
+ name = user.name
+ expect(helper.link_to_author(event)).to eq(link_to(name, user_path(user.username), title: name))
+ end
+
+ it 'returns the author name if the author is not present' do
+ event.author = nil
+
+ expect(helper.link_to_author(event)).to eq(escape_once(event.author_name))
+ end
+
+ it 'returns "You" if the author is the current user' do
+ allow(helper).to receive(:current_user).and_return(user)
+
+ name = _('You')
+ expect(helper.link_to_author(event, self_added: true)).to eq(link_to(name, user_path(user.username), title: name))
+ end
+ end
describe '#event_target_path' do
subject { helper.event_target_path(event.present) }
diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb
index 14ff5d97057..1797b0e32cd 100644
--- a/spec/helpers/form_helper_spec.rb
+++ b/spec/helpers/form_helper_spec.rb
@@ -44,43 +44,19 @@ RSpec.describe FormHelper do
describe '#assignees_dropdown_options' do
let(:merge_request) { build(:merge_request) }
- context "with the :limit_assignees_per_issuable feature flag on" do
- context "with multiple assignees" do
- it 'correctly returns the max amount of assignees to allow' do
- allow(helper).to receive(:merge_request_supports_multiple_assignees?).and_return(true)
+ context "with multiple assignees" do
+ it 'correctly returns the max amount of assignees to allow' do
+ allow(helper).to receive(:merge_request_supports_multiple_assignees?).and_return(true)
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS)
- end
- end
-
- context "with only 1 assignee" do
- it 'correctly returns the max amount of assignees to allow' do
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(1)
- end
+ expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
+ .to eq(Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS)
end
end
- context "with the :limit_assignees_per_issuable feature flag off" do
- before do
- stub_feature_flags(limit_assignees_per_issuable: false)
- end
-
- context "with multiple assignees" do
- it 'correctly returns the max amount of assignees to allow' do
- allow(helper).to receive(:merge_request_supports_multiple_assignees?).and_return(true)
-
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(nil)
- end
- end
-
- context "with only 1 assignee" do
- it 'correctly returns the max amount of assignees to allow' do
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(1)
- end
+ context "with only 1 assignee" do
+ it 'correctly returns the max amount of assignees to allow' do
+ expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
+ .to eq(1)
end
end
end
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
index 0d53225bbcf..4d1280533dd 100644
--- a/spec/helpers/groups/group_members_helper_spec.rb
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -7,16 +7,6 @@ RSpec.describe Groups::GroupMembersHelper do
let_it_be(:group) { create(:group) }
- describe '.group_member_select_options' do
- before do
- helper.instance_variable_set(:@group, group)
- end
-
- it 'returns an options hash' do
- expect(helper.group_member_select_options).to include(multiple: true, scope: :all, email_user: true)
- end
- end
-
describe '#group_members_app_data' do
include_context 'group_group_link'
@@ -36,6 +26,7 @@ RSpec.describe Groups::GroupMembersHelper do
allow(helper).to receive(:group_group_member_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
allow(helper).to receive(:can?).with(current_user, :admin_group_member, shared_group).and_return(true)
+ allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, shared_group).and_return(true)
end
subject do
@@ -63,7 +54,8 @@ RSpec.describe Groups::GroupMembersHelper do
it 'returns expected json' do
expected = {
source_id: shared_group.id,
- can_manage_members: true
+ can_manage_members: true,
+ can_manage_access_requests: true
}
expect(subject).to include(expected)
@@ -109,6 +101,7 @@ RSpec.describe Groups::GroupMembersHelper do
allow(helper).to receive(:group_group_member_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
allow(helper).to receive(:group_group_link_path).with(sub_shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
allow(helper).to receive(:can?).with(current_user, :admin_group_member, sub_shared_group).and_return(true)
+ allow(helper).to receive(:can?).with(current_user, :admin_member_access_request, sub_shared_group).and_return(true)
allow(helper).to receive(:can?).with(current_user, :export_group_memberships, sub_shared_group).and_return(true)
end
diff --git a/spec/helpers/groups/observability_helper_spec.rb b/spec/helpers/groups/observability_helper_spec.rb
new file mode 100644
index 00000000000..4393f4e9bec
--- /dev/null
+++ b/spec/helpers/groups/observability_helper_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Groups::ObservabilityHelper do
+ let(:group) { build_stubbed(:group) }
+ let(:observability_url) { Gitlab::Observability.observability_url }
+
+ describe '#observability_iframe_src' do
+ context 'if observability_path is missing from params' do
+ it 'returns the iframe src for action: dashboards' do
+ allow(helper).to receive(:params).and_return({ action: 'dashboards' })
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/")
+ end
+
+ it 'returns the iframe src for action: manage' do
+ allow(helper).to receive(:params).and_return({ action: 'manage' })
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/dashboards")
+ end
+
+ it 'returns the iframe src for action: explore' do
+ allow(helper).to receive(:params).and_return({ action: 'explore' })
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/explore")
+ end
+ end
+
+ context 'if observability_path exists in params' do
+ context 'if observability_path is valid' do
+ it 'returns the iframe src by injecting the observability path' do
+ allow(helper).to receive(:params).and_return({ action: '/explore', observability_path: '/foo?bar=foobar' })
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/#{group.id}/foo?bar=foobar")
+ end
+ end
+
+ context 'if observability_path is not valid' do
+ it 'returns the iframe src by injecting the sanitised observability path' do
+ allow(helper).to receive(:params).and_return({
+ action: '/explore',
+ observability_path:
+ "/test?groupId=<script>alert('attack!')</script>"
+ })
+ expect(helper.observability_iframe_src(group)).to eq(
+ "#{observability_url}/#{group.id}/test?groupId=alert('attack!')"
+ )
+ end
+ end
+ end
+
+ context 'when observability ui is standalone' do
+ before do
+ stub_env('STANDALONE_OBSERVABILITY_UI', 'true')
+ end
+
+ it 'returns the iframe src without group.id for action: dashboards' do
+ allow(helper).to receive(:params).and_return({ action: 'dashboards' })
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/")
+ end
+
+ it 'returns the iframe src without group.id for action: manage' do
+ allow(helper).to receive(:params).and_return({ action: 'manage' })
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/dashboards")
+ end
+
+ it 'returns the iframe src without group.id for action: explore' do
+ allow(helper).to receive(:params).and_return({ action: 'explore' })
+ expect(helper.observability_iframe_src(group)).to eq("#{observability_url}/explore")
+ end
+ end
+ end
+
+ describe '#observability_page_title' do
+ it 'returns the title for action: dashboards' do
+ allow(helper).to receive(:params).and_return({ action: 'dashboards' })
+ expect(helper.observability_page_title).to eq("Dashboards")
+ end
+
+ it 'returns the title for action: manage' do
+ allow(helper).to receive(:params).and_return({ action: 'manage' })
+ expect(helper.observability_page_title).to eq("Manage Dashboards")
+ end
+
+ it 'returns the title for action: explore' do
+ allow(helper).to receive(:params).and_return({ action: 'explore' })
+ expect(helper.observability_page_title).to eq("Explore")
+ end
+
+ it 'returns the default title for unknown action' do
+ allow(helper).to receive(:params).and_return({ action: 'unknown' })
+ expect(helper.observability_page_title).to eq("Dashboards")
+ end
+ end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index a38483a956d..8b4ac6a7cfd 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -287,39 +287,6 @@ RSpec.describe GroupsHelper do
end
end
- describe '#parent_group_options' do
- let_it_be(:current_user) { create(:user) }
- let_it_be(:group) { create(:group, name: 'group') }
- let_it_be(:group2) { create(:group, name: 'group2') }
-
- before do
- group.add_owner(current_user)
- group2.add_owner(current_user)
- end
-
- it 'includes explicitly owned groups except self' do
- expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json)
- end
-
- it 'excludes parent group' do
- subgroup = create(:group, parent: group2)
-
- expect(parent_group_options(subgroup)).to eq([{ id: group.id, text: group.human_name }].to_json)
- end
-
- it 'includes subgroups with inherited ownership' do
- subgroup = create(:group, parent: group)
-
- expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }, { id: subgroup.id, text: subgroup.human_name }].to_json)
- end
-
- it 'excludes own subgroups' do
- create(:group, parent: group2)
-
- expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json)
- end
- end
-
describe '#can_disable_group_emails?' do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, name: 'group') }
@@ -502,32 +469,6 @@ RSpec.describe GroupsHelper do
end
end
- describe '#subgroups_and_projects_list_app_data' do
- let_it_be(:group) { create(:group) }
- let_it_be(:user) { create(:user) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
-
- allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true }
- allow(helper).to receive(:can?).with(user, :create_projects, group) { true }
- end
-
- it 'returns expected hash' do
- expect(helper.subgroups_and_projects_list_app_data(group)).to match({
- show_schema_markup: 'true',
- new_subgroup_path: including("groups/new?parent_id=#{group.id}#create-group-pane"),
- new_project_path: including("/projects/new?namespace_id=#{group.id}"),
- new_subgroup_illustration: including('illustrations/subgroup-create-new-sm'),
- new_project_illustration: including('illustrations/project-create-new-sm'),
- empty_subgroup_illustration: including('illustrations/empty-state/empty-subgroup-md'),
- render_empty_state: 'true',
- can_create_subgroups: 'true',
- can_create_projects: 'true'
- })
- end
- end
-
describe '#group_overview_tabs_app_data' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
@@ -548,8 +489,17 @@ RSpec.describe GroupsHelper do
shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
current_group_visibility: group.visibility,
- initial_sort: initial_sort
- }.merge(helper.group_overview_tabs_app_data(group))
+ initial_sort: initial_sort,
+ show_schema_markup: 'true',
+ new_subgroup_path: including("groups/new?parent_id=#{group.id}#create-group-pane"),
+ new_project_path: including("/projects/new?namespace_id=#{group.id}"),
+ new_subgroup_illustration: including('illustrations/subgroup-create-new-sm'),
+ new_project_illustration: including('illustrations/project-create-new-sm'),
+ empty_subgroup_illustration: including('illustrations/empty-state/empty-subgroup-md'),
+ render_empty_state: 'true',
+ can_create_subgroups: 'true',
+ can_create_projects: 'true'
+ }
)
end
end
diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb
index 8f438a3ddc8..98a1f77b414 100644
--- a/spec/helpers/hooks_helper_spec.rb
+++ b/spec/helpers/hooks_helper_spec.rb
@@ -3,16 +3,33 @@
require 'spec_helper'
RSpec.describe HooksHelper do
- let(:project) { create(:project) }
- let(:project_hook) { create(:project_hook, project: project) }
- let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
- let(:system_hook) { create(:system_hook) }
+ let(:project) { build_stubbed(:project) }
+ let(:project_hook) { build_stubbed(:project_hook, project: project) }
+ let(:service_hook) { build_stubbed(:service_hook, integration: build_stubbed(:drone_ci_integration)) }
+ let(:system_hook) { build_stubbed(:system_hook) }
describe '#webhook_form_data' do
subject { helper.webhook_form_data(project_hook) }
- it { expect(subject[:url]).to eq(project_hook.url) }
- it { expect(subject[:url_variables]).to be_nil }
+ context 'when there are no URL variables' do
+ it 'returns proper data' do
+ expect(subject).to match(
+ url: project_hook.url,
+ url_variables: "[]"
+ )
+ end
+ end
+
+ context 'when there are URL variables' do
+ let(:project_hook) { build_stubbed(:project_hook, :url_variables, project: project) }
+
+ it 'returns proper data' do
+ expect(subject).to match(
+ url: project_hook.url,
+ url_variables: Gitlab::Json.dump([{ key: 'abc' }])
+ )
+ end
+ end
end
describe '#link_to_test_hook' do
@@ -31,7 +48,7 @@ RSpec.describe HooksHelper do
describe '#hook_log_path' do
context 'with a project hook' do
- let(:web_hook_log) { create(:web_hook_log, web_hook: project_hook) }
+ let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: project_hook) }
it 'returns project-namespaced link' do
expect(helper.hook_log_path(project_hook, web_hook_log))
@@ -40,7 +57,7 @@ RSpec.describe HooksHelper do
end
context 'with a service hook' do
- let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
+ let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: service_hook) }
it 'returns project-namespaced link' do
expect(helper.hook_log_path(project_hook, web_hook_log))
@@ -49,7 +66,7 @@ RSpec.describe HooksHelper do
end
context 'with a system hook' do
- let(:web_hook_log) { create(:web_hook_log, web_hook: system_hook) }
+ let(:web_hook_log) { build_stubbed(:web_hook_log, web_hook: system_hook) }
it 'returns admin-namespaced link' do
expect(helper.hook_log_path(system_hook, web_hook_log))
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 139e8be33d5..2f1682e9194 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -234,7 +234,7 @@ RSpec.describe IconsHelper do
describe 'gl_loading_icon' do
it 'returns the default spinner markup' do
expect(gl_loading_icon.to_s)
- .to eq '<div class="gl-spinner-container" role="status"><span class="gl-spinner gl-spinner-dark gl-spinner-sm gl-vertical-align-text-bottom!" aria-label="Loading"></span></div>'
+ .to eq '<div class="gl-spinner-container" role="status"><span aria-label="Loading" class="gl-spinner gl-spinner-sm gl-spinner-dark gl-vertical-align-text-bottom!"></span></div>'
end
context 'when css_class is provided' do
diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb
index e750379f62d..447967fd345 100644
--- a/spec/helpers/ide_helper_spec.rb
+++ b/spec/helpers/ide_helper_spec.rb
@@ -26,6 +26,8 @@ RSpec.describe IdeHelper do
'can-use-new-web-ide' => 'true',
'use-new-web-ide' => 'true',
'user-preferences-path' => profile_preferences_path,
+ 'new-web-ide-help-page-path' =>
+ help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
'branch-name' => 'master',
'project-path' => project.path_with_namespace,
'csp-nonce' => 'test-csp-nonce'
diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb
index 95dfc51e8fd..9822f9fac05 100644
--- a/spec/helpers/integrations_helper_spec.rb
+++ b/spec/helpers/integrations_helper_spec.rb
@@ -150,4 +150,54 @@ RSpec.describe IntegrationsHelper do
end
end
end
+
+ describe '#integration_issue_type' do
+ using RSpec::Parameterized::TableSyntax
+ let_it_be(:issue) { create(:issue) }
+
+ where(:issue_type, :expected_i18n_issue_type) do
+ "issue" | _('Issue')
+ "incident" | _('Incident')
+ "test_case" | _('Test case')
+ "requirement" | _('Requirement')
+ "task" | _('Task')
+ end
+
+ with_them do
+ before do
+ issue.update!(issue_type: issue_type)
+ end
+
+ it "return the correct i18n issue type" do
+ expect(described_class.integration_issue_type(issue.issue_type)).to eq(expected_i18n_issue_type)
+ end
+ end
+
+ it "only consider these enumeration values are valid" do
+ expected_valid_types = %w[issue incident test_case requirement task objective key_result]
+ expect(Issue.issue_types.keys).to contain_exactly(*expected_valid_types)
+ end
+ end
+
+ describe '#integration_todo_target_type' do
+ using RSpec::Parameterized::TableSyntax
+ let!(:todo) { create(:todo, commit_id: '123') }
+
+ where(:target_type, :expected_i18n_target_type) do
+ "Commit" | _("Commit")
+ "Issue" | _("Issue")
+ "MergeRequest" | _("Merge Request")
+ 'Epic' | _('Epic')
+ DesignManagement::Design.name | _('design')
+ AlertManagement::Alert.name | _('alert')
+ end
+
+ with_them do
+ before do
+ todo.update!(target_type: target_type)
+ end
+
+ it { expect(described_class.integration_todo_target_type(todo.target_type)).to eq(expected_i18n_target_type) }
+ end
+ end
end
diff --git a/spec/helpers/json_helper_spec.rb b/spec/helpers/json_helper_spec.rb
new file mode 100644
index 00000000000..b9dfabb1b23
--- /dev/null
+++ b/spec/helpers/json_helper_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe JsonHelper do
+ let(:hash) { { "foo" => "bar" } }
+ let(:json) { '{"foo":"bar"}' }
+
+ describe ".json_generate" do
+ subject { helper.json_generate(hash) }
+
+ it "generates JSON" do
+ expect(subject).to eq(json)
+ end
+
+ it "calls the Gitlab::Json class" do
+ expect(Gitlab::Json).to receive(:generate).with(hash)
+
+ subject
+ end
+ end
+
+ describe ".json_parse" do
+ subject { helper.json_parse(json) }
+
+ it "parses JSON" do
+ expect(subject).to eq(hash)
+ end
+
+ it "calls the Gitlab::Json class" do
+ expect(Gitlab::Json).to receive(:parse).with(json)
+
+ subject
+ end
+ end
+end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index a2e34471324..0b3d400041c 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -332,8 +332,8 @@ RSpec.describe MarkupHelper do
context 'when file is Markdown' do
let(:extension) { 'md' }
- it 'renders using #markdown_unsafe helper method' do
- expect(helper).to receive(:markdown_unsafe).with('wiki content', context)
+ it 'renders using CommonMark method' do
+ expect(Banzai).to receive(:render).with('wiki content', context)
helper.render_wiki_content(wiki_page)
end
@@ -377,24 +377,16 @@ RSpec.describe MarkupHelper do
end
end
- context 'when file is Kramdown' do
+ context 'when file is R Markdown' do
let(:extension) { 'rmd' }
- let(:content) do
- <<-EOF
-{::options parse_block_html="true" /}
-
-<div>
-FooBar
-</div>
- EOF
- end
+ let(:content) { '## Header' }
- it 'renders using #markdown_unsafe helper method' do
- expect(helper).to receive(:markdown_unsafe).with(content, context)
+ it 'renders using CommonMark method' do
+ expect(Markup::RenderingService).to receive(:new).and_call_original
result = helper.render_wiki_content(wiki_page)
- expect(result).to be_empty
+ expect(result).to include('Header</h2>')
end
end
@@ -424,23 +416,9 @@ FooBar
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
end
- it 'delegates to #markdown_unsafe when file name corresponds to Markdown' do
- expect(Gitlab::MarkupHelper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
- expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
-
- expect(helper.markup('foo.md', content)).to eq('NOEL')
- end
-
- it 'delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc' do
- expect(Gitlab::MarkupHelper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
- expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
-
- expect(helper.markup('foo.adoc', content)).to eq('NOEL')
- end
-
it 'uses passed in rendered content' do
expect(Gitlab::MarkupHelper).not_to receive(:gitlab_markdown?)
- expect(helper).not_to receive(:markdown_unsafe)
+ expect(Markup::RenderingService).not_to receive(:execute)
expect(helper.markup('foo.md', content, rendered: '<p>NOEL</p>')).to eq('<p>NOEL</p>')
end
@@ -448,113 +426,18 @@ FooBar
it 'defaults to CommonMark' do
expect(helper.markup('foo.md', 'x^2')).to include('x^2')
end
- end
-
- describe '#markup_unsafe' do
- subject { helper.markup_unsafe(file_name, text, context) }
-
- let_it_be(:project_base) { create(:project, :repository) }
- let_it_be(:context) { { project: project_base } }
-
- let(:file_name) { 'foo.bar' }
- let(:text) { 'Noël' }
-
- context 'when text is missing' do
- let(:text) { nil }
-
- it 'returns an empty string' do
- is_expected.to eq('')
- end
- end
-
- context 'when rendering takes too long' do
- before do
- stub_const("MarkupHelper::RENDER_TIMEOUT", 0.1)
- allow(Gitlab::OtherMarkup).to receive(:render) { sleep(0.2) }
- end
-
- it 'times out' do
- expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(Timeout::Error),
- project_id: project.id, file_name: file_name
- )
-
- subject
- end
-
- context 'when markup_rendering_timeout is disabled' do
- it 'waits until the execution completes' do
- stub_feature_flags(markup_rendering_timeout: false)
-
- expect(Gitlab::RenderTimeout).not_to receive(:timeout)
-
- subject
- end
- end
- end
-
- context 'when file is a markdown file' do
- let(:file_name) { 'foo.md' }
- it 'returns html (rendered by Banzai)' do
- expected_html = '<p data-sourcepos="1:1-1:5" dir="auto">Noël</p>'
-
- expect(Banzai).to receive(:render).with(text, context) { expected_html }
-
- is_expected.to eq(expected_html)
- end
-
- context 'when renderer returns an error' do
- before do
- allow(Banzai).to receive(:render).and_raise(StandardError, "An error")
- end
-
- it 'returns html (rendered by ActionView:TextHelper)' do
- is_expected.to eq('<p>Noël</p>')
- end
-
- it 'logs the error' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(StandardError),
- project_id: project.id, file_name: 'foo.md'
- )
-
- subject
- end
- end
- end
-
- context 'when file is asciidoc file' do
- let(:file_name) { 'foo.adoc' }
-
- it 'returns html (rendered by Gitlab::Asciidoc)' do
- expected_html = "<div>\n<p>Noël</p>\n</div>"
-
- expect(Gitlab::Asciidoc).to receive(:render).with(text, context) { expected_html }
-
- is_expected.to eq(expected_html)
- end
- end
-
- context 'when file is a regular text file' do
- let(:file_name) { 'foo.txt' }
-
- it 'returns html (rendered by ActionView::TagHelper)' do
- is_expected.to eq('<pre class="plain-readme">Noël</pre>')
- end
- end
-
- context 'when file has an unknown type' do
- let(:file_name) { 'foo.tex' }
+ it 'sets additional context for Asciidoc' do
+ context = {}
+ assign(:commit, commit)
+ assign(:ref, 'ref')
+ assign(:path, 'path')
- it 'returns html (rendered by Gitlab::OtherMarkup)' do
- expected_html = 'Noël'
+ expect(Gitlab::Asciidoc).to receive(:render)
- expect(Gitlab::OtherMarkup).to receive(:render).with(file_name, text, context) { expected_html }
+ helper.markup('foo.adoc', content, context)
- is_expected.to eq(expected_html)
- end
+ expect(context).to include(commit: commit, ref: 'ref', requested_path: 'path')
end
end
diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb
index 9c396d6bf25..0d43cfaae90 100644
--- a/spec/helpers/nav/top_nav_helper_spec.rb
+++ b/spec/helpers/nav/top_nav_helper_spec.rb
@@ -27,11 +27,9 @@ RSpec.describe Nav::TopNavHelper do
let(:subject) { helper.top_nav_view_model(project: current_project, group: current_group) }
- let(:menu_title) { 'Menu' }
+ let(:menu_tooltip) { 'Main menu' }
before do
- stub_feature_flags(new_navbar_layout: false)
-
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 }
@@ -46,8 +44,8 @@ RSpec.describe Nav::TopNavHelper do
allow(helper).to receive(:dashboard_nav_link?).with(:activity) { with_activity }
end
- it 'has :menuTitle' do
- expect(subject[:menuTitle]).to eq(menu_title)
+ it 'has :menuTooltip' do
+ expect(subject[:menuTooltip]).to eq(menu_tooltip)
end
context 'when current_user is nil (anonymous)' do
@@ -108,7 +106,7 @@ RSpec.describe Nav::TopNavHelper do
let(:current_user) { user }
it 'has no menu items or views by default' do
- expect(subject).to eq({ menuTitle: menu_title,
+ expect(subject).to eq({ menuTooltip: menu_tooltip,
primary: [],
secondary: [],
shortcuts: [],
@@ -176,74 +174,6 @@ RSpec.describe Nav::TopNavHelper do
expect(projects_view[:linksSecondary]).to eq([])
end
- context 'when extra submenu options are not hidden' do
- before do
- stub_feature_flags(remove_extra_primary_submenu_options: false)
- end
-
- it 'has expected :linksPrimary' do
- expected_links_primary = [
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Your projects',
- **menu_data_tracking_attrs('your_projects')
- },
- href: '/dashboard/projects',
- id: 'your',
- title: 'Your projects'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Starred projects',
- **menu_data_tracking_attrs('starred_projects')
- },
- href: '/dashboard/projects/starred',
- id: 'starred',
- title: 'Starred projects'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Explore projects',
- **menu_data_tracking_attrs('explore_projects')
- },
- href: '/explore',
- id: 'explore',
- title: 'Explore projects'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Explore topics',
- **menu_data_tracking_attrs('explore_topics')
- },
- href: '/explore/projects/topics',
- id: 'topics',
- title: 'Explore topics'
- )
- ]
- expect(projects_view[:linksPrimary]).to eq(expected_links_primary)
- end
-
- it 'has expected :linksSecondary' do
- expected_links_secondary = [
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Create new project',
- **menu_data_tracking_attrs('create_new_project')
- },
- href: '/projects/new',
- id: 'create',
- title: 'Create new project'
- )
- ]
- expect(projects_view[:linksSecondary]).to eq(expected_links_secondary)
- end
- end
-
context 'with current nav as project' do
before do
helper.nav('project')
@@ -341,54 +271,6 @@ RSpec.describe Nav::TopNavHelper do
expect(groups_view[:linksSecondary]).to eq([])
end
- context 'when extra submenu options are not hidden' do
- before do
- stub_feature_flags(remove_extra_primary_submenu_options: false)
- end
-
- it 'has expected :linksPrimary' do
- expected_links_primary = [
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Your groups',
- **menu_data_tracking_attrs('your_groups')
- },
- href: '/dashboard/groups',
- id: 'your',
- title: 'Your groups'
- ),
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Explore groups',
- **menu_data_tracking_attrs('explore_groups')
- },
- href: '/explore/groups',
- id: 'explore',
- title: 'Explore groups'
- )
- ]
- expect(groups_view[:linksPrimary]).to eq(expected_links_primary)
- end
-
- it 'has expected :linksSecondary' do
- expected_links_secondary = [
- ::Gitlab::Nav::TopNavMenuItem.build(
- data: {
- qa_selector: 'menu_item_link',
- qa_title: 'Create group',
- **menu_data_tracking_attrs('create_group')
- },
- href: '/groups/new',
- id: 'create',
- title: 'Create group'
- )
- ]
- expect(groups_view[:linksSecondary]).to eq(expected_links_secondary)
- end
- end
-
context 'with external user' do
let(:current_user) { external_user }
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index a78a8add336..97b75ae5080 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -110,6 +110,7 @@ RSpec.describe Projects::AlertManagementHelper do
describe '#alert_management_detail_data' do
let(:alert_id) { 1 }
let(:issues_path) { project_issues_path(project) }
+ let(:details_alert_management_path) { details_project_alert_management_path(project, alert_id) }
let(:can_update_alert) { true }
before do
@@ -125,6 +126,7 @@ RSpec.describe Projects::AlertManagementHelper do
'project-path' => project_path,
'project-id' => project_id,
'project-issues-path' => issues_path,
+ 'project-alert-management-details-path' => details_alert_management_path,
'page' => 'OPERATIONS',
'can-update' => 'true'
)
diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb
new file mode 100644
index 00000000000..e4421ff7606
--- /dev/null
+++ b/spec/helpers/projects/ml/experiments_helper_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'rspec'
+
+require 'spec_helper'
+require 'mime/types'
+
+RSpec.describe Projects::Ml::ExperimentsHelper do
+ let_it_be(:project) { build(:project, :private) }
+ let_it_be(:experiment) { build(:ml_experiments, user_id: project.creator, project: project) }
+ let_it_be(:candidates) do
+ create_list(:ml_candidates, 2, experiment: experiment, user: project.creator).tap do |c|
+ c[0].params.create!([{ name: 'param1', value: 'p1' }, { name: 'param2', value: 'p2' }])
+ c[0].metrics.create!(
+ [{ name: 'metric1', value: 0.1 }, { name: 'metric2', value: 0.2 }, { name: 'metric3', value: 0.3 }]
+ )
+
+ c[1].params.create!([{ name: 'param2', value: 'p3' }, { name: 'param3', value: 'p4' }])
+ c[1].metrics.create!(name: 'metric3', value: 0.4)
+ end
+ end
+
+ describe '#candidates_table_items' do
+ subject { helper.candidates_table_items(candidates) }
+
+ it 'creates the correct model for the table' do
+ expected_value = [
+ { 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000' },
+ { 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000' }
+ ]
+
+ expect(Gitlab::Json.parse(subject)).to match_array(expected_value)
+ end
+ end
+
+ describe '#unique_logged_names' do
+ context 'when for params' do
+ subject { Gitlab::Json.parse(helper.unique_logged_names(candidates, &:params)) }
+
+ it { is_expected.to match_array(%w[param1 param2 param3]) }
+ end
+
+ context 'when latest_metrics is passed' do
+ subject { Gitlab::Json.parse(helper.unique_logged_names(candidates, &:latest_metrics)) }
+
+ it { is_expected.to match_array(%w[metric1 metric2 metric3]) }
+ end
+ end
+end
diff --git a/spec/helpers/projects/pipeline_helper_spec.rb b/spec/helpers/projects/pipeline_helper_spec.rb
index a70544ace1a..0d3466d6ed2 100644
--- a/spec/helpers/projects/pipeline_helper_spec.rb
+++ b/spec/helpers/projects/pipeline_helper_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Projects::PipelineHelper do
suite_endpoint: project_pipeline_test_path(project, pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(project, pipeline.sha),
has_test_report: pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)),
+ empty_dag_svg_path: match_asset_path('illustrations/empty-state/empty-dag-md.svg'),
empty_state_image_path: match_asset_path('illustrations/empty-state/empty-test-cases-lg.svg'),
artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg'),
tests_count: pipeline.test_report_summary.total[:count]
diff --git a/spec/helpers/projects/project_members_helper_spec.rb b/spec/helpers/projects/project_members_helper_spec.rb
index 844c33de635..f3201ce0e14 100644
--- a/spec/helpers/projects/project_members_helper_spec.rb
+++ b/spec/helpers/projects/project_members_helper_spec.rb
@@ -41,7 +41,8 @@ RSpec.describe Projects::ProjectMembersHelper do
it 'returns expected json' do
expected = {
source_id: project.id,
- can_manage_members: true
+ can_manage_members: true,
+ can_manage_access_requests: true
}.as_json
expect(subject).to include(expected)
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 07c2d50f70a..39b8b552672 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -969,7 +969,8 @@ RSpec.describe ProjectsHelper do
containerRegistryAccessLevel: project.project_feature.container_registry_access_level,
environmentsAccessLevel: project.project_feature.environments_access_level,
featureFlagsAccessLevel: project.project_feature.feature_flags_access_level,
- releasesAccessLevel: project.project_feature.releases_access_level
+ releasesAccessLevel: project.project_feature.releases_access_level,
+ infrastructureAccessLevel: project.project_feature.infrastructure_access_level
)
end
diff --git a/spec/helpers/recaptcha_helper_spec.rb b/spec/helpers/recaptcha_helper_spec.rb
index 2c327431437..d97712ce302 100644
--- a/spec/helpers/recaptcha_helper_spec.rb
+++ b/spec/helpers/recaptcha_helper_spec.rb
@@ -10,29 +10,12 @@ RSpec.describe RecaptchaHelper, type: :helper do
end
shared_examples 'Gitlab QA bypass' do
- context 'when GITLAB_QA_USER_AGENT env var is present' do
- using RSpec::Parameterized::TableSyntax
-
- where(:dot_com, :user_agent, :qa_user_agent, :result) do
- false | 'qa_user_agent' | 'qa_user_agent' | true
- true | nil | 'qa_user_agent' | true
- true | '' | 'qa_user_agent' | true
- true | 'qa_user_agent' | '' | true
- true | 'qa_user_agent' | nil | true
- true | 'qa_user_agent' | 'qa_user_agent' | false
+ context 'when it is a QA request' do
+ before do
+ allow(Gitlab::Qa).to receive(:request?).and_return(true)
end
- with_them do
- before do
- allow(Gitlab).to receive(:com?).and_return(dot_com)
- stub_env('GITLAB_QA_USER_AGENT', qa_user_agent)
-
- request_double = instance_double(ActionController::TestRequest, user_agent: user_agent)
- allow(helper).to receive(:request).and_return(request_double)
- end
-
- it { is_expected.to eq result }
- end
+ it { is_expected.to eq false }
end
end
diff --git a/spec/helpers/routing/packages_helper_spec.rb b/spec/helpers/routing/packages_helper_spec.rb
new file mode 100644
index 00000000000..fa5d34fed73
--- /dev/null
+++ b/spec/helpers/routing/packages_helper_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Routing::PackagesHelper do
+ describe '#package_path' do
+ let(:package) { build_stubbed(:package) }
+
+ it "creates package's path" do
+ expect(helper.package_path(package)).to eq("/#{package.project.full_path}/-/packages/#{package.id}")
+ end
+ end
+end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 20718ad2f48..192dfaa9caf 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -195,7 +195,7 @@ RSpec.describe SearchHelper do
expect(search_autocomplete_opts("Network").size).to eq(1)
expect(search_autocomplete_opts("Graph").size).to eq(1)
- allow(self).to receive(:can?).with(user, :download_code, @project).and_return(false)
+ allow(self).to receive(:can?).with(user, :read_code, @project).and_return(false)
expect(search_autocomplete_opts("Files").size).to eq(0)
expect(search_autocomplete_opts("Commits").size).to eq(0)
@@ -597,6 +597,7 @@ RSpec.describe SearchHelper do
'<script type="text/javascript">alert(\'Another XSS\');</script> test' | ' <span class="gl-text-gray-900 gl-font-weight-bold">test</span>'
'Lorem test ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec.' | 'Lorem <span class="gl-text-gray-900 gl-font-weight-bold">test</span> ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Don...'
'<img src="https://random.foo.com/test.png" width="128" height="128" />some image' | 'some image'
+ '<h2 data-sourcepos="11:1-11:26" dir="auto"><a id="user-content-additional-information" class="anchor" href="#additional-information" aria-hidden="true"></a>Additional information test:</h2><textarea data-update-url="/freepascal.org/fpc/source/-/issues/6163.json" dir="auto" data-testid="textarea" class="hidden js-task-list-field"></textarea>' | '<a class="anchor" href="#additional-information"></a>Additional information <span class="gl-text-gray-900 gl-font-weight-bold">test</span>:'
end
with_them do
@@ -705,112 +706,94 @@ RSpec.describe SearchHelper do
let(:user) { create(:user) }
let(:can_download) { false }
- let(:for_group) { false }
- let(:group) { nil }
- let(:group_metadata) { nil }
-
- let(:for_project) { false }
- let(:project) { nil }
- let(:project_metadata) { nil }
+ let_it_be(:group) { nil }
+ let_it_be(:project) { nil }
let(:scope) { nil }
- let(:code_search) { false }
+
let(:ref) { nil }
- let(:for_snippets) { false }
-
- let(:search_context) do
- instance_double(Gitlab::SearchContext,
- group: group,
- group_metadata: group_metadata,
- project: project,
- project_metadata: project_metadata,
- scope: scope,
- ref: ref)
- end
+ let(:snippet) { nil }
before do
- allow(self).to receive(:search_context).and_return(search_context)
+ @project = project
+ @group = group
+ @ref = ref
+ @snippet = snippet
+
allow(self).to receive(:current_user).and_return(user)
+ allow(self).to receive(:search_scope).and_return(scope)
allow(self).to receive(:can?).and_return(can_download)
+ end
- allow(search_context).to receive(:for_group?).and_return(for_group)
- allow(search_context).to receive(:for_project?).and_return(for_project)
+ context 'no group or project data' do
+ it 'does not add :group, :group_metadata, or :scope to hash' do
+ expect(header_search_context[:group]).to eq(nil)
+ expect(header_search_context[:group_metadata]).to eq(nil)
+ expect(header_search_context[:scope]).to eq(nil)
+ end
- allow(search_context).to receive(:code_search?).and_return(code_search)
- allow(search_context).to receive(:for_snippets?).and_return(for_snippets)
+ it 'does not add :project, :project_metadata, :code_search, or :ref' do
+ expect(header_search_context[:project]).to eq(nil)
+ expect(header_search_context[:project_metadata]).to eq(nil)
+ expect(header_search_context[:code_search]).to eq(nil)
+ expect(header_search_context[:ref]).to eq(nil)
+ end
end
context 'group data' do
- let(:group) { create(:group) }
- let(:group_metadata) { { group_path: group.path, issues_path: "/issues" } }
+ let_it_be(:group) { create(:group) }
+ let(:group_metadata) { { issues_path: issues_group_path(group), mr_path: merge_requests_group_path(group) } }
let(:scope) { 'issues' }
- let(:code_search) { true }
-
- context 'when for_group? is true' do
- let(:for_group) { true }
-
- it 'adds the :group and :group_metadata correctly to hash' do
- expect(header_search_context[:group]).to eq({ id: group.id, name: group.name, full_name: group.full_name })
- expect(header_search_context[:group_metadata]).to eq(group_metadata)
- end
- it 'adds scope and code_search? correctly to hash' do
- expect(header_search_context[:scope]).to eq(scope)
- expect(header_search_context[:code_search]).to eq(code_search)
- end
+ it 'adds the :group, :group_metadata, and :scope correctly to hash' do
+ expect(header_search_context[:group]).to eq({ id: group.id, name: group.name, full_name: group.full_name })
+ expect(header_search_context[:group_metadata]).to eq(group_metadata)
+ expect(header_search_context[:scope]).to eq(scope)
end
- context 'when for_group? is false' do
- let(:for_group) { false }
-
- it 'does not add the :group and :group_metadata to hash' do
- expect(header_search_context[:group]).to eq(nil)
- expect(header_search_context[:group_metadata]).to eq(nil)
- end
-
- it 'does not add scope and code_search? to hash' do
- expect(header_search_context[:scope]).to eq(nil)
- expect(header_search_context[:code_search]).to eq(nil)
- end
+ it 'does not add :project, :project_metadata, :code_search, or :ref' do
+ expect(header_search_context[:project]).to eq(nil)
+ expect(header_search_context[:project_metadata]).to eq(nil)
+ expect(header_search_context[:code_search]).to eq(nil)
+ expect(header_search_context[:ref]).to eq(nil)
end
end
context 'project data' do
- let_it_be(:project) { create(:project) }
- let(:project_metadata) { { project_path: project.path, issues_path: "/issues" } }
- let(:scope) { 'issues' }
- let(:code_search) { true }
+ let_it_be(:project_group) { create(:group) }
+ let_it_be(:project) { create(:project, group: project_group) }
+ let(:project_metadata) { { issues_path: project_issues_path(project), mr_path: project_merge_requests_path(project) } }
+ let(:group_metadata) { { issues_path: issues_group_path(project_group), mr_path: merge_requests_group_path(project_group) } }
- context 'when for_project? is true' do
- let(:for_project) { true }
-
- it 'adds the :project and :project_metadata correctly to hash' do
- expect(header_search_context[:project]).to eq({ id: project.id, name: project.name })
- expect(header_search_context[:project_metadata]).to eq(project_metadata)
- end
+ it 'adds the :group and :group-metadata from the project correctly to hash' do
+ expect(header_search_context[:group]).to eq({ id: project_group.id, name: project_group.name, full_name: project_group.full_name })
+ expect(header_search_context[:group_metadata]).to eq(group_metadata)
+ end
- it 'adds scope and code_search? correctly to hash' do
- expect(header_search_context[:scope]).to eq(scope)
- expect(header_search_context[:code_search]).to eq(code_search)
- end
+ it 'adds the :project and :project-metadata correctly to hash' do
+ expect(header_search_context[:project]).to eq({ id: project.id, name: project.name })
+ expect(header_search_context[:project_metadata]).to eq(project_metadata)
end
- context 'when for_project? is false' do
- let(:for_project) { false }
+ context 'with scope' do
+ let(:scope) { 'issues' }
- it 'does not add the :project and :project_metadata to hash' do
- expect(header_search_context[:project]).to eq(nil)
- expect(header_search_context[:project_metadata]).to eq(nil)
+ it 'adds :scope and false :code_search to hash' do
+ expect(header_search_context[:scope]).to eq(scope)
+ expect(header_search_context[:code_search]).to eq(false)
end
+ end
- it 'does not add scope and code_search? to hash' do
+ context 'without scope' do
+ it 'adds code_search true to hash and not :scope' do
expect(header_search_context[:scope]).to eq(nil)
- expect(header_search_context[:code_search]).to eq(nil)
+ expect(header_search_context[:code_search]).to eq(true)
end
end
end
context 'ref data' do
+ let_it_be(:project) { create(:project) }
let(:ref) { 'test-branch' }
context 'when user can? download project data' do
@@ -830,20 +813,18 @@ RSpec.describe SearchHelper do
end
end
- context 'snippets' do
- context 'when for_snippets? is true' do
- let(:for_snippets) { true }
+ context 'snippet' do
+ context 'when searching from snippets' do
+ let(:snippet) { create(:snippet) }
- it 'adds :for_snippets correctly to hash' do
- expect(header_search_context[:for_snippets]).to eq(for_snippets)
+ it 'adds :for_snippets true correctly to hash' do
+ expect(header_search_context[:for_snippets]).to eq(true)
end
end
- context 'when for_snippets? is false' do
- let(:for_snippets) { false }
-
- it 'adds :for_snippets correctly to hash' do
- expect(header_search_context[:for_snippets]).to eq(for_snippets)
+ context 'when not searching from snippets' do
+ it 'adds :for_snippets nil correctly to hash' do
+ expect(header_search_context[:for_snippets]).to eq(nil)
end
end
end
@@ -880,8 +861,8 @@ RSpec.describe SearchHelper do
where(:feature_flag_tab_enabled, :show_elasticsearch_tabs, :project_search_tabs, :condition) do
false | false | false | false
true | true | true | true
- true | false | false | true
- false | true | false | true
+ true | false | false | false
+ false | true | false | false
false | false | true | true
true | false | true | true
end
@@ -1042,6 +1023,7 @@ RSpec.describe SearchHelper do
describe '.search_navigation_json' do
using RSpec::Parameterized::TableSyntax
+
context 'with data' do
example_data_1 = {
projects: { label: _("Projects"), condition: true },
@@ -1060,13 +1042,13 @@ RSpec.describe SearchHelper do
}
where(:data, :matcher) do
- example_data_1 | -> { include("projects") }
- example_data_2 | -> { eq("{}") }
- example_data_3 | -> { include("projects", "blobs", "epics") }
+ example_data_1 | -> { include("projects") }
+ example_data_2 | -> { eq("{}") }
+ example_data_3 | -> { include("projects", "blobs", "epics") }
end
with_them do
- it 'converts correctly' do
+ it 'renders data correctly' do
allow(self).to receive(:search_navigation).with(no_args).and_return(data)
expect(search_navigation_json).to instance_exec(&matcher)
@@ -1075,6 +1057,23 @@ RSpec.describe SearchHelper do
end
end
+ describe '.search_navigation_json with .search_navigation' do
+ before do
+ allow(self).to receive(:current_user).and_return(build(:user))
+ allow(self).to receive(:can?).and_return(true)
+ allow(self).to receive(:project_search_tabs?).and_return(true)
+ allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
+ allow(search_service).to receive(:show_elasticsearch_tabs?).and_return(true)
+ allow(self).to receive(:feature_flag_tab_enabled?).and_return(true)
+ @show_snippets = true
+ @project = nil
+ end
+
+ it 'test search navigation item order for CE all options enabled' do
+ expect(Gitlab::Json.parse(search_navigation_json).keys).to eq(%w[projects blobs issues merge_requests wiki_blobs commits notes milestones users snippet_titles])
+ end
+ end
+
describe '.search_filter_link_json' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
index 37520affc5a..48f67a291c4 100644
--- a/spec/helpers/snippets_helper_spec.rb
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe SnippetsHelper do
let(:snippet) { public_personal_snippet }
it 'returns copy button of embedded snippets' do
- expect(subject).to eq(copy_button("#{blob.id}"))
+ expect(subject).to eq(copy_button(blob.id.to_s))
end
end
@@ -82,7 +82,7 @@ RSpec.describe SnippetsHelper do
let(:snippet) { public_project_snippet }
it 'returns copy button of embedded snippets' do
- expect(subject).to eq(copy_button("#{blob.id}"))
+ expect(subject).to eq(copy_button(blob.id.to_s))
end
end
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index c64d5990cd9..7c91dd0570f 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -127,10 +127,22 @@ RSpec.describe TodosHelper do
context 'when given a task' do
let(:todo) { task_todo }
- it 'responds with an appropriate path' do
+ context 'when the use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
+ it 'responds with an appropriate path' do
+ path = helper.todo_target_path(todo)
+
+ expect(path).to eq("/#{todo.project.full_path}/-/work_items/#{todo.target.id}")
+ end
+ end
+
+ it 'responds with an appropriate path using iid' do
path = helper.todo_target_path(todo)
- expect(path).to eq("/#{todo.project.full_path}/-/work_items/#{todo.target.id}")
+ expect(path).to eq("/#{todo.project.full_path}/-/work_items/#{todo.target.iid}?iid_path=true")
end
end
@@ -310,4 +322,33 @@ RSpec.describe TodosHelper do
it { expect(helper.todos_filter_params[:state]).to eq(result) }
end
end
+
+ describe '#todo_action_name' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:action, :self_added?, :expected_action_name) do
+ Todo::ASSIGNED | false | s_('Todos|assigned you')
+ Todo::ASSIGNED | true | s_('Todos|assigned')
+ Todo::REVIEW_REQUESTED | true | s_('Todos|requested a review of')
+ Todo::MENTIONED | true | format(s_("Todos|mentioned %{who} on"), who: s_('Todos|yourself'))
+ Todo::MENTIONED | false | format(s_("Todos|mentioned %{who} on"), who: _('you'))
+ Todo::DIRECTLY_ADDRESSED | true | format(s_("Todos|mentioned %{who} on"), who: s_('Todos|yourself'))
+ Todo::DIRECTLY_ADDRESSED | false | format(s_("Todos|mentioned %{who} on"), who: _('you'))
+ Todo::BUILD_FAILED | true | s_('Todos|The pipeline failed in')
+ Todo::MARKED | true | s_('Todos|added a todo for')
+ Todo::APPROVAL_REQUIRED | true | format(s_("Todos|set %{who} as an approver for"), who: s_('Todos|yourself'))
+ Todo::APPROVAL_REQUIRED | false | format(s_("Todos|set %{who} as an approver for"), who: _('you'))
+ Todo::UNMERGEABLE | true | s_('Todos|Could not merge')
+ Todo::MERGE_TRAIN_REMOVED | true | s_("Todos|Removed from Merge Train:")
+ end
+
+ with_them do
+ before do
+ alert_todo.action = action
+ alert_todo.user = self_added? ? alert_todo.author : user
+ end
+
+ it { expect(helper.todo_action_name(alert_todo)).to eq(expected_action_name) }
+ end
+ end
end
diff --git a/spec/initializers/hashie_mash_permitted_patch_spec.rb b/spec/initializers/hashie_mash_permitted_patch_spec.rb
new file mode 100644
index 00000000000..0e9f8a485ff
--- /dev/null
+++ b/spec/initializers/hashie_mash_permitted_patch_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Hashie::Mash#permitted patch' do
+ let(:mash) { Hashie::Mash.new }
+
+ before do
+ load Rails.root.join('config/initializers/hashie_mash_permitted_patch.rb')
+ end
+
+ describe '#respond_to? with :permitted?' do
+ it 'returns false' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ { message: 'Hashie::Mash#respond_to?(:permitted?)', caller: instance_of(Array) })
+
+ expect(mash.respond_to?(:permitted?)).to be false
+ end
+ end
+
+ describe '#permitted' do
+ it 'raises ArgumentError' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ { message: 'Hashie::Mash#permitted?', caller: instance_of(Array) })
+
+ expect { mash.permitted? }.to raise_error(ArgumentError)
+ end
+ end
+end
diff --git a/spec/initializers/memory_watchdog_spec.rb b/spec/initializers/memory_watchdog_spec.rb
index 36f96131c3d..92834c889c2 100644
--- a/spec/initializers/memory_watchdog_spec.rb
+++ b/spec/initializers/memory_watchdog_spec.rb
@@ -3,6 +3,24 @@
require 'fast_spec_helper'
RSpec.describe 'memory watchdog' do
+ shared_examples 'starts configured watchdog' do |configure_monitor_method|
+ shared_examples 'configures and starts watchdog' do
+ it "correctly configures and starts watchdog", :aggregate_failures do
+ expect(Gitlab::Memory::Watchdog::Configurator).to receive(configure_monitor_method)
+
+ expect(Gitlab::Memory::Watchdog).to receive(:new).and_return(watchdog)
+ expect(Gitlab::BackgroundTask).to receive(:new).with(watchdog).and_return(background_task)
+ expect(background_task).to receive(:start)
+ expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_start).and_yield
+
+ run_initializer
+ end
+ end
+ end
+
+ let(:watchdog) { instance_double(Gitlab::Memory::Watchdog) }
+ let(:background_task) { instance_double(Gitlab::BackgroundTask) }
+
subject(:run_initializer) do
load rails_root_join('config/initializers/memory_watchdog.rb')
end
@@ -15,10 +33,6 @@ RSpec.describe 'memory watchdog' do
end
context 'when runtime is an application' do
- let(:watchdog) { instance_double(Gitlab::Memory::Watchdog) }
- let(:background_task) { instance_double(Gitlab::BackgroundTask) }
- let(:logger) { Gitlab::AppLogger }
-
before do
allow(Gitlab::Runtime).to receive(:application?).and_return(true)
end
@@ -29,100 +43,20 @@ RSpec.describe 'memory watchdog' do
run_initializer
end
- shared_examples 'starts configured watchdog' do |handler_class|
- let(:configuration) { Gitlab::Memory::Watchdog::Configuration.new }
- let(:watchdog_monitors_params) do
- {
- Gitlab::Memory::Watchdog::Monitor::HeapFragmentation => {
- max_heap_fragmentation: max_heap_fragmentation,
- max_strikes: max_strikes
- },
- Gitlab::Memory::Watchdog::Monitor::UniqueMemoryGrowth => {
- max_mem_growth: max_mem_growth,
- max_strikes: max_strikes
- }
- }
- end
-
- shared_examples 'configures and starts watchdog' do
- it "correctly configures and starts watchdog", :aggregate_failures do
- expect(watchdog).to receive(:configure).and_yield(configuration)
-
- watchdog_monitors_params.each do |monitor_class, params|
- expect(configuration.monitors).to receive(:use).with(monitor_class, **params)
- end
-
- expect(Gitlab::Memory::Watchdog).to receive(:new).and_return(watchdog)
- expect(Gitlab::BackgroundTask).to receive(:new).with(watchdog).and_return(background_task)
- expect(background_task).to receive(:start)
- expect(Gitlab::Cluster::LifecycleEvents).to receive(:on_worker_start).and_yield
-
- run_initializer
-
- expect(configuration.handler).to be_an_instance_of(handler_class)
- expect(configuration.logger).to eq(logger)
- expect(configuration.sleep_time_seconds).to eq(sleep_time_seconds)
- end
- end
-
- context 'when settings are not passed through the environment' do
- let(:max_strikes) { 5 }
- let(:max_heap_fragmentation) { 0.5 }
- let(:max_mem_growth) { 3.0 }
- let(:sleep_time_seconds) { 60 }
-
- include_examples 'configures and starts watchdog'
- end
-
- context 'when settings are passed through the environment' do
- let(:max_strikes) { 6 }
- let(:max_heap_fragmentation) { 0.4 }
- let(:max_mem_growth) { 2.0 }
- let(:sleep_time_seconds) { 50 }
-
- before do
- stub_env('GITLAB_MEMWD_MAX_STRIKES', 6)
- stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 50)
- stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 2.0)
- stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.4)
- end
-
- include_examples 'configures and starts watchdog'
- end
- end
-
- # In tests, the Puma constant does not exist so we cannot use a verified double.
- # rubocop: disable RSpec/VerifiedDoubles
context 'when puma' do
- let(:puma) do
- Class.new do
- def self.cli_config
- Struct.new(:options).new
- end
- end
- end
-
before do
- stub_const('Puma', puma)
- stub_const('Puma::Cluster::WorkerHandle', double.as_null_object)
-
allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
end
- it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::PumaHandler
+ it_behaves_like 'starts configured watchdog', :configure_for_puma
end
- # rubocop: enable RSpec/VerifiedDoubles
context 'when sidekiq' do
before do
allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
end
- it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::TermProcessHandler
- end
-
- context 'when other runtime' do
- it_behaves_like 'starts configured watchdog', Gitlab::Memory::Watchdog::NullHandler
+ it_behaves_like 'starts configured watchdog', :configure_for_sidekiq
end
end
@@ -157,10 +91,24 @@ RSpec.describe 'memory watchdog' do
allow(Gitlab::Runtime).to receive(:application?).and_return(true)
end
- it 'does not register life-cycle hook' do
- expect(Gitlab::Cluster::LifecycleEvents).not_to receive(:on_worker_start)
+ context 'when puma' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
+ end
- run_initializer
+ it_behaves_like 'starts configured watchdog', :configure_for_puma
+ end
+
+ context 'when sidekiq' do
+ before do
+ allow(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
+ end
+
+ it 'does not register life-cycle hook' do
+ expect(Gitlab::Cluster::LifecycleEvents).not_to receive(:on_worker_start)
+
+ run_initializer
+ end
end
end
end
diff --git a/spec/initializers/sawyer_patch_spec.rb b/spec/initializers/sawyer_patch_spec.rb
index b3c10e63460..9fcdc9583aa 100644
--- a/spec/initializers/sawyer_patch_spec.rb
+++ b/spec/initializers/sawyer_patch_spec.rb
@@ -1,36 +1,33 @@
# frozen_string_literal: true
+
require 'spec_helper'
require 'sawyer'
require_relative '../../config/initializers/sawyer_patch'
RSpec.describe 'sawyer_patch' do
- it 'raises error when acessing a method that overlaps a Ruby method' do
+ it 'raises error when acessing Sawyer Resource dynamic methods' do
sawyer_resource = Sawyer::Resource.new(
Sawyer::Agent.new(''),
{
to_s: 'Overriding method',
+ nil?: 'value',
+ login: 'Login',
user: { to_s: 'Overriding method', name: 'User name' }
}
)
- error_message = 'Sawyer method "to_s" overlaps Ruby method. Convert to a hash to access the attribute.'
- expect { sawyer_resource.to_s }.to raise_error(Sawyer::Error, error_message)
- expect { sawyer_resource.to_s? }.to raise_error(Sawyer::Error, error_message)
- expect { sawyer_resource.to_s = 'new value' }.to raise_error(Sawyer::Error, error_message)
- expect { sawyer_resource.user.to_s }.to raise_error(Sawyer::Error, error_message)
- expect(sawyer_resource.user.name).to eq('User name')
- end
-
- it 'raises error when acessing a boolean method that overlaps a Ruby method' do
- sawyer_resource = Sawyer::Resource.new(
- Sawyer::Agent.new(''),
- {
- nil?: 'value'
- }
- )
-
+ expect { sawyer_resource.to_s }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.to_s? }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.to_s = 'new value' }.to raise_error(Sawyer::Error)
expect { sawyer_resource.nil? }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.user.to_s }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.login }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.login? }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.login = 'New value' }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.user.name }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.user.name? }.to raise_error(Sawyer::Error)
+ expect { sawyer_resource.user.name = 'New value' }.to raise_error(Sawyer::Error)
end
it 'raises error when acessing a method that expects an argument' do
@@ -45,47 +42,8 @@ RSpec.describe 'sawyer_patch' do
}
)
- expect(sawyer_resource.user).to eq('value')
- expect { sawyer_resource.user = 'New user' }.to raise_error(ArgumentError)
expect { sawyer_resource == true }.to raise_error(ArgumentError)
expect { sawyer_resource != true }.to raise_error(ArgumentError)
expect { sawyer_resource + 1 }.to raise_error(ArgumentError)
end
-
- it 'does not raise error if is not an overlapping method' do
- sawyer_resource = Sawyer::Resource.new(
- Sawyer::Agent.new(''),
- {
- count_total: 1,
- user: { name: 'User name' }
- }
- )
-
- expect(sawyer_resource.count_total).to eq(1)
- expect(sawyer_resource.count_total?).to eq(true)
- expect(sawyer_resource.count_total + 1).to eq(2)
- sawyer_resource.count_total = 3
- expect(sawyer_resource.count_total).to eq(3)
- expect(sawyer_resource.user.name).to eq('User name')
- end
-
- it 'logs when a sawyer resource dynamic method is called' do
- sawyer_resource = Sawyer::Resource.new(
- Sawyer::Agent.new(''),
- {
- count_total: 1,
- user: { name: 'User name' }
- }
- )
- expected_attributes = []
- allow(Gitlab::Import::Logger).to receive(:warn) do |params|
- expected_attributes.push(params[:attribute])
- end
-
- sawyer_resource.count_total
- sawyer_resource.user
- sawyer_resource.user.name
-
- expect(expected_attributes).to match_array(%i[count_total user user name])
- 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 40f259b86e2..bb0e25d2613 100644
--- a/spec/lib/api/entities/merge_request_basic_spec.rb
+++ b/spec/lib/api/entities/merge_request_basic_spec.rb
@@ -18,12 +18,16 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
subject { entity.as_json }
- it 'includes basic fields' do
- is_expected.to include(
- draft: merge_request.draft?,
- work_in_progress: merge_request.draft?,
- merge_user: nil
- )
+ it 'includes expected fields' do
+ expected_fields = %i[
+ merged_by merge_user merged_at closed_by closed_at target_branch user_notes_count upvotes downvotes
+ author assignees assignee reviewers source_project_id target_project_id labels draft work_in_progress
+ milestone merge_when_pipeline_succeeds merge_status detailed_merge_status sha merge_commit_sha
+ squash_commit_sha discussion_locked should_remove_source_branch force_remove_source_branch
+ reference references web_url time_stats squash task_completion_status has_conflicts blocking_discussions_resolved
+ ]
+
+ is_expected.to include(*expected_fields)
end
context "with :with_api_entity_associations scope" do
diff --git a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
index 2a6d0825e5c..d5a37f53e21 100644
--- a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
+++ b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Entities::Ml::Mlflow::RunInfo do
let_it_be(:candidate) { create(:ml_candidates) }
- subject { described_class.new(candidate).as_json }
+ subject { described_class.new(candidate, packages_url: 'http://example.com').as_json }
context 'when start_time is nil' do
it { expect(subject[:start_time]).to eq(0) }
@@ -53,7 +53,7 @@ RSpec.describe API::Entities::Ml::Mlflow::RunInfo do
describe 'artifact_uri' do
it 'is not implemented' do
- expect(subject[:artifact_uri]).to eq('not_implemented')
+ expect(subject[:artifact_uri]).to eq("http://example.com#{candidate.artifact_root}")
end
end
diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb
index aa2c5126bb9..d1e5f191614 100644
--- a/spec/lib/api/entities/release_spec.rb
+++ b/spec/lib/api/entities/release_spec.rb
@@ -16,13 +16,13 @@ RSpec.describe API::Entities::Release do
end
describe 'evidences' do
- context 'when the current user can download code' do
+ context 'when the current user can read code' do
let(:entity_evidence) { entity[:evidences].first }
it 'exposes the evidence sha and the json path' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
- .with(user, :download_code, project).and_return(true)
+ .with(user, :read_code, project).and_return(true)
expect(entity_evidence[:sha]).to eq(evidence.summary_sha)
expect(entity_evidence[:collected_at]).to eq(evidence.collected_at)
@@ -36,11 +36,11 @@ RSpec.describe API::Entities::Release do
end
end
- context 'when the current user cannot download code' do
+ context 'when the current user cannot read code' do
it 'does not expose any evidence data' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
- .with(user, :download_code, project).and_return(false)
+ .with(user, :read_code, project).and_return(false)
expect(entity.keys).not_to include(:evidences)
end
diff --git a/spec/lib/api/entities/user_counts_spec.rb b/spec/lib/api/entities/user_counts_spec.rb
new file mode 100644
index 00000000000..0ed989ad7e9
--- /dev/null
+++ b/spec/lib/api/entities/user_counts_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::UserCounts do
+ let(:user) { build(:user) }
+
+ subject(:entity) { described_class.new(user).as_json }
+
+ it 'represents user counts', :aggregate_failures do
+ expect(user).to receive(:assigned_open_merge_requests_count).and_return(1).twice
+ expect(user).to receive(:assigned_open_issues_count).and_return(2).once
+ expect(user).to receive(:review_requested_open_merge_requests_count).and_return(3).once
+ expect(user).to receive(:todos_pending_count).and_return(4).once
+
+ expect(entity).to include(
+ merge_requests: 1,
+ assigned_issues: 2,
+ assigned_merge_requests: 1,
+ review_requested_merge_requests: 3,
+ todos: 4
+ )
+ end
+end
diff --git a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
index 66cf06cde20..582795acc4e 100644
--- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
include_context 'dependency proxy helpers context'
let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
let_it_be_with_reload(:package_setting) { create(:namespace_package_setting, namespace: group) }
let(:target) { project }
@@ -76,19 +76,6 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
end
end
- context 'when cascade_package_forwarding_settings is disabled' do
- let(:package_type) { forwardable_package_type }
- let(:forward_to_registry) { true }
-
- before do
- stub_feature_flags(cascade_package_forwarding_settings: false)
- allow_fetch_cascade_application_setting(attribute: "#{forwardable_package_type}_package_requests_forwarding", return_value: true)
- package_setting.update!("#{forwardable_package_type}_package_requests_forwarding" => false)
- end
-
- it_behaves_like 'executing redirect'
- end
-
context 'when no target is present' do
let(:package_type) { forwardable_package_type }
let(:forward_to_registry) { true }
@@ -133,7 +120,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
end
Packages::Package.package_types.keys.without('maven', 'npm', 'pypi').each do |pkg_type|
- context "#{pkg_type}" do
+ context pkg_type.to_s do
let(:package_type) { pkg_type.to_sym }
it 'raises an error' do
diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb
index d764ed4afff..b9c887b3e16 100644
--- a/spec/lib/api/helpers/packages_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages_helpers_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe API::Helpers::PackagesHelpers do
- let_it_be(:helper) { Class.new.include(described_class).new }
+ let_it_be(:helper) { Class.new.include(API::Helpers).include(described_class).new }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:package) { create(:package) }
@@ -121,4 +121,121 @@ RSpec.describe API::Helpers::PackagesHelpers do
expect(subject).to eq nil
end
end
+
+ describe '#user_project' do
+ before do
+ allow(helper).to receive(:params).and_return(id: project.id)
+ end
+
+ it 'calls find_project! on default action' do
+ expect(helper).to receive(:find_project!)
+
+ helper.user_project
+ end
+
+ it 'calls find_project! on read_project action' do
+ expect(helper).to receive(:find_project!)
+
+ helper.user_project(action: :read_project)
+ end
+
+ it 'calls user_project_with_read_package on read_package action' do
+ expect(helper).to receive(:user_project_with_read_package)
+
+ helper.user_project(action: :read_package)
+ end
+
+ it 'throws ArgumentError on unexpected action' do
+ expect { helper.user_project(action: :other_action) }.to raise_error(ArgumentError, 'unexpected action: other_action')
+ end
+ end
+
+ describe '#user_project_with_read_package' do
+ before do
+ helper.clear_memoization(:user_project_with_read_package)
+
+ allow(helper).to receive(:params).and_return(id: params_id)
+ allow(helper).to receive(:route_authentication_setting).and_return({ authenticate_non_public: true })
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:initial_current_user).and_return(user)
+ end
+
+ subject { helper.user_project_with_read_package }
+
+ context 'with non-existing project' do
+ let_it_be(:params_id) { non_existing_record_id }
+
+ context 'with current user' do
+ let_it_be(:user) { create(:user) }
+
+ it 'returns Not Found' do
+ expect(helper).to receive(:render_api_error!).with('404 Project Not Found', 404)
+
+ is_expected.to be_nil
+ end
+ end
+
+ context 'without current user' do
+ let_it_be(:user) { nil }
+
+ it 'returns Unauthorized' do
+ expect(helper).to receive(:render_api_error!).with('401 Unauthorized', 401)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'with existing project' do
+ let_it_be(:params_id) { project.id }
+
+ context 'with current user' do
+ let_it_be(:user) { create(:user) }
+
+ context 'as developer member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns project' do
+ is_expected.to eq(project)
+ end
+ end
+
+ context 'as guest member' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns Forbidden' do
+ expect(helper).to receive(:render_api_error!).with('403 Forbidden', 403)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'without current user' do
+ let_it_be(:user) { nil }
+
+ it 'returns Unauthorized' do
+ expect(helper).to receive(:render_api_error!).with('401 Unauthorized', 401)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'if no authorized project scope' do
+ let_it_be(:params_id) { project.id }
+ let_it_be(:user) { nil }
+
+ it 'returns Forbidden' do
+ expect(helper).to receive(:authorized_project_scope?).and_return(false)
+ expect(helper).to receive(:render_api_error!).with('403 Forbidden', 403)
+
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 652727f371b..d24a3bd13c0 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -798,7 +798,7 @@ RSpec.describe API::Helpers do
context 'with object storage' do
let(:artifact) { create(:ci_job_artifact, :zip, :remote_store) }
- subject { helper.present_artifacts_file!(artifact.file, project: artifact.job.project) }
+ subject { helper.present_artifacts_file!(artifact.file) }
before do
allow(helper).to receive(:env).and_return({})
@@ -830,7 +830,7 @@ RSpec.describe API::Helpers do
it 'retrieves a CDN-fronted URL' do
expect(artifact.file).to receive(:cdn_enabled_url).and_call_original
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact_used_cdn: false).and_call_original
- expect(helper.cdn_fronted_url(artifact.file, artifact.job.project)).to be_a(String)
+ expect(helper.cdn_fronted_url(artifact.file)).to be_a(String)
end
end
@@ -841,7 +841,7 @@ RSpec.describe API::Helpers do
file = double(url: url)
expect(Gitlab::ApplicationContext).not_to receive(:push)
- expect(helper.cdn_fronted_url(file, nil)).to eq(url)
+ expect(helper.cdn_fronted_url(file)).to eq(url)
end
end
end
diff --git a/spec/lib/api/validations/validators/email_or_email_list_spec.rb b/spec/lib/api/validations/validators/email_or_email_list_spec.rb
index ac3111c2319..17cfdf93cdc 100644
--- a/spec/lib/api/validations/validators/email_or_email_list_spec.rb
+++ b/spec/lib/api/validations/validators/email_or_email_list_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe API::Validations::Validators::EmailOrEmailList do
expect_no_validation_error('test' => 'test@example.org')
expect_no_validation_error('test' => 'test1@example.com,test2@example.org')
expect_no_validation_error('test' => 'test1@example.com,test2@example.org,test3@example.co.uk')
+ expect_no_validation_error('test' => %w[test1@example.com test2@example.org test3@example.co.uk])
end
end
@@ -23,6 +24,7 @@ RSpec.describe API::Validations::Validators::EmailOrEmailList do
expect_validation_error('test' => '@example.com')
expect_validation_error('test' => 'test1@example.com,asdf')
expect_validation_error('test' => 'asdf,testa1@example.com,asdf')
+ expect_validation_error('test' => %w[asdf testa1@example.com asdf])
end
end
end
diff --git a/spec/lib/api/validations/validators/git_ref_spec.rb b/spec/lib/api/validations/validators/git_ref_spec.rb
index 0d2d9e8f39a..6b9f5dee5fc 100644
--- a/spec/lib/api/validations/validators/git_ref_spec.rb
+++ b/spec/lib/api/validations/validators/git_ref_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe API::Validations::Validators::GitRef do
expect_validation_error('test' => 'heads/f[/bar')
expect_validation_error('test' => "heads/foo\t")
expect_validation_error('test' => "heads/foo\177")
- expect_validation_error('test' => "#{'a' * 1025}")
+ expect_validation_error('test' => 'a' * 1025)
expect_validation_error('test' => nil)
expect_validation_error('test' => '')
end
diff --git a/spec/lib/api/validations/validators/limit_spec.rb b/spec/lib/api/validations/validators/limit_spec.rb
index 0c10e2f74d2..11d808d390b 100644
--- a/spec/lib/api/validations/validators/limit_spec.rb
+++ b/spec/lib/api/validations/validators/limit_spec.rb
@@ -13,13 +13,13 @@ RSpec.describe API::Validations::Validators::Limit do
it 'does not raise a validation error' do
expect_no_validation_error('test' => '123-456')
expect_no_validation_error('test' => '00000000-ffff-0000-ffff-000000000000')
- expect_no_validation_error('test' => "#{'a' * 255}")
+ expect_no_validation_error('test' => 'a' * 255)
end
end
context 'longer than limit param' do
it 'raises a validation error' do
- expect_validation_error('test' => "#{'a' * 256}")
+ expect_validation_error('test' => 'a' * 256)
end
end
diff --git a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
index b3157dd15fb..86d672067a3 100644
--- a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
+++ b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb
@@ -16,7 +16,8 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
let(:jwt_headers) { { kid: public_key_id } }
let(:jwt) { JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers) }
let(:public_key) { private_key.public_key }
- let(:install_keys_url) { "https://connect-install-keys.atlassian.com/#{public_key_id}" }
+ let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' }
+ let(:install_keys_url) { "#{stub_asymmetric_jwt_cdn}/#{public_key_id}" }
let(:qsh) do
Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/installed', 'POST', 'https://gitlab.test')
end
@@ -85,6 +86,38 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do
it { is_expected.not_to be_valid }
end
+
+ context 'with jira_connect_proxy_url setting' do
+ let(:stub_asymmetric_jwt_cdn) { 'https://example.com/-/jira_connect/public_keys' }
+
+ before do
+ stub_application_setting(jira_connect_proxy_url: 'https://example.com')
+ end
+
+ it 'requests the settings CDN' do
+ expect(JWT).to receive(:decode).twice.and_call_original
+
+ expect(asymmetric_jwt).to be_valid
+
+ expect(WebMock).to have_requested(:get, "https://example.com/-/jira_connect/public_keys/#{public_key_id}")
+ end
+
+ context 'when jira_connect_oauth_self_managed disabled' do
+ let(:stub_asymmetric_jwt_cdn) { 'https://connect-install-keys.atlassian.com' }
+
+ before do
+ stub_feature_flags(jira_connect_oauth_self_managed: false)
+ end
+
+ it 'requests the default CDN' do
+ expect(JWT).to receive(:decode).twice.and_call_original
+
+ expect(asymmetric_jwt).to be_valid
+
+ expect(WebMock).to have_requested(:get, install_keys_url)
+ end
+ end
+ end
end
describe '#iss_claim' do
diff --git a/spec/lib/backup/database_backup_error_spec.rb b/spec/lib/backup/database_backup_error_spec.rb
index e001f65465c..2745d1540ea 100644
--- a/spec/lib/backup/database_backup_error_spec.rb
+++ b/spec/lib/backup/database_backup_error_spec.rb
@@ -19,12 +19,12 @@ RSpec.describe Backup::DatabaseBackupError do
it { is_expected.to respond_to :db_file_name }
it 'expects exception message to include database file' do
- expect(subject.message).to include("#{db_file_name}")
+ expect(subject.message).to include(db_file_name.to_s)
end
it 'expects exception message to include database paths being back-up' do
- expect(subject.message).to include("#{config[:host]}")
- expect(subject.message).to include("#{config[:port]}")
- expect(subject.message).to include("#{config[:database]}")
+ expect(subject.message).to include(config[:host].to_s)
+ expect(subject.message).to include(config[:port].to_s)
+ expect(subject.message).to include(config[:database].to_s)
end
end
diff --git a/spec/lib/backup/file_backup_error_spec.rb b/spec/lib/backup/file_backup_error_spec.rb
index bb174bbe4a0..948e15b7e5c 100644
--- a/spec/lib/backup/file_backup_error_spec.rb
+++ b/spec/lib/backup/file_backup_error_spec.rb
@@ -13,11 +13,11 @@ RSpec.describe Backup::FileBackupError do
it { is_expected.to respond_to :backup_tarball }
it 'expects exception message to include file backup path location' do
- expect(subject.message).to include("#{subject.backup_tarball}")
+ expect(subject.message).to include(subject.backup_tarball.to_s)
end
it 'expects exception message to include file being back-up' do
- expect(subject.message).to include("#{subject.app_files_dir}")
+ expect(subject.message).to include(subject.app_files_dir.to_s)
end
end
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index ba15860f3c9..75108130602 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -195,7 +195,7 @@ RSpec.describe Banzai::Filter::AutolinkFilter do
it 'escapes RTLO and other characters' do
# rendered text looks like "http://example.com/evilexe.mp3"
evil_link = "#{link}evil\u202E3pm.exe"
- doc = filter("#{evil_link}")
+ doc = filter(evil_link.to_s)
expect(doc.at_css('a')['href']).to eq "http://about.gitlab.com/evil%E2%80%AE3pm.exe"
end
diff --git a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
index ef23725c790..a11fe203541 100644
--- a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter do
link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
doc = filter(link, context.merge(project: other_project))
- expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)}")
+ expect(doc.css('a').last.text).to eq(closed_issue.to_reference(other_project).to_s)
end
it 'does not append state when filter is not enabled' do
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
index dd116eb1109..c5d2bcd5363 100644
--- a/spec/lib/banzai/filter/math_filter_spec.rb
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -97,7 +97,8 @@ RSpec.describe Banzai::Filter::MathFilter do
describe 'block display math using $$\n...\n$$ syntax' do
context 'with valid syntax' do
where(:text, :result_template) do
- "$$\n2+2\n$$" | "<math>2+2</math>"
+ "$$\n2+2\n$$" | "<math>2+2</math>"
+ "$$\n2+2\n3+4\n$$" | "<math>2+2\n3+4</math>"
end
with_them do
@@ -110,35 +111,35 @@ RSpec.describe Banzai::Filter::MathFilter do
describe 'display math using ```math...``` syntax' do
it 'adds data-math-style display attribute to display math' do
- doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
+ doc = filter('<pre lang="math"><code>2+2</code></pre>')
pre = doc.xpath('descendant-or-self::pre').first
expect(pre['data-math-style']).to eq 'display'
end
it 'adds js-render-math class to display math' do
- doc = filter('<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>')
+ doc = filter('<pre lang="math"><code>2+2</code></pre>')
pre = doc.xpath('descendant-or-self::pre').first
expect(pre[:class]).to include("js-render-math")
end
it 'ignores code blocks that are not math' do
- input = '<pre class="code highlight js-syntax-highlight language-plaintext" v-pre="true"><code>2+2</code></pre>'
+ input = '<pre lang="plaintext"><code>2+2</code></pre>'
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'requires the pre to contain both code and math' do
- input = '<pre class="highlight js-syntax-highlight language-plaintext language-math" v-pre="true"><code>2+2</code></pre>'
+ input = '<pre lang="math">something</pre>'
doc = filter(input)
expect(doc.to_s).to eq input
end
it 'dollar signs around to display math' do
- doc = filter('$<pre class="code highlight js-syntax-highlight language-math" v-pre="true"><code>2+2</code></pre>$')
+ doc = filter('$<pre lang="math"><code>2+2</code></pre>$')
before = doc.xpath('descendant-or-self::text()[1]').first
after = doc.xpath('descendant-or-self::text()[3]').first
diff --git a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
index cba41166be4..c1fdee48f12 100644
--- a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb
@@ -229,7 +229,7 @@ RSpec.describe Banzai::Filter::References::AlertReferenceFilter do
let(:alert2_reference) { alert2.to_reference(full: true) }
it 'does not have N+1 per multiple references per project', :use_sql_query_cache do
- markdown = "#{alert_reference}"
+ markdown = alert_reference.to_s
max_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
reference_filter(markdown)
end.count
diff --git a/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
index 6bcea41a603..c368a852ea9 100644
--- a/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/commit_reference_filter_spec.rb
@@ -282,7 +282,7 @@ RSpec.describe Banzai::Filter::References::CommitReferenceFilter do
let(:commit3_reference) { commit3.to_reference(full: true) }
it 'does not have N+1 per multiple references per project', :use_sql_query_cache do
- markdown = "#{commit_reference}"
+ markdown = commit_reference.to_s
max_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
reference_filter(markdown)
end.count
diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
index d17deaa4736..32538948b4b 100644
--- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
@@ -392,7 +392,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
context 'cross-project URL in link href' do
let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
- let(:reference) { "#{issue_url + "#note_123"}" }
+ let(:reference) { (issue_url + "#note_123").to_s }
let(:issue) { create(:issue, project: project2) }
let(:project2) { create(:project, :public, namespace: namespace) }
let(:namespace) { create(:namespace, name: 'cross-reference') }
@@ -497,7 +497,7 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
it 'links to a valid reference for cross-reference in link href' do
- reference = "#{issue_url + "#note_123"}"
+ reference = (issue_url + "#note_123").to_s
reference_link = %{<a href="#{reference}">Reference</a>}
doc = reference_filter("See #{reference_link}", context)
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 12cdb5cfb95..d5b9c71b861 100644
--- a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb
@@ -715,13 +715,13 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
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(:project_reference) { project_label.to_reference.to_s }
+ let_it_be(:project_reference2) { project_label2.to_reference.to_s }
+ let_it_be(:project2_reference) { project2_label.to_reference.to_s }
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}"
+ markdown = project_reference.to_s
control_count = 1
expect do
@@ -737,7 +737,7 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
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}"
+ markdown = project_reference.to_s
control_count = 1
expect do
diff --git a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
index c21a9339ebb..98090af06b1 100644
--- a/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb
@@ -490,13 +490,13 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do
let_it_be(:project_milestone2) { create(:milestone, project: project) }
let_it_be(:project2_milestone) { create(:milestone, project: project2) }
let_it_be(:group2_milestone) { create(:milestone, group: group2) }
- let_it_be(:project_reference) { "#{project_milestone.to_reference}" }
- let_it_be(:project_reference2) { "#{project_milestone2.to_reference}" }
- let_it_be(:project2_reference) { "#{project2_milestone.to_reference(full: true)}" }
+ let_it_be(:project_reference) { project_milestone.to_reference.to_s }
+ let_it_be(:project_reference2) { project_milestone2.to_reference.to_s }
+ let_it_be(:project2_reference) { project2_milestone.to_reference(full: true).to_s }
let_it_be(:group2_reference) { "#{project2.full_path}%\"#{group2_milestone.name}\"" }
it 'does not have N+1 per multiple references per project', :use_sql_query_cache do
- markdown = "#{project_reference}"
+ markdown = project_reference.to_s
control_count = 4
expect do
@@ -511,7 +511,7 @@ RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do
end
it 'has N+1 for multiple unique project/group references', :use_sql_query_cache do
- markdown = "#{project_reference}"
+ markdown = project_reference.to_s
control_count = 4
expect do
diff --git a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
index d88e262883f..0dd52b45f5d 100644
--- a/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/project_reference_filter_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
include FilterSpecHelper
def invalidate_reference(reference)
- "#{reference.reverse}"
+ reference.reverse.to_s
end
def get_reference(project)
@@ -109,7 +109,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
let_it_be(:nested_project_reference) { get_reference(nested_project) }
it 'does not have N+1 per multiple project references', :use_sql_query_cache do
- markdown = "#{normal_project_reference}"
+ markdown = normal_project_reference.to_s
# warm up first
reference_filter(markdown)
diff --git a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
index 70cbdb080a4..b153efd9655 100644
--- a/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/user_reference_filter_spec.rb
@@ -209,7 +209,7 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do
let(:reference3) { group.to_reference }
it 'does not have N+1 per multiple user references', :use_sql_query_cache do
- markdown = "#{reference}"
+ markdown = reference.to_s
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
reference_filter(markdown)
diff --git a/spec/lib/banzai/filter/repository_link_filter_spec.rb b/spec/lib/banzai/filter/repository_link_filter_spec.rb
index c220263b238..4aeb6e2a722 100644
--- a/spec/lib/banzai/filter/repository_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/repository_link_filter_spec.rb
@@ -3,7 +3,6 @@
require 'spec_helper'
RSpec.describe Banzai::Filter::RepositoryLinkFilter do
- include GitHelpers
include RepoHelpers
def filter(doc, contexts = {})
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 33adca0ddfc..a409c15533b 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre><copy-code></copy-code></div>')
end
include_examples "XSS prevention", ""
@@ -31,9 +31,9 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
context "when contains mermaid diagrams" do
it "ignores mermaid blocks" do
- result = filter('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
+ result = filter('<pre data-mermaid-style="display" lang="mermaid"><code class="js-render-mermaid">mermaid code</code></pre>')
- expect(result.to_html).to eq('<pre data-mermaid-style="display"><code>mermaid code</code></pre>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-mermaid-style="display" lang="mermaid" class="code highlight js-syntax-highlight language-mermaid" v-pre="true"><code class="js-render-mermaid"><span id="LC1" class="line" lang="mermaid">mermaid code</span></code></pre><copy-code></copy-code></div>')
end
end
@@ -45,11 +45,32 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
end
end
+ # This can happen with the following markdown
+ #
+ # <div>
+ # <pre><code>
+ # something
+ #
+ # else
+ # </code></pre>
+ # </div>
+ #
+ # The blank line causes markdown to process ` else` as a code block.
+ # Which could lead to an orphaned node being replaced and failing
+ context "when <pre><code> is a child of <pre><code> which is a child of a div " do
+ it "captures all text and doesn't fail trying to replace a node with no parent" do
+ text = "<div>\n<pre><code>\nsomething\n<pre><code>else\n</code></pre></code></pre>\n</div>"
+ result = filter(text)
+
+ expect(result.to_html.delete("\n")).to eq('<div><div class="gl-relative markdown-code-block js-markdown-code"><pre lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext"></span><span id="LC2" class="line" lang="plaintext">something</span><span id="LC3" class="line" lang="plaintext">else</span></code></pre><copy-code></copy-code></div></div>')
+ end
+ end
+
context "when a valid language is specified" do
it "highlights as that language" do
result = filter('<pre lang="ruby"><code>def fun end</code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="ruby" class="code highlight js-syntax-highlight language-ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre><copy-code></copy-code></div>')
end
include_examples "XSS prevention", "ruby"
@@ -59,7 +80,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext" do
result = filter('<pre lang="gnuplot"><code>This is a test</code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" data-canonical-lang="gnuplot" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="gnuplot" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
end
include_examples "XSS prevention", "gnuplot"
@@ -74,7 +95,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext but with the correct language attribute and class" do
result = filter(%{<pre lang="#{lang}"><code>This is a test</code></pre>})
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
+ expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
end
include_examples "XSS prevention", lang
@@ -87,7 +108,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "includes data-lang-params tag with extra information" do
result = filter(%{<pre lang="#{lang}" data-meta="#{lang_params}"><code>This is a test</code></pre>})
- expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
+ expect(result.to_html.delete("\n")).to eq(%{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>})
end
include_examples "XSS prevention", lang
@@ -105,7 +126,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
let(:lang_params) { '-1+10' }
let(:expected_result) do
- %{<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight language-#{lang}" lang="#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}
+ %{<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="#{lang}" class="code highlight js-syntax-highlight language-#{lang}" #{data_attr}="#{lang_params} more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre><copy-code></copy-code></div>}
end
context 'when delimiter is space' do
@@ -130,13 +151,13 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "includes it in the highlighted code block" do
result = filter('<pre data-sourcepos="1:1-3:3"><code lang="plaintext">This is a test</code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos="1:1-3:3" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code lang="plaintext"><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre><copy-code></copy-code></div>')
end
it "escape sourcepos metadata to prevent XSS" do
result = filter('<pre data-sourcepos="&#34;%22 href=&#34;x&#34;></pre><base href=http://unsafe-website.com/><pre x=&#34;"><code></code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos=\'"%22 href="x"&gt;&lt;/pre&gt;&lt;base href=http://unsafe-website.com/&gt;&lt;pre x="\' class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" data-canonical-lang="" v-pre="true"><code></code></pre><copy-code></copy-code></div>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre data-sourcepos=\'"%22 href="x"&gt;&lt;/pre&gt;&lt;base href=http://unsafe-website.com/&gt;&lt;pre x="\' lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code></code></pre><copy-code></copy-code></div>')
end
end
@@ -150,7 +171,7 @@ RSpec.describe Banzai::Filter::SyntaxHighlightFilter do
it "highlights as plaintext" do
result = filter('<pre lang="ruby"><code>This is a test</code></pre>')
- expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre class="code highlight js-syntax-highlight" lang="" data-canonical-lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>')
+ expect(result.to_html.delete("\n")).to eq('<div class="gl-relative markdown-code-block js-markdown-code"><pre lang="" class="code highlight js-syntax-highlight" data-canonical-lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="">This is a test</span></code></pre><copy-code></copy-code></div>')
end
include_examples "XSS prevention", "ruby"
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index d31ccccd6c3..9e77137795a 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe Banzai::ReferenceParser::BaseParser do
context 'when the link does not have a data-project attribute' do
it 'returns the nodes' do
- expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ expect(subject.nodes_visible_to_user(user, [link])).to match_array([link])
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 31cece108bf..3569a1019f0 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -12,12 +12,30 @@ RSpec.describe Banzai::ReferenceParser::CommitParser do
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
- context 'when the link has a data-issue attribute' do
+ context 'when the link has a data-project attribute' do
before do
- link['data-commit'] = 123
+ link['data-project'] = project.id.to_s
end
it_behaves_like "referenced feature visibility", "repository"
+
+ it 'includes the link if can_read_reference? returns true' do
+ expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to contain_exactly(link)
+ end
+
+ it 'excludes the link if can_read_reference? returns false' do
+ expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to be_empty
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
end
end
@@ -129,7 +147,7 @@ RSpec.describe Banzai::ReferenceParser::CommitParser do
end
end
- context 'when checking commits on another projects' do
+ context 'when checking commits on another projects', :request_store do
let!(:control_links) do
[commit_link]
end
@@ -141,7 +159,7 @@ RSpec.describe Banzai::ReferenceParser::CommitParser do
def commit_link
project = create(:project, :repository, :public)
- Nokogiri::HTML.fragment(%Q{<a data-commit="#{project.commit.id}" data-project="#{project.id}"></a>}).children[0]
+ Nokogiri::HTML.fragment(%(<a data-commit="#{project.commit.id}" data-project="#{project.id}"></a>)).children[0]
end
it_behaves_like 'no project N+1 queries'
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index 2f64aef4fb7..172347fc421 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -12,12 +12,30 @@ RSpec.describe Banzai::ReferenceParser::CommitRangeParser do
let(:link) { empty_html_link }
describe '#nodes_visible_to_user' do
- context 'when the link has a data-issue attribute' do
+ context 'when the link has a data-project attribute' do
before do
- link['data-commit-range'] = '123..456'
+ link['data-project'] = project.id.to_s
end
it_behaves_like "referenced feature visibility", "repository"
+
+ it 'includes the link if can_read_reference? returns true' do
+ expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to contain_exactly(link)
+ end
+
+ it 'excludes the link if can_read_reference? returns false' do
+ expect(subject).to receive(:can_read_reference?).with(user, project, link).and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to be_empty
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_visible_to_user(user, [link])).to match_array([link])
+ end
end
end
@@ -136,4 +154,22 @@ RSpec.describe Banzai::ReferenceParser::CommitRangeParser do
end
end
end
+
+ context 'when checking commits ranges on another projects', :request_store do
+ let!(:control_links) do
+ [commit_range_link]
+ end
+
+ let!(:actual_links) do
+ control_links + [commit_range_link, commit_range_link]
+ end
+
+ def commit_range_link
+ project = create(:project, :repository, :public)
+
+ Nokogiri::HTML.fragment(%(<a data-commit-range="123...456" data-project="#{project.id}"></a>)).children[0]
+ end
+
+ it_behaves_like 'no project N+1 queries'
+ end
end
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 7de78710d34..c180a42c91e 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -5,10 +5,10 @@ require 'spec_helper'
RSpec.describe Banzai::ReferenceParser::IssueParser do
include ReferenceParserHelpers
- 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_it_be(:group) { create(:group, :public) }
+ let_it_be_with_reload(: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 }
diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb
index 75c5f363b1f..6962a943755 100644
--- a/spec/lib/bulk_imports/clients/http_spec.rb
+++ b/spec/lib/bulk_imports/clients/http_spec.rb
@@ -202,6 +202,22 @@ RSpec.describe BulkImports::Clients::HTTP do
it 'returns version as an instance of Gitlab::VersionInfo' do
expect(subject.instance_version).to eq(Gitlab::VersionInfo.parse(version))
end
+
+ context 'when /version endpoint is not available' do
+ it 'requests /metadata endpoint' do
+ response_double = double(code: 404, success?: false, parsed_response: 'Not Found', request: double(path: double(path: '/version')))
+
+ allow(Gitlab::HTTP).to receive(:get)
+ .with('http://gitlab.example/api/v4/version', anything)
+ .and_return(response_double)
+
+ expect(Gitlab::HTTP).to receive(:get)
+ .with('http://gitlab.example/api/v4/metadata', anything)
+ .and_return(version_response)
+
+ expect(subject.instance_version).to eq(Gitlab::VersionInfo.parse(version))
+ end
+ end
end
describe '#compatible_for_project_migration?' do
diff --git a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
index 9ea519d367e..dc17dc594a8 100644
--- a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
+++ b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb
@@ -16,8 +16,10 @@ RSpec.describe BulkImports::Common::Pipelines::EntityFinisher do
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_id: entity.id,
bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_class: described_class.name,
message: 'Entity finished',
+ source_version: entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration'
)
end
diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb
index a5a01354d0e..e66f2d26911 100644
--- a/spec/lib/bulk_imports/pipeline/runner_spec.rb
+++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb
@@ -55,14 +55,21 @@ RSpec.describe BulkImports::Pipeline::Runner do
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:error)
.with(
- log_params(
- context,
- pipeline_step: :extractor,
- pipeline_class: 'BulkImports::MyPipeline',
- exception_class: exception_class,
- exception_message: exception_message,
- message: "Pipeline failed",
- importer: 'gitlab_migration'
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'pipeline_step' => :extractor,
+ 'pipeline_class' => 'BulkImports::MyPipeline',
+ 'exception.class' => exception_class,
+ 'exception.message' => exception_message,
+ 'correlation_id' => anything,
+ 'class' => 'BulkImports::MyPipeline',
+ 'message' => "Pipeline failed",
+ 'importer' => 'gitlab_migration',
+ 'exception.backtrace' => anything,
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end
@@ -296,6 +303,8 @@ RSpec.describe BulkImports::Pipeline::Runner do
bulk_import_id: context.bulk_import_id,
bulk_import_entity_id: context.entity.id,
bulk_import_entity_type: context.entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: context.entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration',
context_extra: context.extra
}.merge(extra)
diff --git a/spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb
new file mode 100644
index 00000000000..3c3d0a6d1c4
--- /dev/null
+++ b/spec/lib/bulk_imports/projects/pipelines/references_pipeline_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import, url: 'https://my.gitlab.com') }
+ let_it_be(:entity) do
+ create(
+ :bulk_import_entity,
+ :project_entity,
+ project: project,
+ bulk_import: bulk_import,
+ source_full_path: 'source/full/path'
+ )
+ end
+
+ let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
+
+ let(:issue) { create(:issue, project: project, description: 'https://my.gitlab.com/source/full/path/-/issues/1') }
+ let(:mr) { create(:merge_request, source_project: project, description: 'https://my.gitlab.com/source/full/path/-/merge_requests/1') }
+ let(:issue_note) { create(:note, project: project, noteable: issue, note: 'https://my.gitlab.com/source/full/path/-/issues/1') }
+ let(:mr_note) { create(:note, project: project, noteable: mr, note: 'https://my.gitlab.com/source/full/path/-/merge_requests/1') }
+
+ subject(:pipeline) { described_class.new(context) }
+
+ before do
+ project.add_owner(user)
+ end
+
+ def create_project_data
+ [issue, mr, issue_note, mr_note]
+ end
+
+ describe '#extract' do
+ it 'returns ExtractedData containing issues, mrs & their notes' do
+ create_project_data
+
+ extracted_data = subject.extract(context)
+
+ expect(extracted_data).to be_instance_of(BulkImports::Pipeline::ExtractedData)
+ expect(extracted_data.data).to contain_exactly(issue_note, mr, issue, mr_note)
+ end
+ end
+
+ describe '#transform' do
+ it 'updates matching urls with new ones' do
+ transformed_mr = subject.transform(context, mr)
+ transformed_note = subject.transform(context, mr_note)
+
+ expected_url = URI('')
+ expected_url.scheme = ::Gitlab.config.gitlab.https ? 'https' : 'http'
+ expected_url.host = ::Gitlab.config.gitlab.host
+ expected_url.port = ::Gitlab.config.gitlab.port
+ expected_url.path = "/#{project.full_path}/-/merge_requests/#{mr.iid}"
+
+ expect(transformed_mr.description).to eq(expected_url.to_s)
+ expect(transformed_note.note).to eq(expected_url.to_s)
+ end
+
+ context 'when object does not have reference' do
+ it 'returns object unchanged' do
+ issue.update!(description: 'foo')
+
+ transformed_issue = subject.transform(context, issue)
+
+ expect(transformed_issue.description).to eq('foo')
+ end
+ end
+
+ context 'when there are not matched urls' do
+ let(:url) { 'https://my.gitlab.com/another/project/path/-/issues/1' }
+
+ shared_examples 'returns object unchanged' do
+ it 'returns object unchanged' do
+ issue.update!(description: url)
+
+ transformed_issue = subject.transform(context, issue)
+
+ expect(transformed_issue.description).to eq(url)
+ end
+ end
+
+ include_examples 'returns object unchanged'
+
+ context 'when url path does not start with source full path' do
+ let(:url) { 'https://my.gitlab.com/another/source/full/path/-/issues/1' }
+
+ include_examples 'returns object unchanged'
+ end
+
+ context 'when host does not match and url path starts with source full path' do
+ let(:url) { 'https://another.gitlab.com/source/full/path/-/issues/1' }
+
+ include_examples 'returns object unchanged'
+ end
+
+ context 'when url does not match at all' do
+ let(:url) { 'https://website.example/foo/bar' }
+
+ include_examples 'returns object unchanged'
+ end
+ end
+ end
+
+ describe '#load' do
+ it 'saves the object when object body changed' do
+ transformed_issue = subject.transform(context, issue)
+ transformed_note = subject.transform(context, issue_note)
+
+ expect(transformed_issue).to receive(:save!)
+ expect(transformed_note).to receive(:save!)
+
+ subject.load(context, transformed_issue)
+ subject.load(context, transformed_note)
+ end
+
+ context 'when object body is not changed' do
+ it 'does not save the object' do
+ expect(mr).not_to receive(:save!)
+ expect(mr_note).not_to receive(:save!)
+
+ subject.load(context, mr)
+ subject.load(context, mr_note)
+ end
+ end
+ end
+end
diff --git a/spec/lib/error_tracking/sentry_client/issue_spec.rb b/spec/lib/error_tracking/sentry_client/issue_spec.rb
index ac6a4b9e8cd..eaa3c7ee8bc 100644
--- a/spec/lib/error_tracking/sentry_client/issue_spec.rb
+++ b/spec/lib/error_tracking/sentry_client/issue_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe ErrorTracking::SentryClient::Issue do
it_behaves_like 'issues have correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'issues have correct length', 3
it_behaves_like 'maps Sentry exceptions'
- it_behaves_like 'Sentry API response size limit', enabled_by_default: true
+ it_behaves_like 'Sentry API response size limit'
shared_examples 'has correct external_url' do
describe '#external_url' do
diff --git a/spec/lib/feature/gitaly_spec.rb b/spec/lib/feature/gitaly_spec.rb
index ed80e31e3cd..33696290483 100644
--- a/spec/lib/feature/gitaly_spec.rb
+++ b/spec/lib/feature/gitaly_spec.rb
@@ -6,73 +6,207 @@ RSpec.describe Feature::Gitaly do
let_it_be(:project) { create(:project) }
let_it_be(:project_2) { create(:project) }
+ let_it_be(:repository) { project.repository.raw }
+ let_it_be(:repository_2) { project_2.repository.raw }
+
before do
skip_feature_flags_yaml_validation
+ allow(Feature::Definition).to receive(:get).with(any_args).and_return(
+ Feature::Definition.new('flag.yml', name: :flag, type: :development)
+ )
end
- describe ".enabled?" do
+ describe ".enabled_for_any?" do
context 'when the flag is set globally' do
- let(:feature_flag) { 'global_flag' }
-
context 'when the gate is closed' do
before do
stub_feature_flags(gitaly_global_flag: false)
end
it 'returns false' do
- expect(described_class.enabled?(feature_flag)).to be(false)
+ expect(described_class.enabled_for_any?(:gitaly_global_flag)).to be(false)
end
end
context 'when the flag defaults to on' do
it 'returns true' do
- expect(described_class.enabled?(feature_flag)).to be(true)
+ expect(described_class.enabled_for_any?(:gitaly_global_flag)).to be(true)
end
end
end
context 'when the flag is enabled for a particular project' do
- let(:feature_flag) { 'project_flag' }
-
before do
stub_feature_flags(gitaly_project_flag: project)
end
it 'returns true for that project' do
- expect(described_class.enabled?(feature_flag, project)).to be(true)
+ expect(described_class.enabled_for_any?(:gitaly_project_flag, project)).to be(true)
end
it 'returns false for any other project' do
- expect(described_class.enabled?(feature_flag, project_2)).to be(false)
+ expect(described_class.enabled_for_any?(:gitaly_project_flag, project_2)).to be(false)
end
it 'returns false when no project is passed' do
- expect(described_class.enabled?(feature_flag)).to be(false)
+ expect(described_class.enabled_for_any?(:gitaly_project_flag)).to be(false)
+ end
+ end
+
+ context 'when the flag is enabled for a particular repository' do
+ before do
+ stub_feature_flags(gitaly_repository_flag: repository)
+ end
+
+ it 'returns true for that repository' do
+ expect(described_class.enabled_for_any?(:gitaly_repository_flag, repository)).to be(true)
+ end
+
+ it 'returns false for any other repository' do
+ expect(described_class.enabled_for_any?(:gitaly_repository_flag, repository_2)).to be(false)
+ end
+
+ it 'returns false when no repository is passed' do
+ expect(described_class.enabled_for_any?(:gitaly_repository_flag)).to be(false)
+ end
+ end
+
+ context 'when the flag is checked with multiple input actors' do
+ before do
+ stub_feature_flags(gitaly_flag: repository)
+ end
+
+ it 'returns true if any of the flag is enabled for any of the input actors' do
+ expect(described_class.enabled_for_any?(:gitaly_flag, project, repository)).to be(true)
+ end
+
+ it 'returns false if any of the flag is not enabled for any of the input actors' do
+ expect(
+ described_class.enabled_for_any?(:gitaly_flag, project, project_2, repository_2)
+ ).to be(false)
end
end
end
describe ".server_feature_flags" do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
before do
- stub_feature_flags(gitaly_global_flag: true, gitaly_project_flag: project, non_gitaly_flag: false)
+ stub_feature_flags(
+ gitaly_global_flag: true,
+ gitaly_project_flag: project,
+ gitaly_repository_flag: repository,
+ gitaly_user_flag: user,
+ gitaly_group_flag: group,
+ non_gitaly_flag: false
+ )
end
subject { described_class.server_feature_flags }
it 'returns a hash of flags starting with the prefix, with dashes instead of underscores' do
expect(subject).to eq('gitaly-feature-global-flag' => 'true',
- 'gitaly-feature-project-flag' => 'false')
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
end
context 'when a project is passed' do
it 'returns the value for the flag on the given project' do
- expect(described_class.server_feature_flags(project))
+ expect(described_class.server_feature_flags(project: project))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'true',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+
+ expect(described_class.server_feature_flags(project: project_2))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+ end
+ end
+
+ context 'when a repository is passed' do
+ it 'returns the value for the flag on the given repository' do
+ expect(described_class.server_feature_flags(repository: repository))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'true',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+
+ expect(described_class.server_feature_flags(repository: repository_2))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+ end
+ end
+
+ context 'when a user is passed' do
+ it 'returns the value for the flag on the given user' do
+ expect(described_class.server_feature_flags(user: user))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'true',
+ 'gitaly-feature-group-flag' => 'false')
+
+ expect(described_class.server_feature_flags(user: create(:user)))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+ end
+ end
+
+ context 'when a group is passed' do
+ it 'returns the value for the flag on the given group' do
+ expect(described_class.server_feature_flags(group: group))
.to eq('gitaly-feature-global-flag' => 'true',
- 'gitaly-feature-project-flag' => 'true')
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'true')
- expect(described_class.server_feature_flags(project_2))
+ expect(described_class.server_feature_flags(group: create(:group)))
.to eq('gitaly-feature-global-flag' => 'true',
- 'gitaly-feature-project-flag' => 'false')
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+ end
+ end
+
+ context 'when multiple actors are passed' do
+ it 'returns the corresponding enablement status for actors' do
+ expect(described_class.server_feature_flags(project: project_2, repository: repository))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'false',
+ 'gitaly-feature-repository-flag' => 'true',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+
+ expect(described_class.server_feature_flags(project: project, repository: repository_2))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'true',
+ 'gitaly-feature-repository-flag' => 'false',
+ 'gitaly-feature-user-flag' => 'false',
+ 'gitaly-feature-group-flag' => 'false')
+
+ expect(described_class.server_feature_flags(user: user, project: project, repository: repository, group: group))
+ .to eq('gitaly-feature-global-flag' => 'true',
+ 'gitaly-feature-project-flag' => 'true',
+ 'gitaly-feature-repository-flag' => 'true',
+ 'gitaly-feature-user-flag' => 'true',
+ 'gitaly-feature-group-flag' => 'true')
end
end
@@ -88,4 +222,67 @@ RSpec.describe Feature::Gitaly do
end
end
end
+
+ describe ".user_actor" do
+ let(:user) { create(:user) }
+
+ context 'when user is passed in' do
+ it 'returns a actor wrapper from user' do
+ expect(described_class.user_actor(user).flipper_id).to eql(user.flipper_id)
+ end
+ end
+
+ context 'when called without user and user_id is available in application context' do
+ it 'returns a actor wrapper from user_id' do
+ ::Gitlab::ApplicationContext.with_context(user: user) do
+ expect(described_class.user_actor.flipper_id).to eql(user.flipper_id)
+ end
+ end
+ end
+
+ context 'when called without user and user_id is absent from application context' do
+ it 'returns nil' do
+ expect(described_class.user_actor).to be(nil)
+ end
+ end
+
+ context 'when something else is passed' do
+ it 'returns nil' do
+ expect(described_class.user_actor(1234)).to be(nil)
+ end
+ end
+ end
+
+ describe ".project_actor" do
+ let_it_be(:project) { create(:project) }
+
+ context 'when project is passed in' do
+ it 'returns a actor wrapper from project' do
+ expect(described_class.project_actor(project).flipper_id).to eql(project.flipper_id)
+ end
+ end
+
+ context 'when something else is passed in' do
+ it 'returns nil' do
+ expect(described_class.project_actor(1234)).to be(nil)
+ end
+ end
+ end
+
+ describe ".group_actor" do
+ let_it_be(:group) { create(:group ) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ context 'when project is passed in' do
+ it "returns a actor wrapper from project's group" do
+ expect(described_class.group_actor(project).flipper_id).to eql(group.flipper_id)
+ end
+ end
+
+ context 'when something else is passed in' do
+ it 'returns nil' do
+ expect(described_class.group_actor(1234)).to be(nil)
+ end
+ end
+ end
end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 56e0b4bca30..ad324406450 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -522,7 +522,7 @@ RSpec.describe Feature, stub_feature_flags: false do
it_behaves_like 'logging' do
let(:expected_action) { :enable }
- let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } }
+ let(:expected_extra) { { "extra.thing" => thing.flipper_id.to_s } }
end
end
end
@@ -548,7 +548,7 @@ RSpec.describe Feature, stub_feature_flags: false do
it_behaves_like 'logging' do
let(:expected_action) { :disable }
- let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } }
+ let(:expected_extra) { { "extra.thing" => thing.flipper_id.to_s } }
end
end
end
@@ -561,7 +561,7 @@ RSpec.describe Feature, stub_feature_flags: false do
it_behaves_like 'logging' do
let(:expected_action) { :enable_percentage_of_time }
- let(:expected_extra) { { "extra.percentage" => "#{percentage}" } }
+ let(:expected_extra) { { "extra.percentage" => percentage.to_s } }
end
end
@@ -584,7 +584,7 @@ RSpec.describe Feature, stub_feature_flags: false do
it_behaves_like 'logging' do
let(:expected_action) { :enable_percentage_of_actors }
- let(:expected_extra) { { "extra.percentage" => "#{percentage}" } }
+ let(:expected_extra) { { "extra.percentage" => percentage.to_s } }
end
end
@@ -790,11 +790,47 @@ RSpec.describe Feature, stub_feature_flags: false do
let(:group) { create(:group) }
let(:user_name) { project.first_owner.username }
- subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) }
+ subject do
+ described_class.new(
+ user: user_name,
+ project: project.full_path,
+ group: group.full_path,
+ repository: project.repository.full_path
+ )
+ end
it 'returns all found targets' do
expect(subject.targets).to be_an(Array)
- expect(subject.targets).to eq([project.first_owner, project, group])
+ expect(subject.targets).to eq([project.first_owner, project, group, project.repository])
+ end
+
+ context 'when repository target works with different types of repositories' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :wiki_repo, group: group) }
+ let_it_be(:project_in_user_namespace) { create(:project, namespace: create(:user).namespace) }
+ let(:personal_snippet) { create(:personal_snippet) }
+ let(:project_snippet) { create(:project_snippet, project: project) }
+
+ let(:targets) do
+ [
+ project,
+ project.wiki,
+ project_in_user_namespace,
+ personal_snippet,
+ project_snippet
+ ]
+ end
+
+ subject do
+ described_class.new(
+ repository: targets.map { |t| t.repository.full_path }.join(",")
+ )
+ end
+
+ it 'returns all found targets' do
+ expect(subject.targets).to be_an(Array)
+ expect(subject.targets).to eq(targets.map(&:repository))
+ end
end
end
end
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
index b4aa843bcd7..258f4a0d019 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
@@ -38,6 +38,6 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::Median do
merge_request2.metrics.update!(merged_at: Time.zone.now)
end
- expect(subject).to be_within(0.5).of(7.5.minutes.seconds)
+ expect(subject).to be_within(5.seconds).of(7.5.minutes.seconds)
end
end
diff --git a/spec/lib/gitlab/app_logger_spec.rb b/spec/lib/gitlab/app_logger_spec.rb
index 23bac444dbe..85ca60d539f 100644
--- a/spec/lib/gitlab/app_logger_spec.rb
+++ b/spec/lib/gitlab/app_logger_spec.rb
@@ -5,10 +5,9 @@ require 'spec_helper'
RSpec.describe Gitlab::AppLogger do
subject { described_class }
- it 'builds a Gitlab::Logger object twice' do
- expect(Gitlab::Logger).to receive(:new)
- .exactly(described_class.loggers.size)
- .and_call_original
+ it 'builds two Logger instances' do
+ expect(Gitlab::Logger).to receive(:new).and_call_original
+ expect(Gitlab::JsonLogger).to receive(:new).and_call_original
subject.info('Hello World!')
end
diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb
index 8b2a228b935..58d462aa27f 100644
--- a/spec/lib/gitlab/application_context_spec.rb
+++ b/spec/lib/gitlab/application_context_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::ApplicationContext do
describe '.push' do
it 'passes the expected context on to labkit' do
fake_proc = duck_type(:call)
- expected_context = { user: fake_proc, client_id: fake_proc }
+ expected_context = { user: fake_proc, user_id: fake_proc, client_id: fake_proc }
expect(Labkit::Context).to receive(:push).with(expected_context)
@@ -108,14 +108,16 @@ RSpec.describe Gitlab::ApplicationContext do
context = described_class.new(user: -> { user }, project: -> { project }, namespace: -> { subgroup })
expect(result(context))
- .to include(user: user.username, project: project.full_path, root_namespace: namespace.full_path)
+ .to include(user: user.username, user_id: user.id, project: project.full_path,
+ root_namespace: namespace.full_path)
end
it 'correctly loads the expected values when passed directly' do
context = described_class.new(user: user, project: project, namespace: subgroup)
expect(result(context))
- .to include(user: user.username, project: project.full_path, root_namespace: namespace.full_path)
+ .to include(user: user.username, user_id: user.id, project: project.full_path,
+ root_namespace: namespace.full_path)
end
it 'falls back to a projects namespace when a project is passed but no namespace' do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 8fec8bce23e..d2eb9209f42 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -94,9 +94,18 @@ module Gitlab
# Move this test back to the items hash when removing `use_cmark_renderer` feature flag.
it "does not convert dangerous fenced code with inline script into HTML" do
input = '```mypre"><script>alert(3)</script>'
- output = "<div>\n<div>\n<div class=\"gl-relative markdown-code-block js-markdown-code\">\n<pre class=\"code highlight js-syntax-highlight language-plaintext\" lang=\"plaintext\" data-canonical-lang=\"mypre\" v-pre=\"true\"><code></code></pre>\n<copy-code></copy-code>\n</div>\n</div>\n</div>"
+ output = <<~HTML
+ <div>
+ <div>
+ <div class=\"gl-relative markdown-code-block js-markdown-code\">
+ <pre lang=\"plaintext\" class=\"code highlight js-syntax-highlight language-plaintext\" data-canonical-lang=\"mypre\" v-pre=\"true\"><code></code></pre>
+ <copy-code></copy-code>
+ </div>
+ </div>
+ </div>
+ HTML
- expect(render(input, context)).to include(output)
+ expect(render(input, context)).to include(output.strip)
end
it 'does not allow locked attributes to be overridden' do
@@ -360,7 +369,7 @@ module Gitlab
<div>
<div>
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" data-canonical-lang="js" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
+ <pre lang="javascript" class="code highlight js-syntax-highlight language-javascript" data-canonical-lang="js" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span></span></code></pre>
<copy-code></copy-code>
</div>
</div>
@@ -390,7 +399,7 @@ module Gitlab
<div>class.cpp</div>
<div>
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre class="code highlight js-syntax-highlight language-cpp" lang="cpp" data-canonical-lang="c++" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span></span>
+ <pre lang="cpp" class="code highlight js-syntax-highlight language-cpp" data-canonical-lang="c++" v-pre="true"><code><span id="LC1" class="line" lang="cpp"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span></span>
<span id="LC2" class="line" lang="cpp"></span>
<span id="LC3" class="line" lang="cpp"><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">5</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span></span>
<span id="LC4" class="line" lang="cpp"> <span class="n">std</span><span class="o">::</span><span class="n">cout</span><span class="o">&lt;&lt;</span><span class="s">"*"</span><span class="o">&lt;&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span></span>
@@ -448,7 +457,7 @@ module Gitlab
stem:[2+2] is 4
MD
- expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>')
+ expect(render(input, context)).to include('<pre data-math-style="display" lang="plaintext" class="code math js-render-math" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">eta_x gamma</span></code></pre>')
expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>')
end
end
@@ -567,7 +576,7 @@ module Gitlab
it 'does not allow kroki-plantuml-include to be overridden' do
input = <<~ADOC
- [plantuml, test="{counter:kroki-plantuml-include:/etc/passwd}", format="png"]
+ [plantuml, test="{counter:kroki-plantuml-include:README.md}", format="png"]
....
class BlockProcessor
@@ -578,7 +587,7 @@ module Gitlab
output = <<~HTML
<div>
<div>
- <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" decoding=\"async\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==\"></a>
+ <a class=\"no-attachment-icon\" href=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==?test=README.md\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Diagram\" decoding=\"async\" class=\"lazy\" data-src=\"https://kroki.io/plantuml/png/eNpLzkksLlZwyslPzg4oyk9OLS7OL-LiQuUr2NTo6ipUJ-eX5pWkFlllF-VnZ-oW5CTmlZTm5uhm5iXnlKak1gIABQEb8A==?test=README.md\"></a>
</div>
</div>
HTML
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index e2226952d15..05eca4cf70f 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -89,12 +89,13 @@ RSpec.describe Gitlab::Auth::AuthFinders do
context 'with a running job' do
let(:token) { job.token }
- if without_job_token_allowed == :error
+ case without_job_token_allowed
+ when :error
it 'returns an Unauthorized exception' do
expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
expect(@current_authenticated_job).to be_nil
end
- elsif without_job_token_allowed == :user
+ when :user
it 'returns the user' do
expect(subject).to eq(user)
expect(@current_authenticated_job).to eq job
diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb
new file mode 100644
index 00000000000..77d6cc43114
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_details_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceDetails, :migration do
+ let_it_be(:namespace_details) { table(:namespace_details) }
+ let_it_be(:namespaces) { table(:namespaces) }
+ let_it_be(:projects) { table(:projects) }
+
+ subject(:perform_migration) do
+ described_class.new(start_id: projects.minimum(:id),
+ end_id: projects.maximum(:id),
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 0,
+ connection: ActiveRecord::Base.connection)
+ .perform
+ end
+
+ describe '#perform' do
+ it 'creates details for all project namespaces in range' do
+ namespaces.create!(id: 5, name: 'test1', path: 'test1', description: "Some description1",
+ description_html: "Some description html1", cached_markdown_version: 4)
+ project_namespace1 = namespaces.create!(id: 6, name: 'test2', path: 'test2', type: 'Project')
+ namespaces.create!(id: 7, name: 'test3', path: 'test3', description: "Some description3",
+ description_html: "Some description html3", cached_markdown_version: 4)
+ project_namespace2 = namespaces.create!(id: 8, name: 'test4', path: 'test4', type: 'Project')
+
+ project1 = projects.create!(namespace_id: project_namespace1.id, name: 'gitlab1', path: 'gitlab1',
+ project_namespace_id: project_namespace1.id, description: "Some description2",
+ description_html: "Some description html2", cached_markdown_version: 4)
+ project2 = projects.create!(namespace_id: project_namespace2.id, name: 'gitlab2', path: 'gitlab2',
+ project_namespace_id: project_namespace2.id,
+ description: "Some description3",
+ description_html: "Some description html4", cached_markdown_version: 4)
+
+ namespace_details.delete_all
+
+ expect(namespace_details.pluck(:namespace_id)).to eql []
+
+ expect { perform_migration }
+ .to change { namespace_details.pluck(:namespace_id) }.from([]).to contain_exactly(
+ project_namespace1.id,
+ project_namespace2.id
+ )
+
+ expect(namespace_details.find_by_namespace_id(project_namespace1.id))
+ .to have_attributes(migrated_attributes(project1))
+ expect(namespace_details.find_by_namespace_id(project_namespace2.id))
+ .to have_attributes(migrated_attributes(project2))
+ end
+ end
+
+ def migrated_attributes(project)
+ {
+ description: project.description,
+ description_html: project.description_html,
+ cached_markdown_version: project.cached_markdown_version
+ }
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
index 29833074109..3ca7d28f09d 100644
--- a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb
@@ -54,4 +54,21 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues do
expect { perform_migration }.to change { migration.batch_metrics.timings }
end
+
+ context 'when database timeouts' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(error_class: [ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled])
+
+ with_them do
+ it 'retries on timeout error' do
+ expect(migration).to receive(:update_batch).exactly(3).times.and_raise(error_class)
+ expect(migration).to receive(:sleep).with(5).twice
+
+ expect do
+ perform_migration
+ end.to raise_error(error_class)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb b/spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb
deleted file mode 100644
index 4a65ecf8c75..00000000000
--- a/spec/lib/gitlab/background_migration/backfill_projects_with_coverage_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::BackfillProjectsWithCoverage,
- :suppress_gitlab_schemas_validate_connection, schema: 20210818185845 do
- let(:projects) { table(:projects) }
- let(:project_ci_feature_usages) { table(:project_ci_feature_usages) }
- let(:ci_pipelines) { table(:ci_pipelines) }
- let(:ci_daily_build_group_report_results) { table(:ci_daily_build_group_report_results) }
- let(:group) { table(:namespaces).create!(name: 'user', path: 'user') }
- let(:project_1) { projects.create!(namespace_id: group.id) }
- let(:project_2) { projects.create!(namespace_id: group.id) }
- let(:pipeline_1) { ci_pipelines.create!(project_id: project_1.id, source: 13) }
- let(:pipeline_2) { ci_pipelines.create!(project_id: project_1.id, source: 13) }
- let(:pipeline_3) { ci_pipelines.create!(project_id: project_2.id, source: 13) }
- let(:pipeline_4) { ci_pipelines.create!(project_id: project_2.id, source: 13) }
-
- subject { described_class.new }
-
- describe '#perform' do
- before do
- ci_daily_build_group_report_results.create!(
- id: 1,
- project_id: project_1.id,
- date: 4.days.ago,
- last_pipeline_id: pipeline_1.id,
- ref_path: 'main',
- group_name: 'rspec',
- data: { coverage: 95.0 },
- default_branch: true,
- group_id: group.id
- )
-
- ci_daily_build_group_report_results.create!(
- id: 2,
- project_id: project_1.id,
- date: 3.days.ago,
- last_pipeline_id: pipeline_2.id,
- ref_path: 'main',
- group_name: 'rspec',
- data: { coverage: 95.0 },
- default_branch: true,
- group_id: group.id
- )
-
- ci_daily_build_group_report_results.create!(
- id: 3,
- project_id: project_2.id,
- date: 2.days.ago,
- last_pipeline_id: pipeline_3.id,
- ref_path: 'main',
- group_name: 'rspec',
- data: { coverage: 95.0 },
- default_branch: true,
- group_id: group.id
- )
-
- ci_daily_build_group_report_results.create!(
- id: 4,
- project_id: project_2.id,
- date: 1.day.ago,
- last_pipeline_id: pipeline_4.id,
- ref_path: 'test_branch',
- group_name: 'rspec',
- data: { coverage: 95.0 },
- default_branch: false,
- group_id: group.id
- )
-
- stub_const("#{described_class}::INSERT_DELAY_SECONDS", 0)
- end
-
- it 'creates entries per project and default_branch combination in the given range', :aggregate_failures do
- subject.perform(1, 4, 2)
-
- entries = project_ci_feature_usages.order('project_id ASC, default_branch DESC')
-
- expect(entries.count).to eq(3)
- expect(entries[0]).to have_attributes(project_id: project_1.id, feature: 1, default_branch: true)
- expect(entries[1]).to have_attributes(project_id: project_2.id, feature: 1, default_branch: true)
- expect(entries[2]).to have_attributes(project_id: project_2.id, feature: 1, default_branch: false)
- end
-
- context 'when an entry for the project and default branch combination already exists' do
- before do
- subject.perform(1, 4, 2)
- end
-
- it 'does not create a new entry' do
- expect { subject.perform(1, 4, 2) }.not_to change { project_ci_feature_usages.count }
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb b/spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb
new file mode 100644
index 00000000000..04ada1703bc
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillUserDetailsFields, :migration, schema: 20221018232820 do
+ let(:users) { table(:users) }
+ let(:user_details) { table(:user_details) }
+
+ let!(:user_all_fields_backfill) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:user_long_details_fields) do
+ length = UserDetail::DEFAULT_FIELD_LENGTH + 1
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'l' * length,
+ twitter: 't' * length,
+ skype: 's' * length,
+ website_url: "https://#{'a' * (length - 12)}.com",
+ location: 'l' * length,
+ organization: 'o' * length
+ )
+ end
+
+ let!(:user_nil_details_fields) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1
+ )
+ end
+
+ let!(:user_empty_details_fields) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: '',
+ twitter: '',
+ skype: '',
+ website_url: '',
+ location: '',
+ organization: ''
+ )
+ end
+
+ let!(:user_with_bio) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:bio_user_details) do
+ user_details
+ .find_or_create_by!(user_id: user_with_bio.id)
+ .update!(bio: 'bio')
+ end
+
+ let!(:user_with_details) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:existing_user_details) do
+ user_details
+ .find_or_create_by!(user_id: user_with_details.id)
+ .update!(
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:user_different_details) do
+ users.create!(
+ name: generate(:name),
+ email: generate(:email),
+ projects_limit: 1,
+ linkedin: 'linked-in',
+ twitter: '@twitter',
+ skype: 'skype',
+ website_url: 'https://example.com',
+ location: 'Antarctica',
+ organization: 'Gitlab'
+ )
+ end
+
+ let!(:differing_details) do
+ user_details
+ .find_or_create_by!(user_id: user_different_details.id)
+ .update!(
+ linkedin: 'details-in',
+ twitter: '@details',
+ skype: 'details_skype',
+ website_url: 'https://details.site',
+ location: 'Details Location',
+ organization: 'Details Organization'
+ )
+ end
+
+ let(:user_ids) do
+ [
+ user_all_fields_backfill,
+ user_long_details_fields,
+ user_nil_details_fields,
+ user_empty_details_fields,
+ user_with_bio,
+ user_with_details,
+ user_different_details
+ ].map(&:id)
+ end
+
+ subject do
+ described_class.new(
+ start_id: user_ids.min,
+ end_id: user_ids.max,
+ batch_table: 'users',
+ batch_column: 'id',
+ sub_batch_size: 1_000,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ it 'processes all relevant records' do
+ expect { subject.perform }.to change { user_details.all.size }.to(5)
+ end
+
+ it 'backfills new user_details fields' do
+ subject.perform
+
+ user_detail = user_details.find_by!(user_id: user_all_fields_backfill.id)
+ expect(user_detail.linkedin).to eq('linked-in')
+ expect(user_detail.twitter).to eq('@twitter')
+ expect(user_detail.skype).to eq('skype')
+ expect(user_detail.website_url).to eq('https://example.com')
+ expect(user_detail.location).to eq('Antarctica')
+ expect(user_detail.organization).to eq('Gitlab')
+ end
+
+ it 'does not migrate nil fields' do
+ subject.perform
+
+ expect(user_details.find_by(user_id: user_nil_details_fields)).to be_nil
+ end
+
+ it 'does not migrate empty fields' do
+ subject.perform
+
+ expect(user_details.find_by(user_id: user_empty_details_fields)).to be_nil
+ end
+
+ it 'backfills new fields without overwriting existing `bio` field' do
+ subject.perform
+
+ user_detail = user_details.find_by!(user_id: user_with_bio.id)
+ expect(user_detail.bio).to eq('bio')
+ expect(user_detail.linkedin).to eq('linked-in')
+ expect(user_detail.twitter).to eq('@twitter')
+ expect(user_detail.skype).to eq('skype')
+ expect(user_detail.website_url).to eq('https://example.com')
+ expect(user_detail.location).to eq('Antarctica')
+ expect(user_detail.organization).to eq('Gitlab')
+ end
+
+ context 'when user details are unchanged' do
+ it 'does not change existing details' do
+ expect { subject.perform }.not_to change {
+ user_details.find_by!(user_id: user_with_details.id).attributes
+ }
+ end
+ end
+
+ context 'when user details are changed' do
+ it 'updates existing user details' do
+ expect { subject.perform }.to change {
+ user_details.find_by!(user_id: user_different_details.id).attributes
+ }
+
+ user_detail = user_details.find_by!(user_id: user_different_details.id)
+ expect(user_detail.linkedin).to eq('linked-in')
+ expect(user_detail.twitter).to eq('@twitter')
+ expect(user_detail.skype).to eq('skype')
+ expect(user_detail.website_url).to eq('https://example.com')
+ expect(user_detail.location).to eq('Antarctica')
+ expect(user_detail.organization).to eq('Gitlab')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
index f03f90ddbbb..95be14cefb1 100644
--- a/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
+++ b/spec/lib/gitlab/background_migration/batched_migration_job_spec.rb
@@ -57,6 +57,71 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
end
end
+ describe '.operation_name' do
+ subject(:perform_job) { job_instance.perform }
+
+ let(:job_instance) do
+ job_class.new(start_id: 1, end_id: 10,
+ batch_table: '_test_table',
+ batch_column: 'id',
+ sub_batch_size: 2,
+ pause_ms: 1000,
+ job_arguments: %w(a b),
+ connection: connection)
+ end
+
+ let(:job_class) do
+ Class.new(described_class) do
+ operation_name :update_all
+ end
+ end
+
+ it 'defines method' do
+ expect(job_instance.operation_name).to eq(:update_all)
+ end
+
+ context 'when `operation_name` is not defined' do
+ let(:job_class) do
+ Class.new(described_class) do
+ def perform
+ each_sub_batch do |sub_batch|
+ sub_batch.update_all('to_column = from_column')
+ end
+ end
+ end
+ end
+
+ let(:test_table) { table(:_test_table) }
+ let(:test_insert_table) { table(:_test_insert_table) }
+
+ before do
+ allow(job_instance).to receive(:sleep)
+
+ connection.create_table :_test_table do |t|
+ t.timestamps_with_timezone null: false
+ t.integer :from_column, null: false
+ end
+
+ connection.create_table :_test_insert_table, id: false do |t|
+ t.integer :to_column
+ t.index :to_column, unique: true
+ end
+
+ test_table.create!(id: 1, from_column: 5)
+ test_table.create!(id: 2, from_column: 10)
+ end
+
+ after do
+ connection.drop_table(:_test_table)
+ connection.drop_table(:_test_insert_table)
+ end
+
+ it 'raises an exception' do
+ expect { perform_job }.to raise_error(RuntimeError, /Operation name is required/)
+ end
+ end
+ end
+
describe '.scope_to' do
subject(:job_instance) do
job_class.new(start_id: 1, end_id: 10,
@@ -133,9 +198,10 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
context 'when the subclass uses sub-batching' do
let(:job_class) do
Class.new(described_class) do
+ operation_name :update
+
def perform(*job_arguments)
each_sub_batch(
- operation_name: :update,
batching_arguments: { order_hint: :updated_at },
batching_scope: -> (relation) { relation.where.not(bar: nil) }
) do |sub_batch|
@@ -177,10 +243,10 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
let(:job_class) do
Class.new(described_class) do
scope_to ->(r) { r.where('mod(id, 2) = 0') }
+ operation_name :update
def perform(*job_arguments)
each_sub_batch(
- operation_name: :update,
batching_arguments: { order_hint: :updated_at },
batching_scope: -> (relation) { relation.where.not(bar: nil) }
) do |sub_batch|
@@ -237,8 +303,10 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
let(:job_class) do
Class.new(described_class) do
+ operation_name :insert
+
def perform(*job_arguments)
- distinct_each_batch(operation_name: :insert) do |sub_batch|
+ distinct_each_batch do |sub_batch|
sub_batch.pluck(:from_column).each do |value|
connection.execute("INSERT INTO _test_insert_table VALUES (#{value})")
end
@@ -291,9 +359,10 @@ RSpec.describe Gitlab::BackgroundMigration::BatchedMigrationJob do
let(:job_class) do
Class.new(described_class) do
scope_to ->(r) { r.where.not(from_column: 10) }
+ operation_name :insert
def perform(*job_arguments)
- distinct_each_batch(operation_name: :insert) do |sub_batch|
+ distinct_each_batch do |sub_batch|
end
end
end
diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
index 264faa4de3b..c522c8b307f 100644
--- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
@@ -241,7 +241,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur
context 'when legacy uploads are stored in object storage' do
let(:legacy_upload) { create_remote_upload(note, filename) }
let(:remote_file) do
- { key: "#{legacy_upload.path}" }
+ { key: legacy_upload.path.to_s }
end
let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) }
diff --git a/spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb b/spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb
new file mode 100644
index 00000000000..74f674e052d
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_projects_star_count_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateProjectsStarCount, schema: 20221019105041 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+ let(:users_star_projects) { table(:users_star_projects) }
+
+ let(:namespace1) { namespaces.create!(name: 'namespace 1', path: 'namespace1') }
+ let(:namespace2) { namespaces.create!(name: 'namespace 2', path: 'namespace2') }
+ let(:namespace3) { namespaces.create!(name: 'namespace 3', path: 'namespace3') }
+ let(:namespace4) { namespaces.create!(name: 'namespace 4', path: 'namespace4') }
+ let(:namespace5) { namespaces.create!(name: 'namespace 5', path: 'namespace5') }
+
+ let(:project1) { projects.create!(namespace_id: namespace1.id, project_namespace_id: namespace1.id) }
+ let(:project2) { projects.create!(namespace_id: namespace2.id, project_namespace_id: namespace2.id) }
+ let(:project3) { projects.create!(namespace_id: namespace3.id, project_namespace_id: namespace3.id) }
+ let(:project4) { projects.create!(namespace_id: namespace4.id, project_namespace_id: namespace4.id) }
+ let(:project5) { projects.create!(namespace_id: namespace5.id, project_namespace_id: namespace5.id) }
+
+ let(:user_active) { users.create!(state: 'active', email: 'test1@example.com', projects_limit: 5) }
+ let(:user_blocked) { users.create!(state: 'blocked', email: 'test2@example.com', projects_limit: 5) }
+
+ let(:migration) do
+ described_class.new(
+ start_id: project1.id,
+ end_id: project4.id,
+ batch_table: :projects,
+ batch_column: :id,
+ sub_batch_size: 2,
+ pause_ms: 2,
+ connection: ApplicationRecord.connection
+ )
+ end
+
+ subject(:perform_migration) { migration.perform }
+
+ it 'correctly populates the star counters' do
+ users_star_projects.create!(project_id: project1.id, user_id: user_active.id)
+ users_star_projects.create!(project_id: project2.id, user_id: user_blocked.id)
+ users_star_projects.create!(project_id: project4.id, user_id: user_active.id)
+ users_star_projects.create!(project_id: project4.id, user_id: user_blocked.id)
+ users_star_projects.create!(project_id: project5.id, user_id: user_active.id)
+
+ perform_migration
+
+ expect(project1.reload.star_count).to eq(1)
+ expect(project2.reload.star_count).to eq(0)
+ expect(project3.reload.star_count).to eq(0)
+ expect(project4.reload.star_count).to eq(1)
+ expect(project5.reload.star_count).to eq(0)
+ end
+
+ context 'when database timeouts' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(error_class: [ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled])
+
+ with_them do
+ it 'retries on timeout error' do
+ expect(migration).to receive(:update_batch).exactly(3).times.and_raise(error_class)
+ expect(migration).to receive(:sleep).with(5).twice
+
+ expect do
+ perform_migration
+ end.to raise_error(error_class)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
index 3de84a4e880..fc06012ed20 100644
--- a/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateVulnerabilityReads, :migrati
project_id: project.id,
external_type: 'uuid-v5',
external_id: 'uuid-v5',
- fingerprint: Digest::SHA1.hexdigest("#{vulnerability.id}"),
+ fingerprint: Digest::SHA1.hexdigest(vulnerability.id.to_s),
name: 'Identifier for UUIDv5')
create_finding!(
diff --git a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
index 41266cb24da..10597e65910 100644
--- a/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_backfilled_job_artifacts_expire_at_spec.rb
@@ -85,8 +85,9 @@ RSpec.describe Gitlab::BackgroundMigration::RemoveBackfilledJobArtifactsExpireAt
private
def create_job_artifact(id:, file_type:, expire_at:)
- job = table(:ci_builds, database: :ci).create!(id: id)
- job_artifact.create!(id: id, job_id: job.id, expire_at: expire_at, project_id: project.id, file_type: file_type)
+ job = table(:ci_builds, database: :ci).create!(id: id, partition_id: 100)
+ job_artifact.create!(id: id, job_id: job.id, expire_at: expire_at, project_id: project.id,
+ file_type: file_type, partition_id: 100)
end
end
end
diff --git a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_spec.rb b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_spec.rb
deleted file mode 100644
index b6da8f7fc2d..00000000000
--- a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_spec.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenEncryptedValues,
- :migration,
- schema: 20220922143634 do
- it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchedMigrationJob }
-
- describe '#perform' do
- let(:ci_runners) { table(:ci_runners, database: :ci) }
-
- let(:test_worker) do
- described_class.new(
- start_id: 1,
- end_id: 4,
- batch_table: :ci_runners,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: Ci::ApplicationRecord.connection
- )
- end
-
- subject(:perform) { test_worker.perform }
-
- before do
- ci_runners.create!(id: 1, runner_type: 1, token_encrypted: 'duplicate')
- ci_runners.create!(id: 2, runner_type: 1, token_encrypted: 'a-token')
- ci_runners.create!(id: 3, runner_type: 1, token_encrypted: 'duplicate-2')
- ci_runners.create!(id: 4, runner_type: 1, token_encrypted: nil)
- ci_runners.create!(id: 5, runner_type: 1, token_encrypted: 'duplicate-2')
- ci_runners.create!(id: 6, runner_type: 1, token_encrypted: 'duplicate')
- ci_runners.create!(id: 7, runner_type: 1, token_encrypted: 'another-token')
- ci_runners.create!(id: 8, runner_type: 1, token_encrypted: 'another-token')
- end
-
- it 'nullifies duplicate encrypted tokens', :aggregate_failures do
- expect { perform }.to change { ci_runners.all.order(:id).pluck(:id, :token_encrypted).to_h }
- .from(
- {
- 1 => 'duplicate',
- 2 => 'a-token',
- 3 => 'duplicate-2',
- 4 => nil,
- 5 => 'duplicate-2',
- 6 => 'duplicate',
- 7 => 'another-token',
- 8 => 'another-token'
- }
- )
- .to(
- {
- 1 => nil,
- 2 => 'a-token',
- 3 => nil,
- 4 => nil,
- 5 => nil,
- 6 => nil,
- 7 => 'another-token',
- 8 => 'another-token'
- }
- )
- expect(ci_runners.count).to eq(8)
- expect(ci_runners.pluck(:token_encrypted).uniq).to match_array [
- nil, 'a-token', 'another-token'
- ]
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_spec.rb b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_spec.rb
deleted file mode 100644
index 423b1815e75..00000000000
--- a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_spec.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenValues,
- :migration,
- schema: 20220922143143 do
- it { expect(described_class).to be < Gitlab::BackgroundMigration::BatchedMigrationJob }
-
- describe '#perform' do
- let(:ci_runners) { table(:ci_runners, database: :ci) }
-
- let(:test_worker) do
- described_class.new(
- start_id: 1,
- end_id: 4,
- batch_table: :ci_runners,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: Ci::ApplicationRecord.connection
- )
- end
-
- subject(:perform) { test_worker.perform }
-
- before do
- ci_runners.create!(id: 1, runner_type: 1, token: 'duplicate')
- ci_runners.create!(id: 2, runner_type: 1, token: 'a-token')
- ci_runners.create!(id: 3, runner_type: 1, token: 'duplicate-2')
- ci_runners.create!(id: 4, runner_type: 1, token: nil)
- ci_runners.create!(id: 5, runner_type: 1, token: 'duplicate-2')
- ci_runners.create!(id: 6, runner_type: 1, token: 'duplicate')
- ci_runners.create!(id: 7, runner_type: 1, token: 'another-token')
- ci_runners.create!(id: 8, runner_type: 1, token: 'another-token')
- end
-
- it 'nullifies duplicate tokens', :aggregate_failures do
- expect { perform }.to change { ci_runners.all.order(:id).pluck(:id, :token).to_h }
- .from(
- {
- 1 => 'duplicate',
- 2 => 'a-token',
- 3 => 'duplicate-2',
- 4 => nil,
- 5 => 'duplicate-2',
- 6 => 'duplicate',
- 7 => 'another-token',
- 8 => 'another-token'
- }
- )
- .to(
- {
- 1 => nil,
- 2 => 'a-token',
- 3 => nil,
- 4 => nil,
- 5 => nil,
- 6 => nil,
- 7 => 'another-token',
- 8 => 'another-token'
- }
- )
- expect(ci_runners.count).to eq(8)
- expect(ci_runners.pluck(:token).uniq).to match_array [
- nil, 'a-token', 'another-token'
- ]
- end
- end
-end
diff --git a/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb b/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
new file mode 100644
index 00000000000..2c5c47e39c9
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::SanitizeConfidentialTodos, :migration, schema: 20221110045406 do
+ let(:todos) { table(:todos) }
+ let(:notes) { table(:notes) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:project_features) { table(:project_features) }
+ let(:users) { table(:users) }
+ let(:issues) { table(:issues) }
+ let(:members) { table(:members) }
+ let(:project_authorizations) { table(:project_authorizations) }
+
+ let(:user) { users.create!(first_name: 'Test', last_name: 'User', email: 'test@user.com', projects_limit: 1) }
+ let(:project_namespace1) { namespaces.create!(path: 'pns1', name: 'pns1') }
+ let(:project_namespace2) { namespaces.create!(path: 'pns2', name: 'pns2') }
+
+ let(:project1) do
+ projects.create!(namespace_id: project_namespace1.id,
+ project_namespace_id: project_namespace1.id, visibility_level: 20)
+ end
+
+ let(:project2) do
+ projects.create!(namespace_id: project_namespace2.id,
+ project_namespace_id: project_namespace2.id)
+ end
+
+ let(:issue1) { issues.create!(project_id: project1.id, issue_type: 1, title: 'issue1', author_id: user.id) }
+ let(:issue2) { issues.create!(project_id: project2.id, issue_type: 1, title: 'issue2') }
+
+ let(:public_note) { notes.create!(note: 'text', project_id: project1.id) }
+
+ let(:confidential_note) do
+ notes.create!(note: 'text', project_id: project1.id, confidential: true,
+ noteable_id: issue1.id, noteable_type: 'Issue')
+ end
+
+ let(:other_confidential_note) do
+ notes.create!(note: 'text', project_id: project2.id, confidential: true,
+ noteable_id: issue2.id, noteable_type: 'Issue')
+ end
+
+ let(:common_params) { { user_id: user.id, author_id: user.id, action: 1, state: 'pending', target_type: 'Note' } }
+ let!(:ignored_todo1) { todos.create!(**common_params) }
+ let!(:ignored_todo2) { todos.create!(**common_params, target_id: public_note.id, note_id: public_note.id) }
+ let!(:valid_todo) { todos.create!(**common_params, target_id: confidential_note.id, note_id: confidential_note.id) }
+ let!(:invalid_todo) do
+ todos.create!(**common_params, target_id: other_confidential_note.id, note_id: other_confidential_note.id)
+ end
+
+ describe '#perform' do
+ before do
+ project_features.create!(project_id: project1.id, issues_access_level: 20, pages_access_level: 20)
+ members.create!(state: 0, source_id: project1.id, source_type: 'Project',
+ type: 'ProjectMember', user_id: user.id, access_level: 50, notification_level: 0,
+ member_namespace_id: project_namespace1.id)
+ project_authorizations.create!(project_id: project1.id, user_id: user.id, access_level: 50)
+ end
+
+ subject(:perform) do
+ described_class.new(
+ start_id: notes.minimum(:id),
+ end_id: notes.maximum(:id),
+ batch_table: :notes,
+ batch_column: :id,
+ sub_batch_size: 1,
+ pause_ms: 0,
+ connection: ApplicationRecord.connection
+ ).perform
+ end
+
+ it 'deletes todos where user can not read its note and logs deletion', :aggregate_failures do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger|
+ expect(logger).to receive(:info).with(
+ hash_including(
+ message: "#{described_class.name} deleting invalid todo",
+ attributes: hash_including(invalid_todo.attributes.slice(:id, :user_id, :target_id, :target_type))
+ )
+ ).once
+ end
+
+ expect { perform }.to change(todos, :count).by(-1)
+
+ expect(todos.all).to match_array([ignored_todo1, ignored_todo2, valid_todo])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb b/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb
index 98939e15952..fad10aba882 100644
--- a/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb
+++ b/spec/lib/gitlab/background_migration/update_ci_pipeline_artifacts_unknown_locked_status_spec.rb
@@ -26,8 +26,8 @@ RSpec.describe Gitlab::BackgroundMigration::UpdateCiPipelineArtifactsUnknownLock
let(:locked) { 1 }
let(:unknown) { 2 }
- let(:unlocked_pipeline) { pipelines.create!(locked: unlocked) }
- let(:locked_pipeline) { pipelines.create!(locked: locked) }
+ let(:unlocked_pipeline) { pipelines.create!(locked: unlocked, partition_id: 100) }
+ let(:locked_pipeline) { pipelines.create!(locked: locked, partition_id: 100) }
# rubocop:disable Layout/LineLength
let!(:locked_artifact) { pipeline_artifacts.create!(project_id: project.id, pipeline_id: locked_pipeline.id, size: 1024, file_type: 0, file_format: 'gzip', file: 'a.gz', locked: unknown) }
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 186d4e1fb42..f83ce01c617 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe Gitlab::BitbucketImport::Importer do
before do
stub_omniauth_provider('bitbucket')
- stub_feature_flags(stricter_mr_branch_name: false)
end
let(:statuses) do
diff --git a/spec/lib/gitlab/cache/metrics_spec.rb b/spec/lib/gitlab/cache/metrics_spec.rb
new file mode 100644
index 00000000000..d8103837708
--- /dev/null
+++ b/spec/lib/gitlab/cache/metrics_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Cache::Metrics do
+ subject(:metrics) do
+ described_class.new(
+ caller_id: caller_id,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource
+ )
+ end
+
+ let(:caller_id) { 'caller-id' }
+ let(:cache_identifier) { 'ApplicationController#show' }
+ let(:feature_category) { :source_code_management }
+ let(:backing_resource) { :unknown }
+
+ let(:counter_mock) { instance_double(Prometheus::Client::Counter) }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(
+ :redis_hit_miss_operations_total,
+ 'Hit/miss Redis cache counter'
+ ).and_return(counter_mock)
+ end
+
+ describe '#initialize' do
+ context 'when backing resource is not supported' do
+ let(:backing_resource) { 'foo' }
+
+ it { expect { metrics }.to raise_error(RuntimeError) }
+
+ context 'when on production' do
+ before do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ end
+
+ it 'does not raise an exception' do
+ expect { metrics }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ describe '#increment_cache_hit' do
+ subject { metrics.increment_cache_hit }
+
+ it 'increments number of hits' do
+ expect(counter_mock)
+ .to receive(:increment)
+ .with(
+ {
+ caller_id: caller_id,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource,
+ cache_hit: true
+ }
+ ).once
+
+ subject
+ end
+ end
+
+ describe '#increment_cache_miss' do
+ subject { metrics.increment_cache_miss }
+
+ it 'increments number of misses' do
+ expect(counter_mock)
+ .to receive(:increment)
+ .with(
+ {
+ caller_id: caller_id,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource,
+ cache_hit: false
+ }
+ ).once
+
+ subject
+ end
+ end
+
+ describe '#observe_cache_generation' do
+ subject do
+ metrics.observe_cache_generation { action }
+ end
+
+ let(:action) { 'action' }
+ let(:histogram_mock) { instance_double(Prometheus::Client::Histogram) }
+
+ before do
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(100.0, 500.0)
+ end
+
+ it 'updates histogram metric' do
+ expect(Gitlab::Metrics).to receive(:histogram).with(
+ :redis_cache_generation_duration_seconds,
+ 'Duration of Redis cache generation',
+ {
+ caller_id: caller_id,
+ cache_identifier: cache_identifier,
+ feature_category: feature_category,
+ backing_resource: backing_resource
+ },
+ [0, 1, 5]
+ ).and_return(histogram_mock)
+
+ expect(histogram_mock).to receive(:observe).with({}, 400.0)
+
+ is_expected.to eq(action)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index 3468094ffa5..abad2bfa905 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -9,13 +9,26 @@ RSpec.describe Gitlab::Checks::LfsIntegrity do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:newrev) do
- operations = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- BareRepoOperations.new(repository.path)
- end
+ newrev = repository.commit_files(
+ project.creator,
+ branch_name: 'lfs_integrity_spec',
+ message: 'New LFS objects',
+ actions: [{
+ action: :create,
+ file_path: 'files/lfs/some.iso',
+ content: <<~LFS
+ version https://git-lfs.github.com/spec/v1
+ oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897
+ size 1575078
+ LFS
+ }]
+ )
# Create a commit not pointed at by any ref to emulate being in the
# pre-receive hook so that `--not --all` returns some objects
- operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects")
+ repository.delete_branch('lfs_integrity_spec')
+
+ newrev
end
let(:newrevs) { [newrev] }
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
index f9ebab149a5..647653f8e9e 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
@@ -4,11 +4,37 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
describe '#satisfied_by?' do
- shared_examples 'an exists rule with a context' do
+ subject(:satisfied_by?) { described_class.new(globs).satisfied_by?(nil, context) }
+
+ shared_examples 'a rules:exists with a context' do
it_behaves_like 'a glob matching rule' do
let(:project) { create(:project, :custom_repo, files: files) }
end
+ context 'when the rules:exists has a variable' do
+ let_it_be(:project) { create(:project, :custom_repo, files: { 'helm/helm_file.txt' => '' }) }
+
+ let(:globs) { ['$HELM_DIR/**/*'] }
+
+ let(:variables_hash) do
+ { 'HELM_DIR' => 'helm' }
+ end
+
+ before do
+ allow(context).to receive(:variables_hash).and_return(variables_hash)
+ end
+
+ context 'when the context has the specified variables' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when variable expansion does not match' do
+ let(:variables_hash) { {} }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
context 'after pattern comparision limit is reached' do
let(:globs) { ['*definitely_not_a_matching_glob*'] }
let(:project) { create(:project, :repository) }
@@ -22,26 +48,24 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do
end
end
- subject(:satisfied_by?) { described_class.new(globs).satisfied_by?(nil, context) }
-
- context 'when context is Build::Context::Build' do
- it_behaves_like 'an exists rule with a context' do
+ context 'when the rules are being evaluated at job level' do
+ it_behaves_like 'a rules:exists with a context' do
let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) }
let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: project.repository.commit.sha) }
end
end
- context 'when context is Build::Context::Global' do
- it_behaves_like 'an exists rule with a context' do
+ context 'when the rules are being evaluated for an entire pipeline' do
+ it_behaves_like 'a rules:exists with a context' do
let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.commit.sha) }
let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) }
end
end
- context 'when context is Config::External::Context' do
+ context 'when rules are being evaluated with `include`' do
let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: sha) }
- it_behaves_like 'an exists rule with a context' do
+ it_behaves_like 'a rules:exists with a context' do
let(:sha) { project.repository.commit.sha }
end
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index c56f2d25074..8da46561b73 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
- subject { described_class.new(config, name: :my_bridge) }
+ subject(:entry) { described_class.new(config, name: :my_bridge) }
it_behaves_like 'with inheritable CI config' do
let(:inheritable_key) { 'default' }
@@ -380,4 +380,38 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
end
end
end
+
+ describe '#when' do
+ context 'when bridge is a manual action' do
+ let(:config) { { script: 'deploy', when: 'manual' } }
+
+ it { expect(entry.when).to eq('manual') }
+ end
+
+ context 'when bridge has no `when` attribute' do
+ let(:config) { { script: 'deploy' } }
+
+ it { expect(entry.when).to be_nil }
+ end
+
+ context 'when the `when` keyword is not a string' do
+ context 'when it is an array' do
+ let(:config) { { script: 'exit 0', when: ['always'] } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'bridge when should be a string'
+ end
+ end
+
+ context 'when it is a boolean' do
+ let(:config) { { script: 'exit 0', when: true } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'bridge when should be a string'
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 75ac2ca87ab..acf60a6cdda 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -317,6 +317,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
end
end
+ context 'when the `when` keyword is not a string' do
+ context 'when it is an array' do
+ let(:config) { { script: 'exit 0', when: ['always'] } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job when should be a string'
+ end
+ end
+
+ context 'when it is a boolean' do
+ let(:config) { { script: 'exit 0', when: true } }
+
+ it 'returns error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job when should be a string'
+ end
+ end
+ end
+
context 'when only: is used with rules:' do
let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }
@@ -653,7 +673,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
with_them do
let(:config) { { script: 'ls', rules: rules, only: only }.compact }
- it "#{name}" do
+ it name.to_s do
expect(workflow).to receive(:has_rules?) { has_workflow_rules? }
entry.compose!(deps)
diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
index ad90dd59585..f1578a068b9 100644
--- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb
@@ -208,7 +208,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
it 'reports error about variable' do
expect(entry.errors)
- .to include 'variables:var2 config must be a string'
+ .to include 'variables:var2 config uses invalid data keys: description'
end
end
end
@@ -248,7 +248,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
with_them do
let(:config) { { script: 'ls', rules: rules, only: only }.compact }
- it "#{name}" do
+ it name.to_s do
expect(workflow).to receive(:has_rules?) { has_workflow_rules? }
entry.compose!(deps)
@@ -447,6 +447,29 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do
)
end
end
+
+ context 'when variables have "expand" data' do
+ let(:config) do
+ {
+ script: 'echo',
+ variables: { 'VAR1' => 'val 1',
+ 'VAR2' => { value: 'val 2', expand: false },
+ 'VAR3' => { value: 'val 3', expand: true } }
+ }
+ end
+
+ it 'returns correct value' do
+ expect(entry.value).to eq(
+ name: :rspec,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ job_variables: { 'VAR1' => { value: 'val 1' },
+ 'VAR2' => { value: 'val 2', raw: true },
+ 'VAR3' => { value: 'val 3', raw: false } },
+ root_variables_inheritance: true
+ )
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index a55e13e7c2d..085293d7368 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -316,6 +316,35 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do
end
end
end
+
+ context 'when variables have "expand" data' do
+ let(:hash) do
+ {
+ variables: { 'VAR1' => 'val 1',
+ 'VAR2' => { value: 'val 2', expand: false },
+ 'VAR3' => { value: 'val 3', expand: true } },
+ rspec: { script: 'rspec' }
+ }
+ end
+
+ before do
+ root.compose!
+ end
+
+ it 'returns correct value' do
+ expect(root.variables_entry.value_with_data).to eq(
+ 'VAR1' => { value: 'val 1' },
+ 'VAR2' => { value: 'val 2', raw: true },
+ 'VAR3' => { value: 'val 3', raw: false }
+ )
+
+ expect(root.variables_value).to eq(
+ 'VAR1' => 'val 1',
+ 'VAR2' => 'val 2',
+ 'VAR3' => 'val 3'
+ )
+ end
+ end
end
context 'when configuration is not valid' do
diff --git a/spec/lib/gitlab/ci/config/entry/variable_spec.rb b/spec/lib/gitlab/ci/config/entry/variable_spec.rb
index 076a5b32e92..d7023072312 100644
--- a/spec/lib/gitlab/ci/config/entry/variable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variable_spec.rb
@@ -92,6 +92,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
describe '#value_with_data' do
subject(:value_with_data) { entry.value_with_data }
+ it { is_expected.to eq(value: 'value') }
+ end
+
+ describe '#value_with_prefill_data' do
+ subject(:value_with_prefill_data) { entry.value_with_prefill_data }
+
it { is_expected.to eq(value: 'value', description: 'description') }
end
@@ -107,6 +113,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
describe '#value_with_data' do
subject(:value_with_data) { entry.value_with_data }
+ it { is_expected.to eq(value: 'value') }
+ end
+
+ describe '#value_with_prefill_data' do
+ subject(:value_with_prefill_data) { entry.value_with_prefill_data }
+
it { is_expected.to eq(value: 'value', description: 'description') }
end
end
@@ -123,6 +135,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
describe '#value_with_data' do
subject(:value_with_data) { entry.value_with_data }
+ it { is_expected.to eq(value: '123') }
+ end
+
+ describe '#value_with_prefill_data' do
+ subject(:value_with_prefill_data) { entry.value_with_prefill_data }
+
it { is_expected.to eq(value: '123', description: 'description') }
end
end
@@ -139,6 +157,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
describe '#value_with_data' do
subject(:value_with_data) { entry.value_with_data }
+ it { is_expected.to eq(value: 'value') }
+ end
+
+ describe '#value_with_prefill_data' do
+ subject(:value_with_prefill_data) { entry.value_with_prefill_data }
+
it { is_expected.to eq(value: 'value', description: :description) }
end
end
@@ -192,6 +216,94 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
it { is_expected.to eq(value: 'value') }
end
+
+ describe '#value_with_prefill_data' do
+ subject(:value_with_prefill_data) { entry.value_with_prefill_data }
+
+ it { is_expected.to eq(value: 'value') }
+ end
+ end
+ end
+
+ context 'when config is a hash with expand' do
+ let(:config) { { value: 'value', expand: false } }
+
+ context 'when metadata allowed_value_data is not provided' do
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'var1 config must be a string' }
+ end
+ end
+
+ context 'when metadata allowed_value_data is (value, expand)' do
+ let(:metadata) { { allowed_value_data: %i[value expand] } }
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ it { is_expected.to eq('value') }
+ end
+
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
+
+ it { is_expected.to eq(value: 'value', raw: true) }
+
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
+ before do
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
+ end
+
+ it { is_expected.to eq(value: 'value') }
+ end
+ end
+
+ context 'when config expand is true' do
+ let(:config) { { value: 'value', expand: true } }
+
+ describe '#value_with_data' do
+ subject(:value_with_data) { entry.value_with_data }
+
+ it { is_expected.to eq(value: 'value', raw: false) }
+ end
+ end
+
+ context 'when config expand is a string' do
+ let(:config) { { value: 'value', expand: "true" } }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'var1 config expand should be a boolean value' }
+ end
+ end
+ end
+
+ context 'when metadata allowed_value_data is (value, xyz)' do
+ let(:metadata) { { allowed_value_data: %i[value xyz] } }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ subject(:errors) { entry.errors }
+
+ it { is_expected.to include 'var1 config uses invalid data keys: expand' }
+ end
end
end
end
@@ -229,6 +341,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
describe '#value_with_data' do
subject(:value_with_data) { entry.value_with_data }
+ it { is_expected.to eq(value: 'value') }
+ end
+
+ describe '#value_with_prefill_data' do
+ subject(:value_with_prefill_data) { entry.value_with_prefill_data }
+
it { is_expected.to eq(value: 'value', description: 'description', value_options: %w[value value2]) }
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index 085f304094e..609e4422d5c 100644
--- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -66,6 +66,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
)
end
end
+
+ describe '#value_with_prefill_data' do
+ it 'returns variable with prefill data' do
+ expect(entry.value_with_prefill_data).to eq(
+ 'VARIABLE_1' => { value: 'value 1' },
+ 'VARIABLE_2' => { value: 'value 2' }
+ )
+ end
+ end
end
context 'with numeric keys and values in the config' do
@@ -119,6 +128,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
describe '#value_with_data' do
it 'returns variable with data' do
expect(entry.value_with_data).to eq(
+ 'VARIABLE_1' => { value: 'value' }
+ )
+ end
+ end
+
+ describe '#value_with_prefill_data' do
+ it 'returns variable with prefill data' do
+ expect(entry.value_with_prefill_data).to eq(
'VARIABLE_1' => { value: 'value', description: 'variable 1' }
)
end
@@ -147,6 +164,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
describe '#value_with_data' do
it 'returns variable with data' do
expect(entry.value_with_data).to eq(
+ 'VARIABLE_1' => { value: 'value1' }
+ )
+ end
+ end
+
+ describe '#value_with_prefill_data' do
+ it 'returns variable with prefill data' do
+ expect(entry.value_with_prefill_data).to eq(
'VARIABLE_1' => { value: 'value1', value_options: %w[value1 value2], description: 'variable 1' }
)
end
@@ -174,6 +199,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
describe '#value_with_data' do
it 'returns variable with data' do
expect(entry.value_with_data).to eq(
+ 'VARIABLE_1' => { value: 'value 1' },
+ 'VARIABLE_2' => { value: 'value 2' }
+ )
+ end
+ end
+
+ describe '#value_with_prefill_data' do
+ it 'returns variable with prefill data' do
+ expect(entry.value_with_prefill_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
'VARIABLE_2' => { value: 'value 2' }
)
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index 1306d61d99c..8475c3a8b19 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -14,6 +14,10 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base do
super
end
+
+ def validate_context!
+ # no-op
+ end
end
end
@@ -95,6 +99,24 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base do
expect(file.error_message).to eq('Included file `some/file/xxxxxxxxxxxxxxxx.yml` does not have valid YAML syntax!')
end
end
+
+ context 'when the class has no validate_context!' do
+ let(:test_class) do
+ Class.new(described_class) do
+ def initialize(params, context)
+ @location = params
+
+ super
+ end
+ end
+ end
+
+ let(:location) { 'some/file/config.yaml' }
+
+ it 'raises an error' do
+ expect { valid? }.to raise_error(NotImplementedError)
+ end
+ end
end
describe '#to_hash' do
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index e12f5dcee0a..d905568f01e 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -113,7 +113,19 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
it_behaves_like 'logging config file fetch', 'config_file_fetch_template_content_duration_s', 1
end
- context 'when the key is a hash of file and remote' do
+ context 'when the key is not valid' do
+ let(:local_file) { 'secret-file.yml' }
+ let(:values) do
+ { include: { invalid: local_file },
+ image: 'image:1.0' }
+ end
+
+ it 'returns ambigious specification error' do
+ expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, '`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`')
+ end
+ end
+
+ context 'when the key is a hash of local and remote' do
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret-file', 'masked' => true }]) }
let(:local_file) { 'secret-file.yml' }
let(:remote_url) { 'https://gitlab.com/secret-file.yml' }
@@ -123,7 +135,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
end
it 'returns ambigious specification error' do
- expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, 'Include `{"local":"xxxxxxxxxxx.yml","remote":"https://gitlab.com/xxxxxxxxxxx.yml"}` needs to match exactly one accessor!')
+ expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, 'Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`')
end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 475503de7da..c4a6641ff6b 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -484,7 +484,7 @@ RSpec.describe Gitlab::Ci::Config do
it 'raises ConfigError' do
expect { config }.to raise_error(
described_class::ConfigError,
- 'Include `{"remote":"http://url","local":"/local/file.yml"}` needs to match exactly one accessor!'
+ /Each include must use only one of/
)
end
end
@@ -714,7 +714,7 @@ RSpec.describe Gitlab::Ci::Config do
it 'raises an error' do
expect { config }.to raise_error(
described_class::ConfigError,
- /needs to match exactly one accessor!/
+ /does not have a valid subkey for include/
)
end
end
diff --git a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb
index 6a08e8f0b7f..1ef341ff863 100644
--- a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
describe '#parse!' do
- subject(:parse) { described_class.new.parse!(code_climate, codequality_report) }
+ subject(:parse) { described_class.new.parse!(code_climate, codequality_report, metadata) }
let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new }
let(:code_climate) do
@@ -35,6 +35,15 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
].to_json
end
+ let_it_be(:group) { create(:group, name: 'test-group') }
+ let_it_be(:project) { create(:project, path: 'test-project', group: group) }
+ let(:metadata) do
+ {
+ project: project,
+ commit_sha: 'f0cc5229e2aa5e9429f1b17a3b3b102f21d7fe31'
+ }
+ end
+
context "when data is code_climate style JSON" do
context "when there are no degradations" do
let(:code_climate) { [].to_json }
@@ -133,5 +142,56 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
expect(codequality_report.degradations_count).to eq(0)
end
end
+
+ context 'for web_url' do
+ let(:code_climate) do
+ [
+ {
+ "categories": [
+ "Complexity"
+ ],
+ "check_name": "argument_count",
+ "content": {
+ "body": ""
+ },
+ "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
+ "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
+ "location": {
+ "path": "foo.rb",
+ "lines": {
+ "begin": 10,
+ "end": 10
+ }
+ },
+ "other_locations": [],
+ "remediation_points": 900000,
+ "severity": "major",
+ "type": "issue",
+ "engine_name": "structure"
+ }
+ ].to_json
+ end
+
+ context 'when metadata has project and commit_sha' do
+ it 'adds a non nil url' do
+ want = 'http://localhost/test-group/test-project/-/blob/f0cc5229e2aa5e9429f1b17a3b3b102f21d7fe31/foo.rb#L10'
+ expect { parse }.not_to raise_error
+
+ expect(codequality_report.degradations_count).to eq(1)
+ expect(codequality_report.all_degradations[0]['web_url']).to eq(want)
+ end
+ end
+
+ context 'when metadata does not have project and commit_sha' do
+ let(:metadata) { {} }
+
+ it 'adds a nil url' do
+ expect { parse }.not_to raise_error
+
+ expect(codequality_report.degradations_count).to eq(1)
+ expect(codequality_report.all_degradations[0]['web_url']).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb
index a9851d78f48..e4ae6b25362 100644
--- a/spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::SaxDocument do
describe '#parse!' do
let(:coverage_report) { Gitlab::Ci::Reports::CoverageReport.new }
let(:project_path) { 'foo/bar' }
+ let(:windows_path) { 'foo\bar' }
let(:paths) { ['app/user.rb'] }
let(:cobertura) do
@@ -269,6 +270,36 @@ RSpec.describe Gitlab::Ci::Parsers::Coverage::SaxDocument do
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
+ context 'and has Windows-style paths' do
+ let(:sources_xml) do
+ <<~EOF_WIN
+ <sources>
+ <source>D:\\builds\\#{windows_path}\\app</source>
+ </sources>
+ EOF_WIN
+ end
+
+ context 'when there is a single <class>' do
+ context 'with a single line' do
+ let(:classes_xml) do
+ <<~EOF
+ <packages><package name="app"><classes>
+ <class filename="user.rb"><lines>
+ <line number="1" hits="2"/>
+ </lines></class>
+ </classes></package></packages>
+ EOF
+ end
+
+ it 'parses XML and returns a single file with the filename relative to project root' do
+ expect { parse_report }.not_to raise_error
+
+ expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2 } })
+ end
+ end
+ end
+ end
+
context 'and has multiple sources with a pattern for Go projects' do
let(:project_path) { 'local/go' } # Make sure we're not making false positives
let(:sources_xml) do
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
index 38b229e0dd8..f09b85aa2c7 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_properties_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do
- subject(:parse_source) { described_class.parse_source(properties) }
+ subject(:parse_source_from_properties) { described_class.parse_source(properties) }
context 'when properties are nil' do
let(:properties) { nil }
@@ -50,9 +50,9 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do
end
it 'does not call dependency_scanning parser' do
- expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).not_to receive(:parse_source)
+ expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).not_to receive(:source)
- parse_source
+ parse_source_from_properties
end
end
@@ -82,7 +82,7 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::CyclonedxProperties do
it 'passes only supported properties to the dependency scanning parser' do
expect(Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning).to receive(:source).with(expected_input)
- parse_source
+ parse_source_from_properties
end
end
end
diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
index f3636106b98..0b094880f69 100644
--- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb
@@ -100,16 +100,53 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx do
]
end
+ before do
+ allow(report).to receive(:add_component)
+ end
+
it 'adds each component, ignoring unused attributes' do
expect(report).to receive(:add_component)
- .with(an_object_having_attributes(name: "activesupport", version: "5.1.4", component_type: "library"))
+ .with(
+ an_object_having_attributes(
+ name: "activesupport",
+ version: "5.1.4",
+ component_type: "library",
+ purl: an_object_having_attributes(type: "gem")
+ )
+ )
expect(report).to receive(:add_component)
- .with(an_object_having_attributes(name: "byebug", version: "10.0.0", component_type: "library"))
+ .with(
+ an_object_having_attributes(
+ name: "byebug",
+ version: "10.0.0",
+ component_type: "library",
+ purl: an_object_having_attributes(type: "gem")
+ )
+ )
expect(report).to receive(:add_component)
.with(an_object_having_attributes(name: "minimal-component", version: nil, component_type: "library"))
parse!
end
+
+ context 'when a component has an invalid purl' do
+ before do
+ components.push(
+ {
+ "name" => "invalid-component",
+ "version" => "v0.0.1",
+ "purl" => "pkg:nil",
+ "type" => "library"
+ }
+ )
+ end
+
+ it 'adds an error to the report' do
+ expect(report).to receive(:add_error).with("/components/#{components.size - 1}/purl is invalid")
+
+ parse!
+ end
+ end
end
context 'when report has metadata properties' do
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
index 7dbad354e4c..03cab021c17 100644
--- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb
@@ -400,26 +400,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end
describe 'parsing tracking' do
- let(:tracking_data) do
- {
- 'type' => 'source',
- 'items' => [
- 'signatures' => [
- { 'algorithm' => 'hash', 'value' => 'hash_value' },
- { 'algorithm' => 'location', 'value' => 'location_value' },
- { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
- ]
- ]
- }
- end
-
- context 'with valid tracking information' do
- it 'creates signatures for each algorithm' do
- finding = report.findings.first
- expect(finding.signatures.size).to eq(3)
- expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location', 'scope_offset'])
- end
- end
+ let(:finding) { report.findings.first }
context 'with invalid tracking information' do
let(:tracking_data) do
@@ -436,15 +417,26 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end
it 'ignores invalid algorithm types' do
- finding = report.findings.first
expect(finding.signatures.size).to eq(2)
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location'])
end
end
context 'with valid tracking information' do
+ let(:tracking_data) do
+ {
+ 'type' => 'source',
+ 'items' => [
+ 'signatures' => [
+ { 'algorithm' => 'hash', 'value' => 'hash_value' },
+ { 'algorithm' => 'location', 'value' => 'location_value' },
+ { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
+ ]
+ ]
+ }
+ end
+
it 'creates signatures for each signature algorithm' do
- finding = report.findings.first
expect(finding.signatures.size).to eq(3)
expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset])
@@ -456,7 +448,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end
it 'sets the uuid according to the higest priority signature' do
- finding = report.findings.first
highest_signature = finding.signatures.max_by(&:priority)
identifiers = if signatures_enabled
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index 6e8b6e40928..9126c6dab21 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -409,4 +409,21 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do
end
end
end
+
+ describe '#observe_pipeline_size' do
+ let(:command) { described_class.new(project: project) }
+
+ let(:pipeline) { instance_double(Ci::Pipeline, total_size: 5, project: project, source: "schedule") }
+
+ it 'logs the pipeline total size to histogram' do
+ histogram = instance_double(Prometheus::Client::Histogram)
+
+ expect(::Gitlab::Ci::Pipeline::Metrics).to receive(:pipeline_size_histogram)
+ .and_return(histogram)
+ expect(histogram).to receive(:observe)
+ .with({ source: pipeline.source, plan: project.actual_plan_name }, pipeline.total_size)
+
+ command.observe_pipeline_size(pipeline)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb
index bc453f1502b..c5a5e905d17 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb
@@ -69,7 +69,9 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::ActiveJobs do
class: described_class.name,
message: described_class::MESSAGE,
project_id: project.id,
- plan: default_plan.name
+ plan: default_plan.name,
+ project_path: project.path,
+ jobs_in_alive_pipelines_count: step.send(:count_jobs_in_alive_pipelines)
)
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb
new file mode 100644
index 00000000000..ce1ee2fcda0
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, ref: 'master', user: user)
+ end
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ origin_ref: 'master')
+ end
+
+ let(:dependencies) do
+ [
+ Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::SeedBlock.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::Seed.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::Populate.new(pipeline, command)
+ ]
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ let(:config) do
+ { rspec: { script: 'rspec' } }
+ end
+
+ def run_chain
+ dependencies.map(&:perform!)
+ step.perform!
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ context 'with pipeline name' do
+ let(:config) do
+ { workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } }
+ end
+
+ it 'does not break the chain' do
+ run_chain
+
+ expect(step.break?).to be false
+ end
+
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(pipeline_name: false)
+ end
+
+ it 'does not build pipeline_metadata' do
+ run_chain
+
+ expect(pipeline.pipeline_metadata).to be_nil
+ end
+ end
+
+ context 'with feature flag enabled' do
+ before do
+ stub_feature_flags(pipeline_name: true)
+ end
+
+ it 'builds pipeline_metadata' do
+ run_chain
+
+ expect(pipeline.pipeline_metadata.name).to eq('Pipeline name')
+ expect(pipeline.pipeline_metadata.project).to eq(pipeline.project)
+ expect(pipeline.pipeline_metadata).not_to be_persisted
+ end
+
+ context 'with empty name' do
+ let(:config) do
+ { workflow: { name: ' ' }, rspec: { script: 'rspec' } }
+ end
+
+ it 'strips whitespace from name' do
+ run_chain
+
+ expect(pipeline.pipeline_metadata).to be_nil
+ end
+ end
+
+ context 'with variables' do
+ let(:config) do
+ {
+ variables: { ROOT_VAR: 'value $WORKFLOW_VAR1' },
+ workflow: {
+ name: 'Pipeline $ROOT_VAR $WORKFLOW_VAR2 $UNKNOWN_VAR',
+ rules: [{ variables: { WORKFLOW_VAR1: 'value1', WORKFLOW_VAR2: 'value2' } }]
+ },
+ rspec: { script: 'rspec' }
+ }
+ end
+
+ it 'substitutes variables' do
+ run_chain
+
+ expect(pipeline.pipeline_metadata.name).to eq('Pipeline value value1 value2 ')
+ end
+ end
+
+ context 'with invalid name' do
+ let(:config) do
+ {
+ variables: { ROOT_VAR: 'a' * 256 },
+ workflow: {
+ name: 'Pipeline $ROOT_VAR'
+ },
+ rspec: { script: 'rspec' }
+ }
+ end
+
+ it 'returns error and breaks chain' do
+ ret = run_chain
+
+ expect(ret)
+ .to match_array(["Failed to build pipeline metadata! Name is too long (maximum is 255 characters)"])
+ expect(pipeline.pipeline_metadata.errors.full_messages)
+ .to match_array(['Name is too long (maximum is 255 characters)'])
+ expect(step.break?).to be true
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 51d1661b586..62de4d2e96d 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -236,47 +236,4 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do
end
end
end
-
- context 'with pipeline name' do
- let(:config) do
- { workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } }
- end
-
- context 'with feature flag disabled' do
- before do
- stub_feature_flags(pipeline_name: false)
- end
-
- it 'does not build pipeline_metadata' do
- run_chain
-
- expect(pipeline.pipeline_metadata).to be_nil
- end
- end
-
- context 'with feature flag enabled' do
- before do
- stub_feature_flags(pipeline_name: true)
- end
-
- it 'builds pipeline_metadata' do
- run_chain
-
- expect(pipeline.pipeline_metadata.title).to eq('Pipeline name')
- expect(pipeline.pipeline_metadata.project).to eq(pipeline.project)
- end
-
- context 'with empty name' do
- let(:config) do
- { workflow: { name: ' ' }, rspec: { script: 'rspec' } }
- end
-
- it 'strips whitespace from name' do
- run_chain
-
- expect(pipeline.pipeline_metadata).to be_nil
- end
- end
- end
- end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index c69aa661b05..31086f6ae4a 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -80,7 +80,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do
subject.build!
expect(histogram).to have_received(:observe)
- .with({ source: 'push' }, 0)
+ .with({ source: 'push', plan: project.actual_plan_name }, 0)
end
describe 'active jobs by pipeline plan histogram' do
diff --git a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
deleted file mode 100644
index 6569ce937ac..00000000000
--- a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do
- let_it_be(:project, refind: true) { create(:project, :repository) }
-
- let(:pipeline) do
- create(:ci_pipeline, project: project, sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0')
- end
-
- let(:job) { build(:ci_build, project: project, pipeline: pipeline) }
- let(:environment) { Gitlab::Ci::Pipeline::Seed::Environment.new(job).to_resource }
- let(:seed) { described_class.new(job, environment) }
- let(:attributes) { {} }
-
- before do
- job.assign_attributes(**attributes)
- end
-
- describe '#to_resource' do
- subject { seed.to_resource }
-
- context 'when job has environment attribute' do
- let(:attributes) do
- {
- environment: 'production',
- options: { environment: { name: 'production', **kubernetes_options } }
- }
- end
-
- let(:kubernetes_options) { {} }
-
- it 'returns a deployment object with environment' do
- expect(subject).to be_a(Deployment)
- expect(subject.iid).to be_present
- expect(subject.environment.name).to eq('production')
- expect(subject.cluster).to be_nil
- expect(subject.deployment_cluster).to be_nil
- end
-
- context 'when environment has deployment platform' do
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project], managed: managed_cluster) }
- let(:managed_cluster) { true }
-
- it 'sets the cluster and deployment_cluster' do
- expect(subject.cluster).to eq(cluster) # until we stop double writing in 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
- expect(subject.deployment_cluster.cluster).to eq(cluster)
- end
-
- context 'when a custom namespace is given' do
- let(:kubernetes_options) { { kubernetes: { namespace: 'the-custom-namespace' } } }
-
- context 'when cluster is managed' do
- it 'does not set the custom namespace' do
- expect(subject.deployment_cluster.kubernetes_namespace).not_to eq('the-custom-namespace')
- end
- end
-
- context 'when cluster is not managed' do
- let(:managed_cluster) { false }
-
- it 'sets the custom namespace' do
- expect(subject.deployment_cluster.kubernetes_namespace).to eq('the-custom-namespace')
- end
- end
- end
- end
-
- context 'when environment has an invalid URL' do
- let(:attributes) do
- {
- environment: '!!!',
- options: { environment: { name: '!!!' } }
- }
- end
-
- it 'returns nothing' do
- is_expected.to be_nil
- end
- end
-
- context 'when job has already deployment' do
- let(:job) { build(:ci_build, :with_deployment, project: project, environment: 'production') }
-
- it 'returns the persisted deployment' do
- is_expected.to eq(job.deployment)
- end
- end
- end
-
- context 'when job does not start environment' do
- where(:action) do
- %w(stop prepare verify access)
- end
-
- with_them do
- let(:attributes) do
- {
- environment: 'production',
- options: { environment: { name: 'production', action: action } }
- }
- end
-
- it 'returns nothing' do
- is_expected.to be_nil
- end
- end
- end
-
- context 'when job does not have environment attribute' do
- let(:attributes) { { name: 'test' } }
-
- it 'returns nothing' do
- is_expected.to be_nil
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
index a76b4874eca..55980ae72a0 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb
@@ -6,7 +6,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let(:seed_context) { Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: []) }
+ let(:root_variables) { [] }
+
+ let(:seed_context) { Gitlab::Ci::Pipeline::Seed::Context.new(pipeline, root_variables: root_variables) }
let(:stages_attributes) do
[
@@ -75,4 +77,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do
expect(seed.deployments_count).to eq(2)
end
end
+
+ describe '#root_variables' do
+ let(:root_variables) { %w[var1 value1] }
+
+ it 'returns root_variables' do
+ expect(seed.root_variables).to eq(root_variables)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
index 06ea3433ef0..cdaf9354104 100644
--- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
@@ -1,23 +1,67 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Sbom::Component do
- let(:attributes) do
- {
- type: 'library',
- name: 'component-name',
- version: 'v0.0.1'
- }
- end
+ let(:component_type) { 'library' }
+ let(:name) { 'component-name' }
+ let(:purl_type) { 'npm' }
+ let(:purl) { Sbom::PackageUrl.new(type: purl_type, name: name, version: version).to_s }
+ let(:version) { 'v0.0.1' }
- subject { described_class.new(**attributes) }
+ subject(:component) do
+ described_class.new(
+ type: component_type,
+ name: name,
+ purl: purl,
+ version: version
+ )
+ end
it 'has correct attributes' do
- expect(subject).to have_attributes(
- component_type: attributes[:type],
- name: attributes[:name],
- version: attributes[:version]
+ expect(component).to have_attributes(
+ component_type: component_type,
+ name: name,
+ purl: an_object_having_attributes(type: purl_type),
+ version: version
)
end
+
+ describe '#ingestible?' do
+ subject { component.ingestible? }
+
+ context 'when component_type is invalid' do
+ let(:component_type) { 'invalid' }
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when purl_type is invalid' do
+ let(:purl_type) { 'invalid' }
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when component_type is valid' do
+ where(:component_type) { ::Enums::Sbom.component_types.keys.map(&:to_s) }
+
+ with_them do
+ it { is_expected.to be(true) }
+ end
+ end
+
+ context 'when purl_type is valid' do
+ where(:purl_type) { ::Enums::Sbom.purl_types.keys.map(&:to_s) }
+
+ with_them do
+ it { is_expected.to be(true) }
+ end
+ end
+
+ context 'when there is no purl' do
+ let(:purl) { nil }
+
+ it { is_expected.to be(true) }
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
index 6ffa93e5fc8..f9a83378f46 100644
--- a/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/report_spec.rb
@@ -5,6 +5,21 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Sbom::Report do
subject(:report) { described_class.new }
+ describe '#valid?' do
+ context 'when there are no errors' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'when report contains errors' do
+ before do
+ report.add_error('error1')
+ report.add_error('error2')
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
describe '#add_error' do
it 'appends errors to a list' do
report.add_error('error1')
diff --git a/spec/lib/gitlab/ci/reports/security/flag_spec.rb b/spec/lib/gitlab/ci/reports/security/flag_spec.rb
index 6ee074f7aeb..0ef8f6c75a0 100644
--- a/spec/lib/gitlab/ci/reports/security/flag_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/flag_spec.rb
@@ -29,5 +29,11 @@ RSpec.describe Gitlab::Ci::Reports::Security::Flag do
)
end
end
+
+ describe '#false_positive?' do
+ subject { security_flag.false_positive? }
+
+ it { is_expected.to be_truthy }
+ end
end
end
diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
index e240edc4a12..33f3317c655 100644
--- a/spec/lib/gitlab/ci/reports/security/reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb
@@ -125,6 +125,32 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do
it { is_expected.to be(false) }
end
+
+ context 'when target_reports is not nil and reports is empty' do
+ let(:without_reports) { described_class.new(pipeline) }
+
+ subject { without_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) }
+
+ before do
+ target_reports.get_report('sast', artifact).add_finding(high_severity_dast)
+ end
+
+ context 'when require_approval_on_scan_removal feature is enabled' do
+ before do
+ stub_feature_flags(require_approval_on_scan_removal: true)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when require_approval_on_scan_removal feature is disabled' do
+ before do
+ stub_feature_flags(require_approval_on_scan_removal: false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
index 4a1f77bed65..05f6a8a8cb6 100644
--- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
@@ -209,7 +209,7 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}" do
- subject { test_suite.public_send("#{status_type}") }
+ subject { test_suite.public_send(status_type.to_s) }
context "when #{status_type} test case exists" do
before do
diff --git a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
index 8204b104832..43deb465025 100644
--- a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
index 65fd2b016ac..f2bff5ff3e0 100644
--- a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
let(:platform_target) { 'ECS' }
diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
index 21052f03cb8..07cfa939623 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
@@ -3,8 +3,20 @@
require 'spec_helper'
RSpec.describe 'Jobs/Build.gitlab-ci.yml' do
+ include Ci::TemplateHelpers
+
subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Build') }
+ describe 'AUTO_BUILD_IMAGE_VERSION' do
+ it 'corresponds to a published image in the registry' do
+ registry = "https://#{template_registry_host}"
+ repository = "gitlab-org/cluster-integration/auto-build-image"
+ reference = YAML.safe_load(template.content).dig('variables', 'AUTO_BUILD_IMAGE_VERSION')
+
+ expect(public_image_exist?(registry, repository, reference)).to be true
+ end
+ end
+
describe 'the created pipeline' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -12,7 +24,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
index d88d9782021..16c5d7a4b6d 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -62,7 +62,8 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do
context 'on master' do
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
@@ -70,7 +71,8 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do
let(:pipeline_ref) { 'feature' }
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
@@ -78,7 +80,8 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do
let(:pipeline_ref) { 'v1.0.0' }
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
index b657f73fa77..acb296082b8 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
+ include Ci::TemplateHelpers
+
subject(:template) do
<<~YAML
stages:
@@ -26,6 +28,17 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
YAML
end
+ describe 'AUTO_DEPLOY_IMAGE_VERSION' do
+ it 'corresponds to a published image in the registry' do
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Deploy')
+ registry = "https://#{template_registry_host}"
+ repository = "gitlab-org/cluster-integration/auto-deploy-image"
+ reference = YAML.safe_load(template.content, aliases: true).dig('variables', 'AUTO_DEPLOY_IMAGE_VERSION')
+
+ expect(public_image_exist?(registry, repository, reference)).to be true
+ end
+ end
+
describe 'the created pipeline' do
let_it_be(:project, refind: true) { create(:project, :repository) }
@@ -33,7 +46,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
index 85516d0bbb0..8a5aea7c0f0 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb
@@ -9,10 +9,10 @@ RSpec.describe 'Jobs/SAST-IaC.gitlab-ci.yml' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
- let(:default_branch) { 'main' }
+ let(:default_branch) { "master" }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -49,7 +49,8 @@ RSpec.describe 'Jobs/SAST-IaC.gitlab-ci.yml' do
context 'on default branch' do
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
@@ -57,7 +58,8 @@ RSpec.describe 'Jobs/SAST-IaC.gitlab-ci.yml' do
let(:pipeline_ref) { 'feature' }
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
index 5ff179b6fee..d540b035f81 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb
@@ -9,10 +9,10 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
- let(:default_branch) { 'main' }
+ let(:default_branch) { "master" }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -50,7 +50,8 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do
context 'on default branch' do
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
@@ -58,7 +59,8 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do
let(:pipeline_ref) { 'feature' }
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
index a92a8397e96..7cf0cf3ed33 100644
--- a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -62,7 +62,8 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do
context 'on master' do
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
@@ -70,7 +71,8 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do
let(:pipeline_ref) { 'feature' }
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
@@ -78,7 +80,8 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do
let(:pipeline_ref) { 'v1.0.0' }
it 'has no jobs' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
end
diff --git a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb
index 432040c4a14..3889d1fc8c9 100644
--- a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb
+++ b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'MATLAB.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
index eca79f37779..42df924f8fd 100644
--- a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Terraform/Base.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
index 0ab81f97f20..332708ffa13 100644
--- a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
index d6c7cd32f79..0f0192ad38f 100644
--- a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do
let(:default_branch) { 'master' }
let(:pipeline_ref) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index 1a909f52ec3..b2ca906e172 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
index de94eec09fe..afb7773ad7a 100644
--- a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Flutter.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
index ebf52e6d65a..62e4188f59b 100644
--- a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'Kaniko.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'Dockerfile' => 'FROM alpine:latest' }) }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb
index 5a62324da74..a44833b0c01 100644
--- a/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/katalon_gitlab_ci_yaml_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Katalon.gitlab-ci.yml' do
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: 'master' ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb
index d86a3a67823..55fd4675f11 100644
--- a/spec/lib/gitlab/ci/templates/npm_spec.rb
+++ b/spec/lib/gitlab/ci/templates/npm_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'npm.gitlab-ci.yml' do
let(:pipeline_tag) { 'v1.2.1' }
let(:pipeline_ref) { pipeline_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
def create_branch(name:)
@@ -42,7 +42,8 @@ RSpec.describe 'npm.gitlab-ci.yml' do
shared_examples 'no pipeline created' do
it 'does not create a pipeline because the only job (publish) is not created' do
- expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError, 'No stages / jobs for this pipeline.')
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
diff --git a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
index 2fc4b509aab..aa7d0249066 100644
--- a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe 'Terraform.gitlab-ci.yml' do
describe 'the created pipeline' do
let(:default_branch) { project.default_branch_or_main }
let(:pipeline_branch) { default_branch }
- let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
+ let_it_be(:project) { create(:project, :repository, create_branch: 'patch-1') }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -27,23 +27,30 @@ RSpec.describe 'Terraform.gitlab-ci.yml' do
end
context 'on master branch' do
- it 'creates init, validate and build jobs', :aggregate_failures do
+ it 'creates init, validate,build terraform jobs as well as kics-iac-sast job', :aggregate_failures do
expect(pipeline.errors).to be_empty
- expect(build_names).to include('validate', 'build', 'deploy')
+ expect(build_names).to include('kics-iac-sast', 'validate', 'build', 'deploy')
end
end
context 'outside the master branch' do
let(:pipeline_branch) { 'patch-1' }
- before do
- project.repository.create_branch(pipeline_branch, default_branch)
- end
-
it 'does not creates a deploy and a test job', :aggregate_failures do
expect(pipeline.errors).to be_empty
expect(build_names).not_to include('deploy')
end
end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request).payload }
+
+ it 'creates a pipeline with no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(pipeline.builds.count).to be_zero
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
index 42e56c4ab3c..6ae51f9783b 100644
--- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb
@@ -12,10 +12,10 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
describe 'the created pipeline' do
let(:default_branch) { project.default_branch_or_main }
let(:pipeline_branch) { default_branch }
- let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
- let(:user) { project.first_owner }
+ let_it_be(:project) { create(:project, :repository, create_branch: 'patch-1') }
+ let_it_be(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -36,14 +36,38 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do
context 'outside the master branch' do
let(:pipeline_branch) { 'patch-1' }
- before do
- project.repository.create_branch(pipeline_branch, default_branch)
- end
-
it 'does not creates a deploy and a test job', :aggregate_failures do
expect(pipeline.errors).to be_empty
expect(build_names).not_to include('deploy')
end
end
+
+ context 'on merge request' do
+ let(:pipeline_branch) { 'patch-1' }
+ let(:mr_service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: pipeline_branch ) }
+ let(:mr_pipeline) { mr_service.execute(merge_request).payload }
+ let(:mr_build_names) { mr_pipeline.builds.pluck(:name) }
+ let(:branch_service) { Ci::CreatePipelineService.new(project, user, ref: merge_request.source_branch ) }
+ let(:branch_pipeline) { branch_service.execute(:push).payload }
+ let(:branch_build_names) { branch_pipeline.builds.pluck(:name) }
+
+ # This is needed so that the terraform artifacts and sast_iac artifacts
+ # are both available in the MR
+ it 'creates a pipeline with the terraform and sast_iac jobs' do
+ expect(mr_pipeline).to be_merge_request_event
+ expect(mr_pipeline.errors.full_messages).to be_empty
+ expect(mr_build_names).to include('kics-iac-sast', 'validate', 'build')
+ end
+
+ it 'does not creates a deploy', :aggregate_failures do
+ expect(mr_build_names).not_to include('deploy')
+ end
+
+ it 'does not create a branch pipeline', :aggregate_failures do
+ expect(branch_build_names).to be_empty
+ expect(branch_pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/templates/themekit_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/themekit_gitlab_ci_yaml_spec.rb
index 4708108f404..157fd39f1cc 100644
--- a/spec/lib/gitlab/ci/templates/themekit_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/themekit_gitlab_ci_yaml_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'ThemeKit.gitlab-ci.yml' do
let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) }
let(:user) { project.first_owner }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
let(:build_names) { pipeline.builds.pluck(:name) }
before do
@@ -51,9 +51,8 @@ RSpec.describe 'ThemeKit.gitlab-ci.yml' do
end
it 'has no jobs' do
- expect { pipeline }.to raise_error(
- Ci::CreatePipelineService::CreateError, 'No stages / jobs for this pipeline.'
- )
+ expect(build_names).to be_empty
+ expect(pipeline.errors.full_messages).to match_array(["No stages / jobs for this pipeline."])
end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index 9443bf6d6d5..f7c6f7f51df 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -197,11 +197,11 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
end
end
- describe '#raw' do
+ describe '#raw?' do
it 'returns false when :raw is not specified' do
item = described_class.new(**variable)
- expect(item.raw).to eq false
+ expect(item.raw?).to eq false
end
context 'when :raw is specified as true' do
@@ -212,7 +212,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Item do
it 'returns true' do
item = described_class.new(**variable)
- expect(item.raw).to eq true
+ expect(item.raw?).to eq true
end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 7d4a1eef70b..10b8f0065d9 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -300,7 +300,6 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_JOB_NAME', value: 'test-1')
.append(key: 'CI_BUILD_ID', value: '1')
- .append(key: 'RAW_VAR', value: '$TEST1', raw: true)
.append(key: 'TEST1', value: 'test-3')
.append(key: 'FILEVAR1', value: 'file value 1', file: true)
end
@@ -322,10 +321,6 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
value: 'key${TEST1}-${CI_JOB_NAME}',
result: 'keytest-3-test-1'
},
- "complex expansions with raw variable": {
- value: 'key${RAW_VAR}-${CI_JOB_NAME}',
- result: 'key$TEST1-test-1'
- },
"missing variable not keeping original": {
value: 'key${MISSING_VAR}-${CI_JOB_NAME}',
result: 'key-test-1'
@@ -339,22 +334,22 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
value: 'key-$TEST1-%%HOME%%-$${HOME}',
result: 'key-test-3-%%HOME%%-$${HOME}'
},
- "file variable with expand_file_vars: true": {
+ "file variable with expand_file_refs: true": {
value: 'key-$FILEVAR1-$TEST1',
result: 'key-file value 1-test-3'
},
- "file variable with expand_file_vars: false": {
+ "file variable with expand_file_refs: false": {
value: 'key-$FILEVAR1-$TEST1',
result: 'key-$FILEVAR1-test-3',
- expand_file_vars: false
+ expand_file_refs: false
}
}
end
with_them do
- let(:options) { { keep_undefined: keep_undefined, expand_file_vars: expand_file_vars }.compact }
+ let(:options) { { keep_undefined: keep_undefined, expand_file_refs: expand_file_refs }.compact }
- subject(:result) { collection.expand_value(value, **options) }
+ subject(:expanded_result) { collection.expand_value(value, **options) }
it 'matches expected expansion' do
is_expected.to eq(result)
@@ -509,17 +504,35 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
{ key: 'variable4', value: 'keyvalue${variable2}value3' }
]
},
- "complex expansions with raw variable": {
+ "complex expansions with raw variable with expand_raw_refs: true (default)": {
+ variables: [
+ { key: 'variable1', value: 'value1' },
+ { key: 'raw_var', value: 'raw-$variable1', raw: true },
+ { key: 'nonraw_var', value: 'nonraw-$variable1' },
+ { key: 'variable2', value: '$raw_var and $nonraw_var' }
+ ],
+ keep_undefined: false,
+ result: [
+ { key: 'variable1', value: 'value1' },
+ { key: 'raw_var', value: 'raw-$variable1', raw: true },
+ { key: 'nonraw_var', value: 'nonraw-value1' },
+ { key: 'variable2', value: 'raw-$variable1 and nonraw-value1' }
+ ]
+ },
+ "complex expansions with raw variable with expand_raw_refs: false": {
variables: [
- { key: 'variable3', value: 'key_${variable}_${variable2}' },
- { key: 'variable', value: '$variable2', raw: true },
- { key: 'variable2', value: 'value2' }
+ { key: 'variable1', value: 'value1' },
+ { key: 'raw_var', value: 'raw-$variable1', raw: true },
+ { key: 'nonraw_var', value: 'nonraw-$variable1' },
+ { key: 'variable2', value: '$raw_var and $nonraw_var' }
],
keep_undefined: false,
+ expand_raw_refs: false,
result: [
- { key: 'variable', value: '$variable2', raw: true },
- { key: 'variable2', value: 'value2' },
- { key: 'variable3', value: 'key_$variable2_value2' }
+ { key: 'variable1', value: 'value1' },
+ { key: 'raw_var', value: 'raw-$variable1', raw: true },
+ { key: 'nonraw_var', value: 'nonraw-value1' },
+ { key: 'variable2', value: '$raw_var and nonraw-value1' }
]
},
"variable value referencing password with special characters": {
@@ -553,8 +566,9 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
with_them do
let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
+ let(:options) { { keep_undefined: keep_undefined, expand_raw_refs: expand_raw_refs }.compact }
- subject { collection.sort_and_expand_all(keep_undefined: keep_undefined) }
+ subject(:expanded_result) { collection.sort_and_expand_all(**options) }
it 'returns Collection' do
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
@@ -601,7 +615,8 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
it 'logs file_variable_is_referenced_in_another_variable once for VAR5' do
expect(Gitlab::AppJsonLogger).to receive(:info).with(
event: 'file_variable_is_referenced_in_another_variable',
- project_id: project.id
+ project_id: project.id,
+ variable: 'FILEVAR4'
).once
sort_and_expand_all
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index ebf8422489e..5de813f7739 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1071,6 +1071,7 @@ module Gitlab
let(:build) { execute.builds.first }
let(:job_variables) { build[:job_variables] }
+ let(:root_variables) { execute.root_variables }
let(:root_variables_inheritance) { build[:root_variables_inheritance] }
context 'when global variables are defined' do
@@ -1193,6 +1194,78 @@ module Gitlab
expect(root_variables_inheritance).to eq(true)
end
end
+
+ context 'when variables have data other than value' do
+ let(:config) do
+ <<~YAML
+ variables:
+ VAR1: value1
+ VAR2:
+ value: value2
+ description: description2
+ VAR3:
+ value: value3
+ expand: false
+
+ rspec:
+ script: rspec
+ variables:
+ VAR4: value4
+ VAR5:
+ value: value5
+ expand: false
+ VAR6:
+ value: value6
+ expand: true
+ YAML
+ end
+
+ it 'returns variables' do
+ expect(job_variables).to contain_exactly(
+ { key: 'VAR4', value: 'value4' },
+ { key: 'VAR5', value: 'value5', raw: true },
+ { key: 'VAR6', value: 'value6', raw: false }
+ )
+
+ expect(execute.root_variables).to contain_exactly(
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2' },
+ { key: 'VAR3', value: 'value3', raw: true }
+ )
+
+ expect(execute.root_variables_with_prefill_data).to eq(
+ 'VAR1' => { value: 'value1' },
+ 'VAR2' => { value: 'value2', description: 'description2' },
+ 'VAR3' => { value: 'value3', raw: true }
+ )
+ end
+
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
+ before do
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
+ end
+
+ it 'returns variables without description and raw' do
+ expect(job_variables).to contain_exactly(
+ { key: 'VAR4', value: 'value4' },
+ { key: 'VAR5', value: 'value5' },
+ { key: 'VAR6', value: 'value6' }
+ )
+
+ expect(execute.root_variables).to contain_exactly(
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2' },
+ { key: 'VAR3', value: 'value3' }
+ )
+
+ expect(execute.root_variables_with_prefill_data).to eq(
+ 'VAR1' => { value: 'value1' },
+ 'VAR2' => { value: 'value2', description: 'description2' },
+ 'VAR3' => { value: 'value3' }
+ )
+ end
+ end
+ end
end
context 'when using `extends`' do
@@ -1334,7 +1407,7 @@ module Gitlab
context "when an array of wrong keyed object is provided" do
let(:include_content) { [{ yolo: "/local.gitlab-ci.yml" }] }
- it_behaves_like 'returns errors', /needs to match exactly one accessor/
+ it_behaves_like 'returns errors', /does not have a valid subkey for include/
end
context "when an array of mixed typed objects is provided" do
@@ -1359,7 +1432,7 @@ module Gitlab
context "when the include type is incorrect" do
let(:include_content) { { name: "/local.gitlab-ci.yml" } }
- it_behaves_like 'returns errors', /needs to match exactly one accessor/
+ it_behaves_like 'returns errors', /does not have a valid subkey for include/
end
end
diff --git a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
index 5eea78acd98..45becb8370c 100644
--- a/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
+++ b/spec/lib/gitlab/cluster/lifecycle_events_spec.rb
@@ -3,38 +3,55 @@
require 'spec_helper'
RSpec.describe Gitlab::Cluster::LifecycleEvents do
+ using RSpec::Parameterized::TableSyntax
+
# we create a new instance to ensure that we do not touch existing hooks
let(:replica) { Class.new(described_class) }
- context 'hooks execution' do
- using RSpec::Parameterized::TableSyntax
+ before do
+ # disable blackout period to speed-up tests
+ stub_config(shutdown: { blackout_seconds: 0 })
+ end
- where(:method, :hook_names) do
- :do_worker_start | %i[worker_start_hooks]
- :do_before_fork | %i[before_fork_hooks]
- :do_before_graceful_shutdown | %i[master_blackout_period master_graceful_shutdown]
- :do_before_master_restart | %i[master_restart_hooks]
+ context 'outside of clustered environments' do
+ where(:hook, :was_executed_immediately) do
+ :on_worker_start | true
+ :on_before_fork | false
+ :on_before_graceful_shutdown | false
+ :on_before_master_restart | false
+ :on_worker_stop | false
end
- before do
- # disable blackout period to speed-up tests
- stub_config(shutdown: { blackout_seconds: 0 })
+ with_them do
+ it 'executes the given block immediately' do
+ was_executed = false
+ replica.public_send(hook, &proc { was_executed = true })
+
+ expect(was_executed).to eq(was_executed_immediately)
+ end
end
+ end
- with_them do
- subject { replica.public_send(method) }
+ context 'in clustered environments' do
+ before do
+ allow(Gitlab::Runtime).to receive(:puma?).and_return(true)
+ replica.set_puma_options(workers: 2)
+ end
- it 'executes all hooks' do
- hook_names.each do |hook_name|
- hook = double
- replica.instance_variable_set(:"@#{hook_name}", [hook])
+ where(:hook, :execution_helper) do
+ :on_worker_start | :do_worker_start
+ :on_before_fork | :do_before_fork
+ :on_before_graceful_shutdown | :do_before_graceful_shutdown
+ :on_before_master_restart | :do_before_master_restart
+ :on_worker_stop | :do_worker_stop
+ end
- # ensure that proper hooks are called
- expect(hook).to receive(:call)
- expect(replica).to receive(:call).with(hook_name, anything).and_call_original
- end
+ with_them do
+ it 'requires explicit execution via do_* helper' do
+ was_executed = false
+ replica.public_send(hook, &proc { was_executed = true })
- subject
+ expect { replica.public_send(execution_helper) }.to change { was_executed }.from(false).to(true)
end
end
end
diff --git a/spec/lib/gitlab/cluster/puma_worker_killer_initializer_spec.rb b/spec/lib/gitlab/cluster/puma_worker_killer_initializer_spec.rb
new file mode 100644
index 00000000000..cb13a711857
--- /dev/null
+++ b/spec/lib/gitlab/cluster/puma_worker_killer_initializer_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'puma_worker_killer'
+
+RSpec.describe Gitlab::Cluster::PumaWorkerKillerInitializer do
+ describe '.start' do
+ context 'when GITLAB_MEMORY_WATCHDOG_ENABLED is false' do
+ before do
+ stub_env('GITLAB_MEMORY_WATCHDOG_ENABLED', 'false')
+ end
+
+ it 'configures and start PumaWorkerKiller' do
+ expect(PumaWorkerKiller).to receive(:config)
+ expect(PumaWorkerKiller).to receive(:start)
+
+ described_class.start({})
+ end
+ end
+
+ context 'when GITLAB_MEMORY_WATCHDOG_ENABLED is not set' do
+ it 'configures and start PumaWorkerKiller' do
+ expect(PumaWorkerKiller).not_to receive(:config)
+ expect(PumaWorkerKiller).not_to receive(:start)
+
+ described_class.start({})
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
index 9af6aed2b02..963c9fe1576 100644
--- a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
+++ b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
end
it 'reports deprecated database notice' do
- is_expected.to contain_exactly(notice_deprecated_database(old_database_version))
+ is_expected.to contain_exactly(notice_deprecated_database('main', old_database_version))
end
end
end
@@ -59,13 +59,13 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
it 'reports deprecated database notice if the main database is using an old version' do
allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(old_database)
allow(Gitlab::Database::Reflection).to receive(:new).with(Ci::ApplicationRecord).and_return(new_database)
- is_expected.to contain_exactly(notice_deprecated_database(old_database_version))
+ is_expected.to contain_exactly(notice_deprecated_database('main', old_database_version))
end
it 'reports deprecated database notice if the ci database is using an old version' do
allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(new_database)
allow(Gitlab::Database::Reflection).to receive(:new).with(Ci::ApplicationRecord).and_return(old_database)
- is_expected.to contain_exactly(notice_deprecated_database(old_database_version))
+ is_expected.to contain_exactly(notice_deprecated_database('ci', old_database_version))
end
end
@@ -77,22 +77,23 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
it 'reports deprecated database notice' do
is_expected.to match_array [
- notice_deprecated_database(old_database_version),
- notice_deprecated_database(old_database_version)
+ notice_deprecated_database('main', old_database_version),
+ notice_deprecated_database('ci', old_database_version)
]
end
end
end
end
- def notice_deprecated_database(database_version)
+ def notice_deprecated_database(database_name, database_version)
{
type: 'warning',
- message: _('You are using PostgreSQL %{pg_version_current}, but PostgreSQL ' \
- '%{pg_version_minimum} is required for this version of GitLab. ' \
- 'Please upgrade your environment to a supported PostgreSQL version, ' \
- 'see %{pg_requirements_url} for details.') % \
+ message: _('Database \'%{database_name}\' is using PostgreSQL %{pg_version_current}, ' \
+ 'but PostgreSQL %{pg_version_minimum} is required for this version of GitLab. ' \
+ 'Please upgrade your environment to a supported PostgreSQL version, ' \
+ 'see %{pg_requirements_url} for details.') % \
{
+ database_name: database_name,
pg_version_current: database_version,
pg_version_minimum: Gitlab::Database::MINIMUM_POSTGRES_VERSION,
pg_requirements_url: Gitlab::ConfigChecker::ExternalDatabaseChecker::PG_REQUIREMENTS_LINK
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 1fa6eee9813..165305476d2 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -3,18 +3,15 @@
require 'spec_helper'
RSpec.describe Gitlab::Conflict::File do
- include GitHelpers
-
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:rugged) { rugged_repo(repository) }
- let(:their_commit) { rugged.branches['conflict-start'].target }
- let(:our_commit) { rugged.branches['conflict-resolvable'].target }
+ let(:their_commit) { TestEnv::BRANCH_SHA['conflict-start'] }
+ let(:our_commit) { TestEnv::BRANCH_SHA['conflict-resolvable'] }
let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
- let(:index) { rugged.merge_commits(our_commit, their_commit) }
- let(:rugged_conflict) { index.conflicts.last }
- let(:raw_conflict_content) { index.merge_file('files/ruby/regex.rb')[:data] }
- let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit.oid, rugged_conflict, raw_conflict_content) }
+ let(:conflicts_client) { repository.gitaly_conflicts_client(our_commit, their_commit) }
+ let(:raw_conflict_files) { conflicts_client.list_conflict_files }
+ let(:conflict_file_name) { 'files/ruby/regex.rb' }
+ let(:raw_conflict_file) { raw_conflict_files.find { |conflict| conflict.our_path == conflict_file_name } }
let(:conflict_file) { described_class.new(raw_conflict_file, merge_request: merge_request) }
describe 'delegates' do
@@ -137,8 +134,7 @@ RSpec.describe Gitlab::Conflict::File do
end
context 'when there are unchanged trailing lines' do
- let(:rugged_conflict) { index.conflicts.first }
- let(:raw_conflict_content) { index.merge_file('files/ruby/popen.rb')[:data] }
+ let(:conflict_file_name) { 'files/ruby/popen.rb' }
it 'assign conflict types and adds match line to the end of the section' do
expect(diff_line_types).to eq(
@@ -294,6 +290,8 @@ RSpec.describe Gitlab::Conflict::File do
FILE
end
+ let(:conflict) { { ancestor: { path: '' }, theirs: { path: conflict_file_name }, ours: { path: conflict_file_name } } }
+ let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit, conflict, raw_conflict_content) }
let(:sections) { conflict_file.sections }
it 'sets the correct match line headers' do
@@ -324,7 +322,7 @@ RSpec.describe Gitlab::Conflict::File do
describe '#as_json' do
it 'includes the blob path for the file' do
expect(conflict_file.as_json[:blob_path])
- .to eq("/#{project.full_path}/-/blob/#{our_commit.oid}/files/ruby/regex.rb")
+ .to eq("/#{project.full_path}/-/blob/#{our_commit}/files/ruby/regex.rb")
end
it 'includes the blob icon for the file' do
@@ -341,7 +339,8 @@ RSpec.describe Gitlab::Conflict::File do
describe '#conflict_type' do
using RSpec::Parameterized::TableSyntax
- let(:rugged_conflict) { { ancestor: { path: ancestor_path }, theirs: { path: their_path }, ours: { path: our_path } } }
+ let(:conflict) { { ancestor: { path: ancestor_path }, theirs: { path: their_path }, ours: { path: our_path } } }
+ let(:raw_conflict_file) { Gitlab::Git::Conflict::File.new(repository, our_commit, conflict, '') }
let(:diff_file) { double(renamed_file?: renamed_file?) }
subject(:conflict_type) { conflict_file.conflict_type(diff_file) }
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 6b1d8d8d1af..aadfb41a46e 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -53,6 +53,18 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
expect(directives['child_src']).to eq("#{directives['frame_src']} #{directives['worker_src']}")
end
+ describe 'the images-src directive' do
+ it 'can be loaded from anywhere' do
+ expect(directives['img_src']).to include('http: https:')
+ end
+ end
+
+ describe 'the media-src directive' do
+ it 'can be loaded from anywhere' do
+ expect(directives['media_src']).to include('http: https:')
+ end
+ end
+
context 'adds all websocket origins to support Safari' do
it 'with insecure domain' do
stub_config_setting(host: 'example.com', https: false)
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 2c239d5868a..544b210651b 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -3,10 +3,9 @@
require 'spec_helper'
RSpec.describe Gitlab::DataBuilder::Build do
- let!(:tag_names) { %w(tag-1 tag-2) }
- let(:runner) { create(:ci_runner, :instance, tag_list: tag_names.map { |n| ActsAsTaggableOn::Tag.create!(name: n) }) }
- let(:user) { create(:user, :public_email) }
- let(:build) { create(:ci_build, :running, runner: runner, user: user) }
+ let_it_be(:runner) { create(:ci_runner, :instance, :tagged_only) }
+ let_it_be(:user) { create(:user, :public_email) }
+ let_it_be(:ci_build) { create(:ci_build, :running, runner: runner, user: user) }
describe '.build' do
around do |example|
@@ -14,25 +13,26 @@ RSpec.describe Gitlab::DataBuilder::Build do
end
let(:data) do
- described_class.build(build)
+ described_class.build(ci_build)
end
it { expect(data).to be_a(Hash) }
- it { expect(data[:ref]).to eq(build.ref) }
- it { expect(data[:sha]).to eq(build.sha) }
- it { expect(data[:tag]).to eq(build.tag) }
- it { expect(data[:build_id]).to eq(build.id) }
- it { expect(data[:build_status]).to eq(build.status) }
- it { expect(data[:build_created_at]).to eq(build.created_at) }
- it { expect(data[:build_started_at]).to eq(build.started_at) }
- it { expect(data[:build_finished_at]).to eq(build.finished_at) }
- it { expect(data[:build_duration]).to eq(build.duration) }
- it { expect(data[:build_queued_duration]).to eq(build.queued_duration) }
+ it { expect(data[:ref]).to eq(ci_build.ref) }
+ it { expect(data[:sha]).to eq(ci_build.sha) }
+ it { expect(data[:tag]).to eq(ci_build.tag) }
+ it { expect(data[:build_id]).to eq(ci_build.id) }
+ it { expect(data[:build_status]).to eq(ci_build.status) }
+ it { expect(data[:build_created_at]).to eq(ci_build.created_at) }
+ it { expect(data[:build_started_at]).to eq(ci_build.started_at) }
+ it { expect(data[:build_finished_at]).to eq(ci_build.finished_at) }
+ it { expect(data[:build_duration]).to eq(ci_build.duration) }
+ it { expect(data[:build_queued_duration]).to eq(ci_build.queued_duration) }
it { expect(data[:build_allow_failure]).to eq(false) }
- it { expect(data[:build_failure_reason]).to eq(build.failure_reason) }
- it { expect(data[:project_id]).to eq(build.project.id) }
- it { expect(data[:project_name]).to eq(build.project.full_name) }
- it { expect(data[:pipeline_id]).to eq(build.pipeline.id) }
+ it { expect(data[:build_failure_reason]).to eq(ci_build.failure_reason) }
+ it { expect(data[:project_id]).to eq(ci_build.project.id) }
+ it { expect(data[:project_name]).to eq(ci_build.project.full_name) }
+ it { expect(data[:pipeline_id]).to eq(ci_build.pipeline.id) }
+ it { expect(data[:retries_count]).to eq(ci_build.retries_count) }
it {
expect(data[:user]).to eq(
@@ -45,44 +45,74 @@ RSpec.describe Gitlab::DataBuilder::Build do
})
}
- it { expect(data[:commit][:id]).to eq(build.pipeline.id) }
- 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[:commit][:id]).to eq(ci_build.pipeline.id) }
+ it { expect(data[:runner][:id]).to eq(ci_build.runner.id) }
+ it { expect(data[:runner][:tags]).to match_array(%w(tag1 tag2)) }
+ it { expect(data[:runner][:description]).to eq(ci_build.runner.description) }
+ it { expect(data[:runner][:runner_type]).to eq(ci_build.runner.runner_type) }
+ it { expect(data[:runner][:is_shared]).to eq(ci_build.runner.instance_type?) }
it { expect(data[:environment]).to be_nil }
+ it 'does not exceed number of expected queries' do
+ ci_build # Make sure the Ci::Build model is created before recording.
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ b = Ci::Build.find(ci_build.id)
+ described_class.build(b) # Don't use ci_build variable here since it has all associations loaded into memory
+ end
+
+ expect(control.count).to eq(13)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(job_webhook_retries_count: false)
+ end
+
+ it { expect(data).not_to have_key(:retries_count) }
+
+ it 'does not exceed number of expected queries' do
+ ci_build # Make sure the Ci::Build model is created before recording.
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ b = Ci::Build.find(ci_build.id)
+ described_class.build(b) # Don't use ci_build variable here since it has all associations loaded into memory
+ end
+
+ expect(control.count).to eq(12)
+ end
+ end
+
context 'commit author_url' do
context 'when no commit present' do
- let(:build) { create(:ci_build) }
+ let(:build) { build(:ci_build) }
it 'sets to mailing address of git_author_email' do
- expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}")
+ expect(data[:commit][:author_url]).to eq("mailto:#{ci_build.pipeline.git_author_email}")
end
end
context 'when commit present but has no author' do
- let(:build) { create(:ci_build, :with_commit) }
+ let(:ci_build) { build(:ci_build, :with_commit) }
it 'sets to mailing address of git_author_email' do
- expect(data[:commit][:author_url]).to eq("mailto:#{build.pipeline.git_author_email}")
+ expect(data[:commit][:author_url]).to eq("mailto:#{ci_build.pipeline.git_author_email}")
end
end
context 'when commit and author are present' do
- let(:build) { create(:ci_build, :with_commit_and_author) }
+ let(:ci_build) { build(:ci_build, :with_commit_and_author) }
it 'sets to GitLab user url' do
- expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: build.commit.author.username))
+ expect(data[:commit][:author_url]).to eq(Gitlab::Routing.url_helpers.user_url(username: ci_build.commit.author.username))
end
end
context 'with environment' do
- let(:build) { create(:ci_build, :teardown_environment) }
+ let(:ci_build) { build(:ci_build, :teardown_environment) }
- it { expect(data[:environment][:name]).to eq(build.expanded_environment_name) }
- it { expect(data[:environment][:action]).to eq(build.environment_action) }
+ it { expect(data[:environment][:name]).to eq(ci_build.expanded_environment_name) }
+ it { expect(data[:environment][:action]).to eq(ci_build.environment_action) }
end
end
end
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 46a12d8c6f6..eb348f5b497 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -103,6 +103,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
expect(merge_request_attrs[:target_project_id]).to eq(merge_request.target_project_id)
expect(merge_request_attrs[:state]).to eq(merge_request.state)
expect(merge_request_attrs[:merge_status]).to eq(merge_request.public_merge_status)
+ expect(merge_request_attrs[:detailed_merge_status]).to eq("mergeable")
expect(merge_request_attrs[:url]).to eq("http://localhost/#{merge_request.target_project.full_path}/-/merge_requests/#{merge_request.iid}")
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 32746a46308..cc9f3d5b7f1 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -7,7 +7,15 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
it { is_expected.to be_a Gitlab::Database::SharedModel }
- it { expect(described_class::TIMEOUT_EXCEPTIONS).to match_array [ActiveRecord::StatementTimeout, ActiveRecord::ConnectionTimeoutError, ActiveRecord::AdapterTimeout, ActiveRecord::LockWaitTimeout] }
+ specify do
+ expect(described_class::TIMEOUT_EXCEPTIONS).to contain_exactly(
+ ActiveRecord::StatementTimeout,
+ ActiveRecord::ConnectionTimeoutError,
+ ActiveRecord::AdapterTimeout,
+ ActiveRecord::LockWaitTimeout,
+ ActiveRecord::QueryCanceled
+ )
+ end
describe 'associations' do
it { is_expected.to belong_to(:batched_migration).with_foreign_key(:batched_background_migration_id) }
@@ -272,7 +280,13 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
context 'when is a timeout exception' do
let(:exception) { ActiveRecord::StatementTimeout.new }
- it { expect(subject).to be_truthy }
+ it { expect(subject).to be_truthy }
+ end
+
+ context 'when is a QueryCanceled exception' do
+ let(:exception) { ActiveRecord::QueryCanceled.new }
+
+ it { expect(subject).to be_truthy }
end
context 'when is not a timeout exception' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index 1ac9cbae036..31ae5e9b55d 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -211,6 +211,102 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
expect(active_migration).to eq(migration3)
end
end
+
+ context 'when there are no active migrations available' do
+ it 'returns nil' do
+ expect(active_migration).to eq(nil)
+ end
+ end
+ end
+
+ describe '.find_executable' do
+ let(:connection) { Gitlab::Database.database_base_models[:main].connection }
+ let(:migration_id) { migration.id }
+
+ subject(:executable_migration) { described_class.find_executable(migration_id, connection: connection) }
+
+ around do |example|
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ example.run
+ end
+ end
+
+ context 'when the migration does not exist' do
+ let(:migration_id) { non_existing_record_id }
+
+ it 'returns nil' do
+ expect(executable_migration).to be_nil
+ end
+ end
+
+ context 'when the migration is not active' do
+ let!(:migration) { create(:batched_background_migration, :finished) }
+
+ it 'returns nil' do
+ expect(executable_migration).to be_nil
+ end
+ end
+
+ context 'when the migration is on hold' do
+ let!(:migration) { create(:batched_background_migration, :active, on_hold_until: 10.minutes.from_now) }
+
+ it 'returns nil' do
+ expect(executable_migration).to be_nil
+ end
+ end
+
+ context 'when the migration is not available for the current connection' do
+ let!(:migration) { create(:batched_background_migration, :active, gitlab_schema: :gitlab_not_existing) }
+
+ it 'returns nil' do
+ expect(executable_migration).to be_nil
+ end
+ end
+
+ context 'when ther migration exists and is executable' do
+ let!(:migration) { create(:batched_background_migration, :active, gitlab_schema: :gitlab_main) }
+
+ it 'returns the migration' do
+ expect(executable_migration).to eq(migration)
+ end
+ end
+ end
+
+ describe '.active_migrations_distinct_on_table' do
+ let(:connection) { Gitlab::Database.database_base_models[:main].connection }
+
+ around do |example|
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ example.run
+ end
+ end
+
+ it 'returns one pending executable migration per table' do
+ # non-active migration
+ create(:batched_background_migration, :finished)
+ # migration put on hold
+ create(:batched_background_migration, :active, on_hold_until: 10.minutes.from_now)
+ # migration not availab for the current connection
+ create(:batched_background_migration, :active, gitlab_schema: :gitlab_not_existing)
+ # active migration that is no longer on hold
+ migration_1 = create(:batched_background_migration, :active, table_name: :users, on_hold_until: 10.minutes.ago)
+ # another active migration for the same table
+ create(:batched_background_migration, :active, table_name: :users)
+ # active migration for different table
+ migration_2 = create(:batched_background_migration, :active, table_name: :projects)
+ # active migration for third table
+ create(:batched_background_migration, :active, table_name: :namespaces)
+
+ actual = described_class.active_migrations_distinct_on_table(connection: connection, limit: 2)
+
+ expect(actual).to eq([migration_1, migration_2])
+ end
+
+ it 'returns epmty collection when there are no pending executable migrations' do
+ actual = described_class.active_migrations_distinct_on_table(connection: connection, limit: 2)
+
+ expect(actual).to be_empty
+ end
end
describe '.created_after' do
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index a87b0c1a3a8..852cc719d01 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -330,7 +330,7 @@ RSpec.describe Gitlab::Database::BatchCount do
end
it 'counts with "id" field' do
- expect(described_class.batch_distinct_count(model, "#{column}")).to eq(2)
+ expect(described_class.batch_distinct_count(model, column.to_s)).to eq(2)
end
it 'counts with table.column field' do
diff --git a/spec/lib/gitlab/database/load_balancing/configuration_spec.rb b/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
index 34370c9a21f..7dc2e0be3e5 100644
--- a/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/configuration_spec.rb
@@ -23,7 +23,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
- use_tcp: false
+ use_tcp: false,
+ max_replica_pools: nil
)
expect(config.pool_size).to eq(Gitlab::Database.default_pool_size)
end
@@ -39,7 +40,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
replica_check_interval: 3,
hosts: %w[foo bar],
discover: {
- 'record' => 'foo.example.com'
+ record: 'foo.example.com',
+ max_replica_pools: 5
}
}
}
@@ -59,7 +61,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
- use_tcp: false
+ use_tcp: false,
+ max_replica_pools: 5
)
expect(config.pool_size).to eq(4)
end
@@ -95,7 +98,8 @@ RSpec.describe Gitlab::Database::LoadBalancing::Configuration, :request_store do
record_type: 'A',
interval: 60,
disconnect_timeout: 120,
- use_tcp: false
+ use_tcp: false,
+ max_replica_pools: nil
)
expect(config.pool_size).to eq(4)
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
index 41312dbedd6..a2076f5b950 100644
--- a/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/connection_proxy_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end
Gitlab::Database::LoadBalancing::ConnectionProxy::NON_STICKY_READS.each do |name|
- describe "#{name}" do
+ describe name.to_s do
it 'runs the query on the replica' do
expect(proxy).to receive(:read_using_load_balancer)
.with(name, 'foo')
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::ConnectionProxy do
end
Gitlab::Database::LoadBalancing::ConnectionProxy::STICKY_WRITES.each do |name|
- describe "#{name}" do
+ describe name.to_s do
it 'runs the query on the primary and sticks to it' do
session = Gitlab::Database::LoadBalancing::Session.new
diff --git a/spec/lib/gitlab/database/load_balancing/service_discovery/sampler_spec.rb b/spec/lib/gitlab/database/load_balancing/service_discovery/sampler_spec.rb
new file mode 100644
index 00000000000..1a49aa2871f
--- /dev/null
+++ b/spec/lib/gitlab/database/load_balancing/service_discovery/sampler_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Database::LoadBalancing::ServiceDiscovery::Sampler do
+ let(:sampler) { described_class.new(max_replica_pools: max_replica_pools, seed: 100) }
+ let(:max_replica_pools) { 3 }
+ let(:address_class) { ::Gitlab::Database::LoadBalancing::ServiceDiscovery::Address }
+ let(:addresses) do
+ [
+ address_class.new("127.0.0.1", 6432),
+ address_class.new("127.0.0.1", 6433),
+ address_class.new("127.0.0.1", 6434),
+ address_class.new("127.0.0.1", 6435),
+ address_class.new("127.0.0.2", 6432),
+ address_class.new("127.0.0.2", 6433),
+ address_class.new("127.0.0.2", 6434),
+ address_class.new("127.0.0.2", 6435)
+ ]
+ end
+
+ describe '#sample' do
+ it 'samples max_replica_pools addresses' do
+ expect(sampler.sample(addresses).count).to eq(max_replica_pools)
+ end
+
+ it 'samples random ports across all hosts' do
+ expect(sampler.sample(addresses)).to eq([
+ address_class.new("127.0.0.1", 6432),
+ address_class.new("127.0.0.2", 6435),
+ address_class.new("127.0.0.1", 6435)
+ ])
+ end
+
+ it 'returns the same answer for the same input when called multiple times' do
+ result = sampler.sample(addresses)
+ expect(sampler.sample(addresses)).to eq(result)
+ expect(sampler.sample(addresses)).to eq(result)
+ end
+
+ it 'gives a consistent answer regardless of input ordering' do
+ expect(sampler.sample(addresses.reverse)).to eq(sampler.sample(addresses))
+ end
+
+ it 'samples fairly across all hosts' do
+ # Choose a bunch of different seeds to prove that it always chooses 2
+ # different ports from each host when selecting 4
+ (1..10).each do |seed|
+ sampler = described_class.new(max_replica_pools: 4, seed: seed)
+
+ result = sampler.sample(addresses)
+
+ expect(result.count { |r| r.hostname == "127.0.0.1" }).to eq(2)
+ expect(result.count { |r| r.hostname == "127.0.0.2" }).to eq(2)
+ end
+ end
+
+ context 'when input is an empty array' do
+ it 'returns an empty array' do
+ expect(sampler.sample([])).to eq([])
+ end
+ end
+
+ context 'when there are less replicas than max_replica_pools' do
+ let(:max_replica_pools) { 100 }
+
+ it 'returns the same addresses' do
+ expect(sampler.sample(addresses)).to eq(addresses)
+ end
+ end
+
+ context 'when max_replica_pools is nil' do
+ let(:max_replica_pools) { nil }
+
+ it 'returns the same addresses' do
+ expect(sampler.sample(addresses)).to eq(addresses)
+ 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
index f05910e5123..984d60e9962 100644
--- a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb
@@ -231,10 +231,13 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
nameserver: 'localhost',
port: 8600,
record: 'foo',
- record_type: record_type
+ record_type: record_type,
+ max_replica_pools: max_replica_pools
)
end
+ let(:max_replica_pools) { nil }
+
let(:packet) { double(:packet, answer: [res1, res2]) }
before do
@@ -266,24 +269,51 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery do
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: []) }
+ let(:res4) { double(:resource, host: 'foo4.service.consul.', port: 5432, weight: 1, priority: 1, ttl: 90) }
+ let(:packet) { double(:packet, answer: [res1, res2, res3, res4], 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)
+ allow(resolver).to receive(:address_for).with('foo4.service.consul.').and_return("127.0.0.2")
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('127.0.0.2', 5432),
described_class::Address.new('255.255.255.0', 5432)
]
expect(service.addresses_from_dns).to eq([90, addresses])
end
+
+ context 'when max_replica_pools is set' do
+ context 'when the number of addresses exceeds max_replica_pools' do
+ let(:max_replica_pools) { 2 }
+
+ it 'limits to max_replica_pools' do
+ expect(service.addresses_from_dns[1].count).to eq(2)
+ end
+ end
+
+ context 'when the number of addresses is less than max_replica_pools' do
+ let(:max_replica_pools) { 5 }
+
+ it 'returns all addresses' do
+ addresses = [
+ described_class::Address.new('127.0.0.1', 5433),
+ described_class::Address.new('127.0.0.2', 5432),
+ described_class::Address.new('255.255.255.0', 5432)
+ ]
+
+ expect(service.addresses_from_dns).to eq([90, addresses])
+ end
+ end
+ end
end
context 'when the resolver returns an empty response' do
diff --git a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
index 88007de53d3..61b63016f1a 100644
--- a/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb
@@ -358,7 +358,7 @@ RSpec.describe Gitlab::Database::LoadBalancing::SidekiqServerMiddleware, :clean_
end
def process_job(job)
- Sidekiq::JobRetry.new.local(worker_class, job.to_json, 'default') do
+ Sidekiq::JobRetry.new(Sidekiq).local(worker_class, job.to_json, 'default') do
worker_class.process_job(job)
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb b/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb
index 6026d979bcf..1eb077fe6ca 100644
--- a/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/transaction_leaking_spec.rb
@@ -4,18 +4,18 @@ require 'spec_helper'
RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis, :delete do
include StubENV
- let(:model) { ApplicationRecord }
+ let(:model) { ActiveRecord::Base }
let(:db_host) { model.connection_pool.db_config.host }
let(:test_table_name) { '_test_foo' }
before do
# Patch in our load balancer config, simply pointing at the test database twice
- allow(Gitlab::Database::LoadBalancing::Configuration).to receive(:for_model) do |base_model|
+ allow(Gitlab::Database::LoadBalancing::Configuration).to receive(:for_model).with(model) do |base_model|
Gitlab::Database::LoadBalancing::Configuration.new(base_model, [db_host, db_host])
end
- Gitlab::Database::LoadBalancing::Setup.new(ApplicationRecord).setup
+ Gitlab::Database::LoadBalancing::Setup.new(model).setup
model.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS #{test_table_name} (id SERIAL PRIMARY KEY, value INTEGER)
@@ -30,6 +30,10 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis
model.connection.execute(<<~SQL)
DROP TABLE IF EXISTS #{test_table_name}
SQL
+
+ # reset load balancing to original state
+ allow(Gitlab::Database::LoadBalancing::Configuration).to receive(:for_model).and_call_original
+ Gitlab::Database::LoadBalancing::Setup.new(model).setup
end
def execute(conn)
@@ -56,6 +60,7 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis
conn = model.connection
expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:warn).with(hash_including(event: :transaction_leak))
+ expect(::Gitlab::Database::LoadBalancing::Logger).to receive(:warn).with(hash_including(event: :read_write_retry))
conn.transaction do
expect(conn).to be_transaction_open
@@ -74,6 +79,8 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis
expect(::Gitlab::Database::LoadBalancing::Logger)
.not_to receive(:warn).with(hash_including(event: :transaction_leak))
+ expect(::Gitlab::Database::LoadBalancing::Logger)
+ .to receive(:warn).with(hash_including(event: :read_write_retry))
expect(conn).not_to be_transaction_open
@@ -105,6 +112,8 @@ RSpec.describe 'Load balancer behavior with errors inside a transaction', :redis
it 'retries when not in a transaction' do
expect(::Gitlab::Database::LoadBalancing::Logger)
.not_to receive(:warn).with(hash_including(event: :transaction_leak))
+ expect(::Gitlab::Database::LoadBalancing::Logger)
+ .to receive(:warn).with(hash_including(event: :read_write_retry))
expect { execute(model.connection) }.not_to raise_error
end
diff --git a/spec/lib/gitlab/database/load_balancing_spec.rb b/spec/lib/gitlab/database/load_balancing_spec.rb
index 76dfaa74ae6..1c85abac91c 100644
--- a/spec/lib/gitlab/database/load_balancing_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing_spec.rb
@@ -468,9 +468,10 @@ RSpec.describe Gitlab::Database::LoadBalancing, :suppress_gitlab_schemas_validat
payload = event.payload
assert =
- if payload[:name] == 'SCHEMA'
+ case payload[:name]
+ when 'SCHEMA'
false
- elsif payload[:name] == 'SQL' # Custom query
+ when 'SQL' # Custom query
true
else
keywords = %w[_test_load_balancing_test]
diff --git a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
index 2055dc33d48..0d75094a2fd 100644
--- a/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers/v2_spec.rb
@@ -35,15 +35,15 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
end
end
- context 'when the existing column has a default value' do
+ context 'when the existing column has a default function' do
before do
- migration.change_column_default :_test_table, existing_column, 'default value'
+ migration.change_column_default :_test_table, existing_column, -> { 'now()' }
end
it 'raises an error' do
expect do
migration.public_send(operation, :_test_table, :original, :renamed)
- end.to raise_error("#{operation} does not currently support columns with default values")
+ end.to raise_error("#{operation} does not currently support columns with default functions")
end
end
@@ -67,6 +67,94 @@ RSpec.describe Gitlab::Database::MigrationHelpers::V2 do
end
end
+ context 'when the existing column has a default value' do
+ before do
+ migration.change_column_default :_test_table, existing_column, 'default value'
+ end
+
+ it 'creates the renamed column, syncing existing data' do
+ existing_record_1 = model.create!(status: 0, existing_column => 'existing')
+ existing_record_2 = model.create!(status: 0)
+
+ migration.send(operation, :_test_table, :original, :renamed)
+ model.reset_column_information
+
+ expect(migration.column_exists?(:_test_table, added_column)).to eq(true)
+
+ expect(existing_record_1.reload).to have_attributes(status: 0, original: 'existing', renamed: 'existing')
+ expect(existing_record_2.reload).to have_attributes(status: 0, original: 'default value', renamed: 'default value')
+ end
+
+ it 'installs triggers to sync new data' do
+ migration.public_send(operation, :_test_table, :original, :renamed)
+ model.reset_column_information
+
+ new_record_1 = model.create!(status: 1, original: 'first')
+ new_record_2 = model.create!(status: 1, renamed: 'second')
+ new_record_3 = model.create!(status: 1)
+ new_record_4 = model.create!(status: 1)
+
+ expect(new_record_1.reload).to have_attributes(status: 1, original: 'first', renamed: 'first')
+ expect(new_record_2.reload).to have_attributes(status: 1, original: 'second', renamed: 'second')
+ expect(new_record_3.reload).to have_attributes(status: 1, original: 'default value', renamed: 'default value')
+ expect(new_record_4.reload).to have_attributes(status: 1, original: 'default value', renamed: 'default value')
+
+ new_record_1.update!(original: 'updated')
+ new_record_2.update!(renamed: nil)
+ new_record_3.update!(renamed: 'update renamed')
+ new_record_4.update!(original: 'update original')
+
+ expect(new_record_1.reload).to have_attributes(status: 1, original: 'updated', renamed: 'updated')
+ expect(new_record_2.reload).to have_attributes(status: 1, original: nil, renamed: nil)
+ expect(new_record_3.reload).to have_attributes(status: 1, original: 'update renamed', renamed: 'update renamed')
+ expect(new_record_4.reload).to have_attributes(status: 1, original: 'update original', renamed: 'update original')
+ end
+ end
+
+ context 'when the existing column has a default value that evaluates to NULL' do
+ before do
+ migration.change_column_default :_test_table, existing_column, -> { "('test' || null)" }
+ end
+
+ it 'creates the renamed column, syncing existing data' do
+ existing_record_1 = model.create!(status: 0, existing_column => 'existing')
+ existing_record_2 = model.create!(status: 0)
+
+ migration.send(operation, :_test_table, :original, :renamed)
+ model.reset_column_information
+
+ expect(migration.column_exists?(:_test_table, added_column)).to eq(true)
+
+ expect(existing_record_1.reload).to have_attributes(status: 0, original: 'existing', renamed: 'existing')
+ expect(existing_record_2.reload).to have_attributes(status: 0, original: nil, renamed: nil)
+ end
+
+ it 'installs triggers to sync new data' do
+ migration.public_send(operation, :_test_table, :original, :renamed)
+ model.reset_column_information
+
+ new_record_1 = model.create!(status: 1, original: 'first')
+ new_record_2 = model.create!(status: 1, renamed: 'second')
+ new_record_3 = model.create!(status: 1)
+ new_record_4 = model.create!(status: 1)
+
+ expect(new_record_1.reload).to have_attributes(status: 1, original: 'first', renamed: 'first')
+ expect(new_record_2.reload).to have_attributes(status: 1, original: 'second', renamed: 'second')
+ expect(new_record_3.reload).to have_attributes(status: 1, original: nil, renamed: nil)
+ expect(new_record_4.reload).to have_attributes(status: 1, original: nil, renamed: nil)
+
+ new_record_1.update!(original: 'updated')
+ new_record_2.update!(renamed: nil)
+ new_record_3.update!(renamed: 'update renamed')
+ new_record_4.update!(original: 'update original')
+
+ expect(new_record_1.reload).to have_attributes(status: 1, original: 'updated', renamed: 'updated')
+ expect(new_record_2.reload).to have_attributes(status: 1, original: nil, renamed: nil)
+ expect(new_record_3.reload).to have_attributes(status: 1, original: 'update renamed', renamed: 'update renamed')
+ expect(new_record_4.reload).to have_attributes(status: 1, original: 'update original', renamed: 'update original')
+ end
+ end
+
it 'creates the renamed column, syncing existing data' do
existing_record_1 = model.create!(status: 0, existing_column => 'existing')
existing_record_2 = model.create!(status: 0, existing_column => nil)
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index bcdd5646994..65fbc8d9935 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -389,6 +389,40 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.add_concurrent_index(:users, :foo)
end
+
+ context 'when targeting a partition table' do
+ let(:schema) { 'public' }
+ let(:name) { '_test_partition_01' }
+ let(:identifier) { "#{schema}.#{name}" }
+
+ before do
+ model.execute(<<~SQL)
+ CREATE TABLE public._test_partitioned_table (
+ id serial NOT NULL,
+ partition_id serial NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ ) PARTITION BY LIST(partition_id);
+
+ CREATE TABLE #{identifier} PARTITION OF public._test_partitioned_table
+ FOR VALUES IN (1);
+ SQL
+ end
+
+ context 'when allow_partition is true' do
+ it 'creates the index concurrently' do
+ expect(model).to receive(:add_index).with(:_test_partition_01, :foo, algorithm: :concurrently)
+
+ model.add_concurrent_index(:_test_partition_01, :foo, allow_partition: true)
+ end
+ end
+
+ context 'when allow_partition is not provided' do
+ it 'raises ArgumentError' do
+ expect { model.add_concurrent_index(:_test_partition_01, :foo) }
+ .to raise_error(ArgumentError, /use add_concurrent_partitioned_index/)
+ end
+ end
+ end
end
context 'inside a transaction' do
@@ -435,6 +469,37 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.remove_concurrent_index(:users, :foo)
end
+ context 'when targeting a partition table' do
+ let(:schema) { 'public' }
+ let(:partition_table_name) { '_test_partition_01' }
+ let(:identifier) { "#{schema}.#{partition_table_name}" }
+ let(:index_name) { '_test_partitioned_index' }
+ let(:partition_index_name) { '_test_partition_01_partition_id_idx' }
+ let(:column_name) { 'partition_id' }
+
+ before do
+ model.execute(<<~SQL)
+ CREATE TABLE public._test_partitioned_table (
+ id serial NOT NULL,
+ partition_id serial NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ ) PARTITION BY LIST(partition_id);
+
+ CREATE INDEX #{index_name} ON public._test_partitioned_table(#{column_name});
+
+ CREATE TABLE #{identifier} PARTITION OF public._test_partitioned_table
+ FOR VALUES IN (1);
+ SQL
+ end
+
+ context 'when dropping an index on the partition table' do
+ it 'raises ArgumentError' do
+ expect { model.remove_concurrent_index(partition_table_name, column_name) }
+ .to raise_error(ArgumentError, /use remove_concurrent_partitioned_index_by_name/)
+ end
+ end
+ end
+
describe 'by index name' do
before do
allow(model).to receive(:index_exists_by_name?).with(:users, "index_x_by_y").and_return(true)
@@ -476,6 +541,36 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.remove_concurrent_index_by_name(:users, "index_x_by_y")
end
+
+ context 'when targeting a partition table' do
+ let(:schema) { 'public' }
+ let(:partition_table_name) { '_test_partition_01' }
+ let(:identifier) { "#{schema}.#{partition_table_name}" }
+ let(:index_name) { '_test_partitioned_index' }
+ let(:partition_index_name) { '_test_partition_01_partition_id_idx' }
+
+ before do
+ model.execute(<<~SQL)
+ CREATE TABLE public._test_partitioned_table (
+ id serial NOT NULL,
+ partition_id serial NOT NULL,
+ PRIMARY KEY (id, partition_id)
+ ) PARTITION BY LIST(partition_id);
+
+ CREATE INDEX #{index_name} ON public._test_partitioned_table(partition_id);
+
+ CREATE TABLE #{identifier} PARTITION OF public._test_partitioned_table
+ FOR VALUES IN (1);
+ SQL
+ end
+
+ context 'when dropping an index on the partition table' do
+ it 'raises ArgumentError' do
+ expect { model.remove_concurrent_index_by_name(partition_table_name, partition_index_name) }
+ .to raise_error(ArgumentError, /use remove_concurrent_partitioned_index_by_name/)
+ end
+ end
+ end
end
end
end
@@ -1006,88 +1101,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe '#disable_statement_timeout' do
- it 'disables statement timeouts to current transaction only' do
- expect(model).to receive(:execute).with('SET LOCAL statement_timeout TO 0')
-
- model.disable_statement_timeout
- end
-
- # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner)
- context 'with real environment', :delete do
- before do
- model.execute("SET statement_timeout TO '20000'")
- end
-
- after do
- model.execute('RESET statement_timeout')
- end
-
- it 'defines statement to 0 only for current transaction' do
- expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s')
-
- model.connection.transaction do
- model.disable_statement_timeout
- expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0')
- end
-
- expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s')
- end
-
- context 'when passing a blocks' do
- it 'disables statement timeouts on session level and executes the block' do
- expect(model).to receive(:execute).with('SET statement_timeout TO 0')
- expect(model).to receive(:execute).with('RESET statement_timeout').at_least(:once)
-
- expect { |block| model.disable_statement_timeout(&block) }.to yield_control
- end
-
- # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner)
- context 'with real environment', :delete do
- before do
- model.execute("SET statement_timeout TO '20000'")
- end
-
- after do
- model.execute('RESET statement_timeout')
- end
-
- it 'defines statement to 0 for any code run inside the block' do
- expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s')
-
- model.disable_statement_timeout do
- model.connection.transaction do
- expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0')
- end
-
- expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0')
- end
- end
- end
- end
- end
-
- # This spec runs without an enclosing transaction (:delete truncation method for db_cleaner)
- context 'when the statement_timeout is already disabled', :delete do
- before do
- ActiveRecord::Migration.connection.execute('SET statement_timeout TO 0')
- end
-
- after do
- # Use ActiveRecord::Migration.connection instead of model.execute
- # so that this call is not counted below
- ActiveRecord::Migration.connection.execute('RESET statement_timeout')
- end
-
- it 'yields control without disabling the timeout or resetting' do
- expect(model).not_to receive(:execute).with('SET statement_timeout TO 0')
- expect(model).not_to receive(:execute).with('RESET statement_timeout')
-
- expect { |block| model.disable_statement_timeout(&block) }.to yield_control
- end
- end
- end
-
describe '#true_value' do
it 'returns the appropriate value' do
expect(model.true_value).to eq("'t'")
@@ -2006,8 +2019,116 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ let(:same_queue_different_worker) do
+ Class.new do
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'test'
+
+ def self.name
+ 'SameQueueDifferentWorkerClass'
+ end
+ end
+ end
+
+ let(:unrelated_worker) do
+ Class.new do
+ include Sidekiq::Worker
+
+ sidekiq_options queue: 'unrelated'
+
+ def self.name
+ 'UnrelatedWorkerClass'
+ end
+ end
+ end
+
before do
stub_const(worker.name, worker)
+ stub_const(unrelated_worker.name, unrelated_worker)
+ stub_const(same_queue_different_worker.name, same_queue_different_worker)
+ end
+
+ describe '#sidekiq_remove_jobs', :clean_gitlab_redis_queues do
+ def clear_queues
+ Sidekiq::Queue.new('test').clear
+ Sidekiq::Queue.new('unrelated').clear
+ Sidekiq::RetrySet.new.clear
+ Sidekiq::ScheduledSet.new.clear
+ end
+
+ around do |example|
+ clear_queues
+ Sidekiq::Testing.disable!(&example)
+ clear_queues
+ end
+
+ it "removes all related job instances from the job class's queue" do
+ worker.perform_async
+ same_queue_different_worker.perform_async
+ unrelated_worker.perform_async
+
+ queue_we_care_about = Sidekiq::Queue.new(worker.queue)
+ unrelated_queue = Sidekiq::Queue.new(unrelated_worker.queue)
+
+ expect(queue_we_care_about.size).to eq(2)
+ expect(unrelated_queue.size).to eq(1)
+
+ model.sidekiq_remove_jobs(job_klass: worker)
+
+ expect(queue_we_care_about.size).to eq(1)
+ expect(queue_we_care_about.map(&:klass)).not_to include(worker.name)
+ expect(queue_we_care_about.map(&:klass)).to include(
+ same_queue_different_worker.name
+ )
+ expect(unrelated_queue.size).to eq(1)
+ end
+
+ context 'when job instances are in the scheduled set' do
+ it 'removes all related job instances from the scheduled set' do
+ worker.perform_in(1.hour)
+ unrelated_worker.perform_in(1.hour)
+
+ scheduled = Sidekiq::ScheduledSet.new
+
+ expect(scheduled.size).to eq(2)
+ expect(scheduled.map(&:klass)).to include(
+ worker.name,
+ unrelated_worker.name
+ )
+
+ model.sidekiq_remove_jobs(job_klass: worker)
+
+ expect(scheduled.size).to eq(1)
+ expect(scheduled.map(&:klass)).not_to include(worker.name)
+ expect(scheduled.map(&:klass)).to include(unrelated_worker.name)
+ end
+ end
+
+ context 'when job instances are in the retry set' do
+ include_context 'when handling retried jobs'
+
+ it 'removes all related job instances from the retry set' do
+ retry_in(worker, 1.hour)
+ retry_in(worker, 2.hours)
+ retry_in(worker, 3.hours)
+ retry_in(unrelated_worker, 4.hours)
+
+ retries = Sidekiq::RetrySet.new
+
+ expect(retries.size).to eq(4)
+ expect(retries.map(&:klass)).to include(
+ worker.name,
+ unrelated_worker.name
+ )
+
+ model.sidekiq_remove_jobs(job_klass: worker)
+
+ expect(retries.size).to eq(1)
+ expect(retries.map(&:klass)).not_to include(worker.name)
+ expect(retries.map(&:klass)).to include(unrelated_worker.name)
+ end
+ end
end
describe '#sidekiq_queue_length' do
@@ -2031,7 +2152,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe '#migrate_sidekiq_queue' do
+ describe '#sidekiq_queue_migrate' do
it 'migrates jobs from one sidekiq queue to another' do
Sidekiq::Testing.disable! do
worker.perform_async('Something', [1])
@@ -2071,6 +2192,110 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#convert_to_type_column' do
+ it 'returns the name of the temporary column used to convert to bigint' do
+ expect(model.convert_to_type_column(:id, :int, :bigint)).to eq('id_convert_int_to_bigint')
+ end
+
+ it 'returns the name of the temporary column used to convert to uuid' do
+ expect(model.convert_to_type_column(:uuid, :string, :uuid)).to eq('uuid_convert_string_to_uuid')
+ end
+ end
+
+ describe '#create_temporary_columns_and_triggers' do
+ let(:table) { :test_table }
+ let(:column) { :id }
+ let(:mappings) do
+ {
+ id: {
+ from_type: :int,
+ to_type: :bigint
+ }
+ }
+ end
+
+ let(:old_bigint_column_naming) { false }
+
+ subject do
+ model.create_temporary_columns_and_triggers(
+ table,
+ mappings,
+ old_bigint_column_naming: old_bigint_column_naming
+ )
+ end
+
+ before do
+ model.create_table table, id: false do |t|
+ t.integer :id, primary_key: true
+ t.integer :non_nullable_column, null: false
+ t.integer :nullable_column
+ t.timestamps
+ end
+ end
+
+ context 'when no mappings are provided' do
+ let(:mappings) { nil }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error("No mappings for column conversion provided")
+ end
+ end
+
+ context 'when any of the mappings does not have the required keys' do
+ let(:mappings) do
+ {
+ id: {
+ from_type: :int
+ }
+ }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error("Some mappings don't have required keys provided")
+ end
+ end
+
+ context 'when the target table does not exist' do
+ it 'raises an error' do
+ expect { model.create_temporary_columns_and_triggers(:non_existent_table, mappings) }.to raise_error("Table non_existent_table does not exist")
+ end
+ end
+
+ context 'when the column to migrate does not exist' do
+ let(:missing_column) { :test }
+ let(:mappings) do
+ {
+ missing_column => {
+ from_type: :int,
+ to_type: :bigint
+ }
+ }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error("Column #{missing_column} does not exist on #{table}")
+ end
+ end
+
+ context 'when old_bigint_column_naming is true' do
+ let(:old_bigint_column_naming) { true }
+
+ it 'calls convert_to_bigint_column' do
+ expect(model).to receive(:convert_to_bigint_column).with(:id).and_return("id_convert_to_bigint")
+
+ subject
+ end
+ end
+
+ context 'when old_bigint_column_naming is false' do
+ it 'calls convert_to_type_column' do
+ expect(model).to receive(:convert_to_type_column).with(:id, :int, :bigint).and_return("id_convert_to_bigint")
+
+ subject
+ end
+ end
+ end
+
describe '#initialize_conversion_of_integer_to_bigint' do
let(:table) { :test_table }
let(:column) { :id }
@@ -2227,7 +2452,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
let(:columns) { :id }
it 'removes column, trigger, and function' do
- temporary_column = model.convert_to_bigint_column(:id)
+ temporary_column = model.convert_to_bigint_column(columns)
trigger_name = model.rename_trigger_name(table, :id, temporary_column)
model.revert_initialize_conversion_of_integer_to_bigint(table, columns)
@@ -2420,101 +2645,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe '#ensure_batched_background_migration_is_finished' do
- let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' }
- let(:table) { :events }
- let(:column_name) { :id }
- let(:job_arguments) { [["id"], ["id_convert_to_bigint"], nil] }
-
- let(:configuration) do
- {
- job_class_name: job_class_name,
- table_name: table,
- column_name: column_name,
- job_arguments: job_arguments
- }
- end
-
- let(:migration_attributes) do
- configuration.merge(gitlab_schema: Gitlab::Database.gitlab_schemas_for_connection(model.connection).first)
- end
-
- before do
- allow(model).to receive(:transaction_open?).and_return(false)
- 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
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!).twice
-
- create(:batched_background_migration, :active, migration_attributes)
-
- allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
- allow(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(false)
- end
-
- 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':" \
- "\t#{configuration}" \
- "\n\n" \
- "Finalize it manually by running the following command in a `bash` or `sh` shell:" \
- "\n\n" \
- "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"id\"]\\,[\"id_convert_to_bigint\"]\\,null]']" \
- "\n\n" \
- "For more information, check the documentation" \
- "\n\n" \
- "\thttps://docs.gitlab.com/ee/user/admin_area/monitoring/background_migrations.html#database-migrations-failing-because-of-batched-background-migration-not-finished"
- end
-
- it 'does not raise error when migration exists and is marked as finished' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
-
- create(:batched_background_migration, :finished, migration_attributes)
-
- expect { ensure_batched_background_migration_is_finished }
- .not_to raise_error
- end
-
- it 'logs a warning when migration does not exist' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
-
- create(:batched_background_migration, :active, migration_attributes.merge(gitlab_schema: :gitlab_something_else))
-
- 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
-
- it 'finalizes the migration' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!).twice
-
- migration = create(:batched_background_migration, :active, configuration)
-
- allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
- expect(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(migration.finish!)
- end
-
- ensure_batched_background_migration_is_finished
- end
-
- context 'when the flag finalize is false' do
- it 'does not finalize the migration' do
- expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
-
- create(:batched_background_migration, :active, configuration)
-
- allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
- expect(runner).not_to receive(:finalize).with(job_class_name, table, column_name, job_arguments)
- end
-
- expect { model.ensure_batched_background_migration_is_finished(**configuration.merge(finalize: false)) }.to raise_error(RuntimeError)
- end
- end
- end
-
describe '#index_exists_by_name?' do
it 'returns true if an index exists' do
ActiveRecord::Migration.connection.execute(
@@ -2621,48 +2751,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe '#with_lock_retries' do
- let(:buffer) { StringIO.new }
- let(:in_memory_logger) { Gitlab::JsonLogger.new(buffer) }
- let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } }
-
- it 'sets the migration class name in the logs' do
- model.with_lock_retries(env: env, logger: in_memory_logger) {}
-
- buffer.rewind
- expect(buffer.read).to include("\"class\":\"#{model.class}\"")
- end
-
- where(raise_on_exhaustion: [true, false])
-
- with_them do
- it 'sets raise_on_exhaustion as requested' do
- with_lock_retries = double
- expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries)
- expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: raise_on_exhaustion)
-
- model.with_lock_retries(env: env, logger: in_memory_logger, raise_on_exhaustion: raise_on_exhaustion) {}
- end
- end
-
- it 'does not raise on exhaustion by default' do
- with_lock_retries = double
- expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries)
- expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false)
-
- model.with_lock_retries(env: env, logger: in_memory_logger) {}
- end
-
- it 'defaults to allowing subtransactions' do
- with_lock_retries = double
-
- expect(Gitlab::Database::WithLockRetries).to receive(:new).with(hash_including(allow_savepoints: true)).and_return(with_lock_retries)
- expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false)
-
- model.with_lock_retries(env: env, logger: in_memory_logger) {}
- end
- end
-
describe '#backfill_iids' do
include MigrationsHelpers
@@ -2778,720 +2866,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
- describe '#check_constraint_name' do
- it 'returns a valid constraint name' do
- name = model.check_constraint_name(:this_is_a_very_long_table_name,
- :with_a_very_long_column_name,
- :with_a_very_long_type)
-
- expect(name).to be_an_instance_of(String)
- expect(name).to start_with('check_')
- expect(name.length).to eq(16)
- end
- end
-
- describe '#check_constraint_exists?' do
- before do
- ActiveRecord::Migration.connection.execute(
- 'ALTER TABLE projects ADD CONSTRAINT check_1 CHECK (char_length(path) <= 5) NOT VALID'
- )
-
- ActiveRecord::Migration.connection.execute(
- 'CREATE SCHEMA new_test_schema'
- )
-
- ActiveRecord::Migration.connection.execute(
- 'CREATE TABLE new_test_schema.projects (id integer, name character varying)'
- )
-
- ActiveRecord::Migration.connection.execute(
- 'ALTER TABLE new_test_schema.projects ADD CONSTRAINT check_2 CHECK (char_length(name) <= 5)'
- )
- end
-
- it 'returns true if a constraint exists' do
- expect(model.check_constraint_exists?(:projects, 'check_1'))
- .to be_truthy
- end
-
- it 'returns false if a constraint does not exist' do
- expect(model.check_constraint_exists?(:projects, 'this_does_not_exist'))
- .to be_falsy
- end
-
- it 'returns false if a constraint with the same name exists in another table' do
- expect(model.check_constraint_exists?(:users, 'check_1'))
- .to be_falsy
- end
-
- it 'returns false if a constraint with the same name exists for the same table in another schema' do
- expect(model.check_constraint_exists?(:projects, 'check_2'))
- .to be_falsy
- end
- end
-
- describe '#add_check_constraint' do
- before do
- allow(model).to receive(:check_constraint_exists?).and_return(false)
- end
-
- context 'constraint name validation' do
- it 'raises an error when too long' do
- expect do
- model.add_check_constraint(
- :test_table,
- 'name IS NOT NULL',
- 'a' * (Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH + 1)
- )
- end.to raise_error(RuntimeError)
- end
-
- it 'does not raise error when the length is acceptable' do
- constraint_name = 'a' * Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH
-
- expect(model).to receive(:transaction_open?).and_return(false)
- expect(model).to receive(:check_constraint_exists?).and_return(false)
- expect(model).to receive(:with_lock_retries).and_call_original
- expect(model).to receive(:execute).with(/ADD CONSTRAINT/)
-
- model.add_check_constraint(
- :test_table,
- 'name IS NOT NULL',
- constraint_name,
- validate: false
- )
- end
- end
-
- context 'inside a transaction' do
- it 'raises an error' do
- expect(model).to receive(:transaction_open?).and_return(true)
-
- expect do
- model.add_check_constraint(
- :test_table,
- 'name IS NOT NULL',
- 'check_name_not_null'
- )
- end.to raise_error(RuntimeError)
- end
- end
-
- context 'outside a transaction' do
- before do
- allow(model).to receive(:transaction_open?).and_return(false)
- end
-
- context 'when the constraint is already defined in the database' do
- it 'does not create a constraint' do
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, 'check_name_not_null')
- .and_return(true)
-
- expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
-
- # setting validate: false to only focus on the ADD CONSTRAINT command
- model.add_check_constraint(
- :test_table,
- 'name IS NOT NULL',
- 'check_name_not_null',
- validate: false
- )
- end
- end
-
- context 'when the constraint is not defined in the database' do
- it 'creates the constraint' do
- expect(model).to receive(:with_lock_retries).and_call_original
- expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
-
- # setting validate: false to only focus on the ADD CONSTRAINT command
- model.add_check_constraint(
- :test_table,
- 'char_length(name) <= 255',
- 'check_name_not_null',
- validate: false
- )
- end
- end
-
- context 'when validate is not provided' do
- it 'performs validation' do
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, 'check_name_not_null')
- .and_return(false).exactly(1)
-
- expect(model).to receive(:disable_statement_timeout).and_call_original
- expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/SET statement_timeout TO/)
- expect(model).to receive(:with_lock_retries).and_call_original
- expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
-
- # we need the check constraint to exist so that the validation proceeds
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, 'check_name_not_null')
- .and_return(true).exactly(1)
-
- expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
-
- model.add_check_constraint(
- :test_table,
- 'char_length(name) <= 255',
- 'check_name_not_null'
- )
- end
- end
-
- context 'when validate is provided with a falsey value' do
- it 'skips validation' do
- expect(model).not_to receive(:disable_statement_timeout)
- expect(model).to receive(:with_lock_retries).and_call_original
- expect(model).to receive(:execute).with(/ADD CONSTRAINT/)
- expect(model).not_to receive(:execute).with(/VALIDATE CONSTRAINT/)
-
- model.add_check_constraint(
- :test_table,
- 'char_length(name) <= 255',
- 'check_name_not_null',
- validate: false
- )
- end
- end
-
- context 'when validate is provided with a truthy value' do
- it 'performs validation' do
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, 'check_name_not_null')
- .and_return(false).exactly(1)
-
- expect(model).to receive(:disable_statement_timeout).and_call_original
- expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/SET statement_timeout TO/)
- expect(model).to receive(:with_lock_retries).and_call_original
- expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
-
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, 'check_name_not_null')
- .and_return(true).exactly(1)
-
- expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
-
- model.add_check_constraint(
- :test_table,
- 'char_length(name) <= 255',
- 'check_name_not_null',
- validate: true
- )
- end
- end
- end
- end
-
- describe '#validate_check_constraint' do
- context 'when the constraint does not exist' do
- it 'raises an error' do
- error_message = /Could not find check constraint "check_1" on table "test_table"/
-
- expect(model).to receive(:check_constraint_exists?).and_return(false)
-
- expect do
- model.validate_check_constraint(:test_table, 'check_1')
- end.to raise_error(RuntimeError, error_message)
- end
- end
-
- context 'when the constraint exists' do
- it 'performs validation' do
- validate_sql = /ALTER TABLE test_table VALIDATE CONSTRAINT check_name/
-
- expect(model).to receive(:check_constraint_exists?).and_return(true)
- expect(model).to receive(:disable_statement_timeout).and_call_original
- expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/SET statement_timeout TO/)
- expect(model).to receive(:execute).ordered.with(validate_sql)
- expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
-
- model.validate_check_constraint(:test_table, 'check_name')
- end
- end
- end
-
- describe '#remove_check_constraint' do
- before do
- allow(model).to receive(:transaction_open?).and_return(false)
- end
-
- it 'removes the constraint' do
- drop_sql = /ALTER TABLE test_table\s+DROP CONSTRAINT IF EXISTS check_name/
-
- expect(model).to receive(:with_lock_retries).and_call_original
- expect(model).to receive(:execute).with(drop_sql)
-
- model.remove_check_constraint(:test_table, 'check_name')
- end
- end
-
- describe '#copy_check_constraints' do
- context 'inside a transaction' do
- it 'raises an error' do
- expect(model).to receive(:transaction_open?).and_return(true)
-
- expect do
- model.copy_check_constraints(:test_table, :old_column, :new_column)
- end.to raise_error(RuntimeError)
- end
- end
-
- context 'outside a transaction' do
- before do
- allow(model).to receive(:transaction_open?).and_return(false)
- allow(model).to receive(:column_exists?).and_return(true)
- end
-
- let(:old_column_constraints) do
- [
- {
- 'schema_name' => 'public',
- 'table_name' => 'test_table',
- 'column_name' => 'old_column',
- 'constraint_name' => 'check_d7d49d475d',
- 'constraint_def' => 'CHECK ((old_column IS NOT NULL))'
- },
- {
- 'schema_name' => 'public',
- 'table_name' => 'test_table',
- 'column_name' => 'old_column',
- 'constraint_name' => 'check_48560e521e',
- 'constraint_def' => 'CHECK ((char_length(old_column) <= 255))'
- },
- {
- 'schema_name' => 'public',
- 'table_name' => 'test_table',
- 'column_name' => 'old_column',
- 'constraint_name' => 'custom_check_constraint',
- 'constraint_def' => 'CHECK (((old_column IS NOT NULL) AND (another_column IS NULL)))'
- },
- {
- 'schema_name' => 'public',
- 'table_name' => 'test_table',
- 'column_name' => 'old_column',
- 'constraint_name' => 'not_valid_check_constraint',
- 'constraint_def' => 'CHECK ((old_column IS NOT NULL)) NOT VALID'
- }
- ]
- end
-
- it 'copies check constraints from one column to another' do
- allow(model).to receive(:check_constraints_for)
- .with(:test_table, :old_column, schema: nil)
- .and_return(old_column_constraints)
-
- allow(model).to receive(:not_null_constraint_name).with(:test_table, :new_column)
- .and_return('check_1')
-
- allow(model).to receive(:text_limit_name).with(:test_table, :new_column)
- .and_return('check_2')
-
- allow(model).to receive(:check_constraint_name)
- .with(:test_table, :new_column, 'copy_check_constraint')
- .and_return('check_3')
-
- expect(model).to receive(:add_check_constraint)
- .with(
- :test_table,
- '(new_column IS NOT NULL)',
- 'check_1',
- validate: true
- ).once
-
- expect(model).to receive(:add_check_constraint)
- .with(
- :test_table,
- '(char_length(new_column) <= 255)',
- 'check_2',
- validate: true
- ).once
-
- expect(model).to receive(:add_check_constraint)
- .with(
- :test_table,
- '((new_column IS NOT NULL) AND (another_column IS NULL))',
- 'check_3',
- validate: true
- ).once
-
- expect(model).to receive(:add_check_constraint)
- .with(
- :test_table,
- '(new_column IS NOT NULL)',
- 'check_1',
- validate: false
- ).once
-
- model.copy_check_constraints(:test_table, :old_column, :new_column)
- end
-
- it 'does nothing if there are no constraints defined for the old column' do
- allow(model).to receive(:check_constraints_for)
- .with(:test_table, :old_column, schema: nil)
- .and_return([])
-
- expect(model).not_to receive(:add_check_constraint)
-
- model.copy_check_constraints(:test_table, :old_column, :new_column)
- end
-
- it 'raises an error when the orginating column does not exist' do
- allow(model).to receive(:column_exists?).with(:test_table, :old_column).and_return(false)
-
- error_message = /Column old_column does not exist on test_table/
-
- expect do
- model.copy_check_constraints(:test_table, :old_column, :new_column)
- end.to raise_error(RuntimeError, error_message)
- end
-
- it 'raises an error when the target column does not exist' do
- allow(model).to receive(:column_exists?).with(:test_table, :new_column).and_return(false)
-
- error_message = /Column new_column does not exist on test_table/
-
- expect do
- model.copy_check_constraints(:test_table, :old_column, :new_column)
- end.to raise_error(RuntimeError, error_message)
- end
- end
- end
-
- describe '#add_text_limit' do
- context 'when it is called with the default options' do
- it 'calls add_check_constraint with an infered constraint name and validate: true' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
- check = "char_length(name) <= 255"
-
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:add_check_constraint)
- .with(:test_table, check, constraint_name, validate: true)
-
- model.add_text_limit(:test_table, :name, 255)
- end
- end
-
- context 'when all parameters are provided' do
- it 'calls add_check_constraint with the correct parameters' do
- constraint_name = 'check_name_limit'
- check = "char_length(name) <= 255"
-
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:add_check_constraint)
- .with(:test_table, check, constraint_name, validate: false)
-
- model.add_text_limit(
- :test_table,
- :name,
- 255,
- constraint_name: constraint_name,
- validate: false
- )
- end
- end
- end
-
- describe '#validate_text_limit' do
- context 'when constraint_name is not provided' do
- it 'calls validate_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
-
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:validate_check_constraint)
- .with(:test_table, constraint_name)
-
- model.validate_text_limit(:test_table, :name)
- end
- end
-
- context 'when constraint_name is provided' do
- it 'calls validate_check_constraint with the correct parameters' do
- constraint_name = 'check_name_limit'
-
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:validate_check_constraint)
- .with(:test_table, constraint_name)
-
- model.validate_text_limit(:test_table, :name, constraint_name: constraint_name)
- end
- end
- end
-
- describe '#remove_text_limit' do
- context 'when constraint_name is not provided' do
- it 'calls remove_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
-
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:remove_check_constraint)
- .with(:test_table, constraint_name)
-
- model.remove_text_limit(:test_table, :name)
- end
- end
-
- context 'when constraint_name is provided' do
- it 'calls remove_check_constraint with the correct parameters' do
- constraint_name = 'check_name_limit'
-
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:remove_check_constraint)
- .with(:test_table, constraint_name)
-
- model.remove_text_limit(:test_table, :name, constraint_name: constraint_name)
- end
- end
- end
-
- describe '#check_text_limit_exists?' do
- context 'when constraint_name is not provided' do
- it 'calls check_constraint_exists? with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'max_length')
-
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, constraint_name)
-
- model.check_text_limit_exists?(:test_table, :name)
- end
- end
-
- context 'when constraint_name is provided' do
- it 'calls check_constraint_exists? with the correct parameters' do
- constraint_name = 'check_name_limit'
-
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, constraint_name)
-
- model.check_text_limit_exists?(:test_table, :name, constraint_name: constraint_name)
- end
- end
- end
-
- describe '#add_not_null_constraint' do
- context 'when it is called with the default options' do
- it 'calls add_check_constraint with an infered constraint name and validate: true' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
- check = "name IS NOT NULL"
-
- expect(model).to receive(:column_is_nullable?).and_return(true)
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:add_check_constraint)
- .with(:test_table, check, constraint_name, validate: true)
-
- model.add_not_null_constraint(:test_table, :name)
- end
- end
-
- context 'when all parameters are provided' do
- it 'calls add_check_constraint with the correct parameters' do
- constraint_name = 'check_name_not_null'
- check = "name IS NOT NULL"
-
- expect(model).to receive(:column_is_nullable?).and_return(true)
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:add_check_constraint)
- .with(:test_table, check, constraint_name, validate: false)
-
- model.add_not_null_constraint(
- :test_table,
- :name,
- constraint_name: constraint_name,
- validate: false
- )
- end
- end
-
- context 'when the column is defined as NOT NULL' do
- it 'does not add a check constraint' do
- expect(model).to receive(:column_is_nullable?).and_return(false)
- expect(model).not_to receive(:check_constraint_name)
- expect(model).not_to receive(:add_check_constraint)
-
- model.add_not_null_constraint(:test_table, :name)
- end
- end
- end
-
- describe '#validate_not_null_constraint' do
- context 'when constraint_name is not provided' do
- it 'calls validate_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
-
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:validate_check_constraint)
- .with(:test_table, constraint_name)
-
- model.validate_not_null_constraint(:test_table, :name)
- end
- end
-
- context 'when constraint_name is provided' do
- it 'calls validate_check_constraint with the correct parameters' do
- constraint_name = 'check_name_not_null'
-
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:validate_check_constraint)
- .with(:test_table, constraint_name)
-
- model.validate_not_null_constraint(:test_table, :name, constraint_name: constraint_name)
- end
- end
- end
-
- describe '#remove_not_null_constraint' do
- context 'when constraint_name is not provided' do
- it 'calls remove_check_constraint with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
-
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:remove_check_constraint)
- .with(:test_table, constraint_name)
-
- model.remove_not_null_constraint(:test_table, :name)
- end
- end
-
- context 'when constraint_name is provided' do
- it 'calls remove_check_constraint with the correct parameters' do
- constraint_name = 'check_name_not_null'
-
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:remove_check_constraint)
- .with(:test_table, constraint_name)
-
- model.remove_not_null_constraint(:test_table, :name, constraint_name: constraint_name)
- end
- end
- end
-
- describe '#check_not_null_constraint_exists?' do
- context 'when constraint_name is not provided' do
- it 'calls check_constraint_exists? with an infered constraint name' do
- constraint_name = model.check_constraint_name(:test_table,
- :name,
- 'not_null')
-
- expect(model).to receive(:check_constraint_name).and_call_original
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, constraint_name)
-
- model.check_not_null_constraint_exists?(:test_table, :name)
- end
- end
-
- context 'when constraint_name is provided' do
- it 'calls check_constraint_exists? with the correct parameters' do
- constraint_name = 'check_name_not_null'
-
- expect(model).not_to receive(:check_constraint_name)
- expect(model).to receive(:check_constraint_exists?)
- .with(:test_table, constraint_name)
-
- model.check_not_null_constraint_exists?(:test_table, :name, constraint_name: constraint_name)
- end
- end
- end
-
- describe '#create_extension' do
- subject { model.create_extension(extension) }
-
- let(:extension) { :btree_gist }
-
- it 'executes CREATE EXTENSION statement' do
- expect(model).to receive(:execute).with(/CREATE EXTENSION IF NOT EXISTS #{extension}/)
-
- subject
- end
-
- context 'without proper permissions' do
- before do
- allow(model).to receive(:execute)
- .with(/CREATE EXTENSION IF NOT EXISTS #{extension}/)
- .and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied')
- end
-
- it 'raises an exception and prints an error message' do
- expect { subject }
- .to output(/user is not allowed/).to_stderr
- .and raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/)
- end
- end
- end
-
- describe '#drop_extension' do
- subject { model.drop_extension(extension) }
-
- let(:extension) { 'btree_gist' }
-
- it 'executes CREATE EXTENSION statement' do
- expect(model).to receive(:execute).with(/DROP EXTENSION IF EXISTS #{extension}/)
-
- subject
- end
-
- context 'without proper permissions' do
- before do
- allow(model).to receive(:execute)
- .with(/DROP EXTENSION IF EXISTS #{extension}/)
- .and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied')
- end
-
- it 'raises an exception and prints an error message' do
- expect { subject }
- .to output(/user is not allowed/).to_stderr
- .and raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/)
- end
- end
- end
-
- describe '#rename_constraint' do
- it "executes the statement to rename constraint" do
- expect(model).to receive(:execute).with /ALTER TABLE "test_table"\nRENAME CONSTRAINT "fk_old_name" TO "fk_new_name"/
-
- model.rename_constraint(:test_table, :fk_old_name, :fk_new_name)
- end
- end
-
- describe '#drop_constraint' do
- it "executes the statement to drop the constraint" do
- expect(model).to receive(:execute).with("ALTER TABLE \"test_table\" DROP CONSTRAINT \"constraint_name\" CASCADE\n")
-
- model.drop_constraint(:test_table, :constraint_name, cascade: true)
- end
-
- context 'when cascade option is false' do
- it "executes the statement to drop the constraint without cascade" do
- expect(model).to receive(:execute).with("ALTER TABLE \"test_table\" DROP CONSTRAINT \"constraint_name\" \n")
-
- model.drop_constraint(:test_table, :constraint_name, cascade: false)
- end
- end
- end
-
describe '#add_primary_key_using_index' do
it "executes the statement to add the primary key" do
expect(model).to receive(:execute).with /ALTER TABLE "test_table" ADD CONSTRAINT "old_name" PRIMARY KEY USING INDEX "new_name"/
@@ -3558,4 +2932,36 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.add_sequence(:test_table, :test_column, :test_table_id_seq, 1)
end
end
+
+ describe "#partition?" do
+ subject { model.partition?(table_name) }
+
+ let(:table_name) { 'ci_builds_metadata' }
+
+ context "when a partition table exist" do
+ context 'when the view postgres_partitions exists' do
+ it 'calls the view', :aggregate_failures do
+ expect(Gitlab::Database::PostgresPartition).to receive(:partition_exists?).with(table_name).and_call_original
+ expect(subject).to be_truthy
+ end
+ end
+
+ context 'when the view postgres_partitions does not exist' do
+ before do
+ allow(model).to receive(:view_exists?).and_return(false)
+ end
+
+ it 'does not call the view', :aggregate_failures do
+ expect(Gitlab::Database::PostgresPartition).to receive(:legacy_partition_exists?).with(table_name).and_call_original
+ expect(subject).to be_truthy
+ end
+ end
+ end
+
+ context "when a partition table does not exist" do
+ let(:table_name) { 'partition_does_not_exist' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
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 f21f1ac5e52..d4fff947c29 100644
--- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb
@@ -14,9 +14,6 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do
shared_examples_for 'helpers that enqueue background migrations' do |worker_class, connection_class, tracking_database|
before do
allow(model).to receive(:tracking_database).and_return(tracking_database)
-
- # Due to lib/gitlab/database/load_balancing/configuration.rb:92 requiring RequestStore
- # we cannot use stub_feature_flags(force_no_sharing_primary_model: true)
allow(connection_class.connection.load_balancer.configuration)
.to receive(:use_dedicated_connection?).and_return(true)
diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
index a2f6e6b43ed..3e249b14f2e 100644
--- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb
@@ -425,4 +425,99 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d
end
end
end
+
+ describe '#ensure_batched_background_migration_is_finished' do
+ let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' }
+ let(:table) { :events }
+ let(:column_name) { :id }
+ let(:job_arguments) { [["id"], ["id_convert_to_bigint"], nil] }
+
+ let(:configuration) do
+ {
+ job_class_name: job_class_name,
+ table_name: table,
+ column_name: column_name,
+ job_arguments: job_arguments
+ }
+ end
+
+ let(:migration_attributes) do
+ configuration.merge(gitlab_schema: Gitlab::Database.gitlab_schemas_for_connection(migration.connection).first)
+ end
+
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+ end
+
+ subject(:ensure_batched_background_migration_is_finished) { migration.ensure_batched_background_migration_is_finished(**configuration) }
+
+ it 'raises an error when migration exists and is not marked as finished' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!).twice
+
+ create(:batched_background_migration, :active, migration_attributes)
+
+ allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
+ allow(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(false)
+ end
+
+ 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':" \
+ "\t#{configuration}" \
+ "\n\n" \
+ "Finalize it manually by running the following command in a `bash` or `sh` shell:" \
+ "\n\n" \
+ "\tsudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"id\"]\\,[\"id_convert_to_bigint\"]\\,null]']" \
+ "\n\n" \
+ "For more information, check the documentation" \
+ "\n\n" \
+ "\thttps://docs.gitlab.com/ee/user/admin_area/monitoring/background_migrations.html#database-migrations-failing-because-of-batched-background-migration-not-finished"
+ end
+
+ it 'does not raise error when migration exists and is marked as finished' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
+
+ create(:batched_background_migration, :finished, migration_attributes)
+
+ expect { ensure_batched_background_migration_is_finished }
+ .not_to raise_error
+ end
+
+ it 'logs a warning when migration does not exist' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
+
+ create(:batched_background_migration, :active, migration_attributes.merge(gitlab_schema: :gitlab_something_else))
+
+ 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
+
+ it 'finalizes the migration' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!).twice
+
+ migration = create(:batched_background_migration, :active, configuration)
+
+ allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
+ expect(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(migration.finish!)
+ end
+
+ ensure_batched_background_migration_is_finished
+ end
+
+ context 'when the flag finalize is false' do
+ it 'does not finalize the migration' do
+ expect(Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas).to receive(:require_dml_mode!)
+
+ create(:batched_background_migration, :active, configuration)
+
+ allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
+ expect(runner).not_to receive(:finalize).with(job_class_name, table, column_name, job_arguments)
+ end
+
+ expect { migration.ensure_batched_background_migration_is_finished(**configuration.merge(finalize: false)) }.to raise_error(RuntimeError)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
new file mode 100644
index 00000000000..6848fc85aa1
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/constraints_helpers_spec.rb
@@ -0,0 +1,679 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::ConstraintsHelpers do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ before do
+ allow(model).to receive(:puts)
+ end
+
+ describe '#check_constraint_name' do
+ it 'returns a valid constraint name' do
+ name = model.check_constraint_name(:this_is_a_very_long_table_name,
+ :with_a_very_long_column_name,
+ :with_a_very_long_type)
+
+ expect(name).to be_an_instance_of(String)
+ expect(name).to start_with('check_')
+ expect(name.length).to eq(16)
+ end
+ end
+
+ describe '#check_constraint_exists?' do
+ before do
+ ActiveRecord::Migration.connection.execute(
+ 'ALTER TABLE projects ADD CONSTRAINT check_1 CHECK (char_length(path) <= 5) NOT VALID'
+ )
+
+ ActiveRecord::Migration.connection.execute(
+ 'CREATE SCHEMA new_test_schema'
+ )
+
+ ActiveRecord::Migration.connection.execute(
+ 'CREATE TABLE new_test_schema.projects (id integer, name character varying)'
+ )
+
+ ActiveRecord::Migration.connection.execute(
+ 'ALTER TABLE new_test_schema.projects ADD CONSTRAINT check_2 CHECK (char_length(name) <= 5)'
+ )
+ end
+
+ it 'returns true if a constraint exists' do
+ expect(model)
+ .to be_check_constraint_exists(:projects, 'check_1')
+ end
+
+ it 'returns false if a constraint does not exist' do
+ expect(model)
+ .not_to be_check_constraint_exists(:projects, 'this_does_not_exist')
+ end
+
+ it 'returns false if a constraint with the same name exists in another table' do
+ expect(model)
+ .not_to be_check_constraint_exists(:users, 'check_1')
+ end
+
+ it 'returns false if a constraint with the same name exists for the same table in another schema' do
+ expect(model)
+ .not_to be_check_constraint_exists(:projects, 'check_2')
+ end
+ end
+
+ describe '#add_check_constraint' do
+ before do
+ allow(model).to receive(:check_constraint_exists?).and_return(false)
+ end
+
+ context 'when constraint name validation' do
+ it 'raises an error when too long' do
+ expect do
+ model.add_check_constraint(
+ :test_table,
+ 'name IS NOT NULL',
+ 'a' * (Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH + 1)
+ )
+ end.to raise_error(RuntimeError)
+ end
+
+ it 'does not raise error when the length is acceptable' do
+ constraint_name = 'a' * Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH
+
+ expect(model).to receive(:transaction_open?).and_return(false)
+ expect(model).to receive(:check_constraint_exists?).and_return(false)
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:execute).with(/ADD CONSTRAINT/)
+
+ model.add_check_constraint(
+ :test_table,
+ 'name IS NOT NULL',
+ constraint_name,
+ validate: false
+ )
+ end
+ end
+
+ context 'when inside a transaction' do
+ it 'raises an error' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.add_check_constraint(
+ :test_table,
+ 'name IS NOT NULL',
+ 'check_name_not_null'
+ )
+ end.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'when outside a transaction' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'when the constraint is already defined in the database' do
+ it 'does not create a constraint' do
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, 'check_name_not_null')
+ .and_return(true)
+
+ expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
+
+ # setting validate: false to only focus on the ADD CONSTRAINT command
+ model.add_check_constraint(
+ :test_table,
+ 'name IS NOT NULL',
+ 'check_name_not_null',
+ validate: false
+ )
+ end
+ end
+
+ context 'when the constraint is not defined in the database' do
+ it 'creates the constraint' do
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
+
+ # setting validate: false to only focus on the ADD CONSTRAINT command
+ model.add_check_constraint(
+ :test_table,
+ 'char_length(name) <= 255',
+ 'check_name_not_null',
+ validate: false
+ )
+ end
+ end
+
+ context 'when validate is not provided' do
+ it 'performs validation' do
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, 'check_name_not_null')
+ .and_return(false).exactly(1)
+
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
+
+ # we need the check constraint to exist so that the validation proceeds
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, 'check_name_not_null')
+ .and_return(true).exactly(1)
+
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
+
+ model.add_check_constraint(
+ :test_table,
+ 'char_length(name) <= 255',
+ 'check_name_not_null'
+ )
+ end
+ end
+
+ context 'when validate is provided with a falsey value' do
+ it 'skips validation' do
+ expect(model).not_to receive(:disable_statement_timeout)
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:execute).with(/ADD CONSTRAINT/)
+ expect(model).not_to receive(:execute).with(/VALIDATE CONSTRAINT/)
+
+ model.add_check_constraint(
+ :test_table,
+ 'char_length(name) <= 255',
+ 'check_name_not_null',
+ validate: false
+ )
+ end
+ end
+
+ context 'when validate is provided with a truthy value' do
+ it 'performs validation' do
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, 'check_name_not_null')
+ .and_return(false).exactly(1)
+
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
+
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, 'check_name_not_null')
+ .and_return(true).exactly(1)
+
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
+
+ model.add_check_constraint(
+ :test_table,
+ 'char_length(name) <= 255',
+ 'check_name_not_null',
+ validate: true
+ )
+ end
+ end
+ end
+ end
+
+ describe '#validate_check_constraint' do
+ context 'when the constraint does not exist' do
+ it 'raises an error' do
+ error_message = /Could not find check constraint "check_1" on table "test_table"/
+
+ expect(model).to receive(:check_constraint_exists?).and_return(false)
+
+ expect do
+ model.validate_check_constraint(:test_table, 'check_1')
+ end.to raise_error(RuntimeError, error_message)
+ end
+ end
+
+ context 'when the constraint exists' do
+ it 'performs validation' do
+ validate_sql = /ALTER TABLE test_table VALIDATE CONSTRAINT check_name/
+
+ expect(model).to receive(:check_constraint_exists?).and_return(true)
+ expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
+ expect(model).to receive(:execute).ordered.with(validate_sql)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
+
+ model.validate_check_constraint(:test_table, 'check_name')
+ end
+ end
+ end
+
+ describe '#remove_check_constraint' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ it 'removes the constraint' do
+ drop_sql = /ALTER TABLE test_table\s+DROP CONSTRAINT IF EXISTS check_name/
+
+ expect(model).to receive(:with_lock_retries).and_call_original
+ expect(model).to receive(:execute).with(drop_sql)
+
+ model.remove_check_constraint(:test_table, 'check_name')
+ end
+ end
+
+ describe '#copy_check_constraints' do
+ context 'when inside a transaction' do
+ it 'raises an error' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'when outside a transaction' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:column_exists?).and_return(true)
+ end
+
+ let(:old_column_constraints) do
+ [
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'check_d7d49d475d',
+ 'constraint_def' => 'CHECK ((old_column IS NOT NULL))'
+ },
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'check_48560e521e',
+ 'constraint_def' => 'CHECK ((char_length(old_column) <= 255))'
+ },
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'custom_check_constraint',
+ 'constraint_def' => 'CHECK (((old_column IS NOT NULL) AND (another_column IS NULL)))'
+ },
+ {
+ 'schema_name' => 'public',
+ 'table_name' => 'test_table',
+ 'column_name' => 'old_column',
+ 'constraint_name' => 'not_valid_check_constraint',
+ 'constraint_def' => 'CHECK ((old_column IS NOT NULL)) NOT VALID'
+ }
+ ]
+ end
+
+ it 'copies check constraints from one column to another' do
+ allow(model).to receive(:check_constraints_for)
+ .with(:test_table, :old_column, schema: nil)
+ .and_return(old_column_constraints)
+
+ allow(model).to receive(:not_null_constraint_name).with(:test_table, :new_column)
+ .and_return('check_1')
+
+ allow(model).to receive(:text_limit_name).with(:test_table, :new_column)
+ .and_return('check_2')
+
+ allow(model).to receive(:check_constraint_name)
+ .with(:test_table, :new_column, 'copy_check_constraint')
+ .and_return('check_3')
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '(new_column IS NOT NULL)',
+ 'check_1',
+ validate: true
+ ).once
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '(char_length(new_column) <= 255)',
+ 'check_2',
+ validate: true
+ ).once
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '((new_column IS NOT NULL) AND (another_column IS NULL))',
+ 'check_3',
+ validate: true
+ ).once
+
+ expect(model).to receive(:add_check_constraint)
+ .with(
+ :test_table,
+ '(new_column IS NOT NULL)',
+ 'check_1',
+ validate: false
+ ).once
+
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end
+
+ it 'does nothing if there are no constraints defined for the old column' do
+ allow(model).to receive(:check_constraints_for)
+ .with(:test_table, :old_column, schema: nil)
+ .and_return([])
+
+ expect(model).not_to receive(:add_check_constraint)
+
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end
+
+ it 'raises an error when the orginating column does not exist' do
+ allow(model).to receive(:column_exists?).with(:test_table, :old_column).and_return(false)
+
+ error_message = /Column old_column does not exist on test_table/
+
+ expect do
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end.to raise_error(RuntimeError, error_message)
+ end
+
+ it 'raises an error when the target column does not exist' do
+ allow(model).to receive(:column_exists?).with(:test_table, :new_column).and_return(false)
+
+ error_message = /Column new_column does not exist on test_table/
+
+ expect do
+ model.copy_check_constraints(:test_table, :old_column, :new_column)
+ end.to raise_error(RuntimeError, error_message)
+ end
+ end
+ end
+
+ describe '#add_text_limit' do
+ context 'when it is called with the default options' do
+ it 'calls add_check_constraint with an infered constraint name and validate: true' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'max_length')
+ check = "char_length(name) <= 255"
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:add_check_constraint)
+ .with(:test_table, check, constraint_name, validate: true)
+
+ model.add_text_limit(:test_table, :name, 255)
+ end
+ end
+
+ context 'when all parameters are provided' do
+ it 'calls add_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_limit'
+ check = "char_length(name) <= 255"
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:add_check_constraint)
+ .with(:test_table, check, constraint_name, validate: false)
+
+ model.add_text_limit(
+ :test_table,
+ :name,
+ 255,
+ constraint_name: constraint_name,
+ validate: false
+ )
+ end
+ end
+ end
+
+ describe '#validate_text_limit' do
+ context 'when constraint_name is not provided' do
+ it 'calls validate_check_constraint with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'max_length')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:validate_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.validate_text_limit(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls validate_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_limit'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:validate_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.validate_text_limit(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#remove_text_limit' do
+ context 'when constraint_name is not provided' do
+ it 'calls remove_check_constraint with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'max_length')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:remove_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.remove_text_limit(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls remove_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_limit'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:remove_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.remove_text_limit(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#check_text_limit_exists?' do
+ context 'when constraint_name is not provided' do
+ it 'calls check_constraint_exists? with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'max_length')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, constraint_name)
+
+ model.check_text_limit_exists?(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls check_constraint_exists? with the correct parameters' do
+ constraint_name = 'check_name_limit'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, constraint_name)
+
+ model.check_text_limit_exists?(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#add_not_null_constraint' do
+ context 'when it is called with the default options' do
+ it 'calls add_check_constraint with an infered constraint name and validate: true' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+ check = "name IS NOT NULL"
+
+ expect(model).to receive(:column_is_nullable?).and_return(true)
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:add_check_constraint)
+ .with(:test_table, check, constraint_name, validate: true)
+
+ model.add_not_null_constraint(:test_table, :name)
+ end
+ end
+
+ context 'when all parameters are provided' do
+ it 'calls add_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+ check = "name IS NOT NULL"
+
+ expect(model).to receive(:column_is_nullable?).and_return(true)
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:add_check_constraint)
+ .with(:test_table, check, constraint_name, validate: false)
+
+ model.add_not_null_constraint(
+ :test_table,
+ :name,
+ constraint_name: constraint_name,
+ validate: false
+ )
+ end
+ end
+
+ context 'when the column is defined as NOT NULL' do
+ it 'does not add a check constraint' do
+ expect(model).to receive(:column_is_nullable?).and_return(false)
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).not_to receive(:add_check_constraint)
+
+ model.add_not_null_constraint(:test_table, :name)
+ end
+ end
+ end
+
+ describe '#validate_not_null_constraint' do
+ context 'when constraint_name is not provided' do
+ it 'calls validate_check_constraint with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:validate_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.validate_not_null_constraint(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls validate_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:validate_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.validate_not_null_constraint(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#remove_not_null_constraint' do
+ context 'when constraint_name is not provided' do
+ it 'calls remove_check_constraint with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:remove_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.remove_not_null_constraint(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls remove_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:remove_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.remove_not_null_constraint(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#check_not_null_constraint_exists?' do
+ context 'when constraint_name is not provided' do
+ it 'calls check_constraint_exists? with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, constraint_name)
+
+ model.check_not_null_constraint_exists?(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls check_constraint_exists? with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, constraint_name)
+
+ model.check_not_null_constraint_exists?(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#rename_constraint' do
+ it "executes the statement to rename constraint" do
+ expect(model).to receive(:execute).with(
+ /ALTER TABLE "test_table"\nRENAME CONSTRAINT "fk_old_name" TO "fk_new_name"/
+ )
+
+ model.rename_constraint(:test_table, :fk_old_name, :fk_new_name)
+ end
+ end
+
+ describe '#drop_constraint' do
+ it "executes the statement to drop the constraint" do
+ expect(model).to receive(:execute).with(
+ "ALTER TABLE \"test_table\" DROP CONSTRAINT \"constraint_name\" CASCADE\n"
+ )
+
+ model.drop_constraint(:test_table, :constraint_name, cascade: true)
+ end
+
+ context 'when cascade option is false' do
+ it "executes the statement to drop the constraint without cascade" do
+ expect(model).to receive(:execute).with("ALTER TABLE \"test_table\" DROP CONSTRAINT \"constraint_name\" \n")
+
+ model.drop_constraint(:test_table, :constraint_name, cascade: false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migrations/extension_helpers_spec.rb b/spec/lib/gitlab/database/migrations/extension_helpers_spec.rb
new file mode 100644
index 00000000000..fb29e06bc01
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/extension_helpers_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::ExtensionHelpers do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ before do
+ allow(model).to receive(:puts)
+ end
+
+ describe '#create_extension' do
+ subject { model.create_extension(extension) }
+
+ let(:extension) { :btree_gist }
+
+ it 'executes CREATE EXTENSION statement' do
+ expect(model).to receive(:execute).with(/CREATE EXTENSION IF NOT EXISTS #{extension}/)
+
+ subject
+ end
+
+ context 'without proper permissions' do
+ before do
+ allow(model).to receive(:execute)
+ .with(/CREATE EXTENSION IF NOT EXISTS #{extension}/)
+ .and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied')
+ end
+
+ it 'raises an exception and prints an error message' do
+ expect { subject }
+ .to output(/user is not allowed/).to_stderr
+ .and raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/)
+ end
+ end
+ end
+
+ describe '#drop_extension' do
+ subject { model.drop_extension(extension) }
+
+ let(:extension) { 'btree_gist' }
+
+ it 'executes CREATE EXTENSION statement' do
+ expect(model).to receive(:execute).with(/DROP EXTENSION IF EXISTS #{extension}/)
+
+ subject
+ end
+
+ context 'without proper permissions' do
+ before do
+ allow(model).to receive(:execute)
+ .with(/DROP EXTENSION IF EXISTS #{extension}/)
+ .and_raise(ActiveRecord::StatementInvalid, 'InsufficientPrivilege: permission denied')
+ end
+
+ it 'raises an exception and prints an error message' do
+ expect { subject }
+ .to output(/user is not allowed/).to_stderr
+ .and raise_error(ActiveRecord::StatementInvalid, /InsufficientPrivilege/)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migrations/lock_retries_helpers_spec.rb b/spec/lib/gitlab/database/migrations/lock_retries_helpers_spec.rb
new file mode 100644
index 00000000000..a8739f6758f
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/lock_retries_helpers_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::LockRetriesHelpers do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ describe '#with_lock_retries' do
+ let(:buffer) { StringIO.new }
+ let(:in_memory_logger) { Gitlab::JsonLogger.new(buffer) }
+ let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } }
+
+ it 'sets the migration class name in the logs' do
+ model.with_lock_retries(env: env, logger: in_memory_logger) {}
+
+ buffer.rewind
+ expect(buffer.read).to include("\"class\":\"#{model.class}\"")
+ end
+
+ where(raise_on_exhaustion: [true, false])
+
+ with_them do
+ it 'sets raise_on_exhaustion as requested' do
+ with_lock_retries = double
+ expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries)
+ expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: raise_on_exhaustion)
+
+ model.with_lock_retries(env: env, logger: in_memory_logger, raise_on_exhaustion: raise_on_exhaustion) {}
+ end
+ end
+
+ it 'does not raise on exhaustion by default' do
+ with_lock_retries = double
+ expect(Gitlab::Database::WithLockRetries).to receive(:new).and_return(with_lock_retries)
+ expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false)
+
+ model.with_lock_retries(env: env, logger: in_memory_logger) {}
+ end
+
+ it 'defaults to allowing subtransactions' do
+ with_lock_retries = double
+
+ expect(Gitlab::Database::WithLockRetries)
+ .to receive(:new).with(hash_including(allow_savepoints: true)).and_return(with_lock_retries)
+ expect(with_lock_retries).to receive(:run).with(raise_on_exhaustion: false)
+
+ model.with_lock_retries(env: env, logger: in_memory_logger) {}
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migrations/runner_spec.rb b/spec/lib/gitlab/database/migrations/runner_spec.rb
index f364ebfa522..bd382547689 100644
--- a/spec/lib/gitlab/database/migrations/runner_spec.rb
+++ b/spec/lib/gitlab/database/migrations/runner_spec.rb
@@ -2,26 +2,65 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_record_base do
- include Database::MultipleDatabases
-
let(:base_result_dir) { Pathname.new(Dir.mktmpdir) }
let(:migration_runs) { [] } # This list gets populated as the runner tries to run migrations
# Tests depend on all of these lists being sorted in the order migrations would be applied
- let(:applied_migrations_other_branches) { [double(ActiveRecord::Migration, version: 1, name: 'migration_complete_other_branch')] }
+ let(:applied_migrations_other_branches) do
+ [
+ double(
+ ActiveRecord::Migration,
+ version: 1,
+ name: 'migration_complete_other_branch',
+ filename: 'db/migrate/1_migration_complete_other_branch.rb'
+ )
+ ]
+ end
let(:applied_migrations_this_branch) do
[
- double(ActiveRecord::Migration, version: 2, name: 'older_migration_complete_this_branch'),
- double(ActiveRecord::Migration, version: 3, name: 'newer_migration_complete_this_branch')
+ double(
+ ActiveRecord::Migration,
+ version: 2,
+ name: 'older_migration_complete_this_branch',
+ filename: 'db/migrate/2_older_migration_complete_this_branch.rb'
+ ),
+ double(
+ ActiveRecord::Migration,
+ version: 3,
+ name: 'post_migration_complete_this_branch',
+ filename: 'db/post_migrate/3_post_migration_complete_this_branch.rb'
+ ),
+ double(
+ ActiveRecord::Migration,
+ version: 4,
+ name: 'newer_migration_complete_this_branch',
+ filename: 'db/migrate/4_newer_migration_complete_this_branch.rb'
+ )
].sort_by(&:version)
end
let(:pending_migrations) do
[
- double(ActiveRecord::Migration, version: 4, name: 'older_migration_pending'),
- double(ActiveRecord::Migration, version: 5, name: 'newer_migration_pending')
+ double(
+ ActiveRecord::Migration,
+ version: 5,
+ name: 'older_migration_pending',
+ filename: 'db/migrate/5_older_migration_pending.rb'
+ ),
+ double(
+ ActiveRecord::Migration,
+ version: 6,
+ name: 'post_migration_pending',
+ filename: 'db/post_migrate/6_post_migration_pending.rb'
+ ),
+ double(
+ ActiveRecord::Migration,
+ version: 7,
+ name: 'newer_migration_pending',
+ filename: 'db/migrate/7_newer_migration_pending.rb'
+ )
].sort_by(&:version)
end
@@ -87,11 +126,11 @@ RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_recor
context 'running migrations' do
subject(:up) { described_class.up(database: database, legacy_mode: legacy_mode) }
- it 'runs the unapplied migrations in version order', :aggregate_failures do
+ it 'runs the unapplied migrations in regular/post order, then version order', :aggregate_failures do
up.run
- expect(migration_runs.map(&:dir)).to match_array([:up, :up])
- expect(migration_runs.map(&:version_to_migrate)).to eq(pending_migrations.map(&:version))
+ expect(migration_runs.map(&:dir)).to match_array([:up, :up, :up])
+ expect(migration_runs.map(&:version_to_migrate)).to eq([5, 7, 6])
end
it 'writes a metadata file with the current schema version and database name' do
@@ -130,8 +169,8 @@ RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_recor
it 'runs the applied migrations for the current branch in reverse order', :aggregate_failures do
down.run
- expect(migration_runs.map(&:dir)).to match_array([:down, :down])
- expect(migration_runs.map(&:version_to_migrate)).to eq(applied_migrations_this_branch.reverse.map(&:version))
+ expect(migration_runs.map(&:dir)).to match_array([:down, :down, :down])
+ expect(migration_runs.map(&:version_to_migrate)).to eq([3, 4, 2])
end
end
diff --git a/spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb b/spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb
new file mode 100644
index 00000000000..d35211af680
--- /dev/null
+++ b/spec/lib/gitlab/database/migrations/timeout_helpers_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Migrations::TimeoutHelpers do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+
+ describe '#disable_statement_timeout' do
+ it 'disables statement timeouts to current transaction only' do
+ expect(model).to receive(:execute).with('SET LOCAL statement_timeout TO 0')
+
+ model.disable_statement_timeout
+ end
+
+ # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner)
+ context 'with real environment', :delete do
+ before do
+ model.execute("SET statement_timeout TO '20000'")
+ end
+
+ after do
+ model.execute('RESET statement_timeout')
+ end
+
+ it 'defines statement to 0 only for current transaction' do
+ expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s')
+
+ model.connection.transaction do
+ model.disable_statement_timeout
+ expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0')
+ end
+
+ expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s')
+ end
+
+ context 'when passing a blocks' do
+ it 'disables statement timeouts on session level and executes the block' do
+ expect(model).to receive(:execute).with('SET statement_timeout TO 0')
+ expect(model).to receive(:execute).with('RESET statement_timeout').at_least(:once)
+
+ expect { |block| model.disable_statement_timeout(&block) }.to yield_control
+ end
+
+ # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner)
+ context 'with real environment', :delete do
+ before do
+ model.execute("SET statement_timeout TO '20000'")
+ end
+
+ after do
+ model.execute('RESET statement_timeout')
+ end
+
+ it 'defines statement to 0 for any code run inside the block' do
+ expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s')
+
+ model.disable_statement_timeout do
+ model.connection.transaction do
+ expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0')
+ end
+
+ expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0')
+ end
+ end
+ end
+ end
+ end
+
+ # This spec runs without an enclosing transaction (:delete truncation method for db_cleaner)
+ context 'when the statement_timeout is already disabled', :delete do
+ before do
+ ActiveRecord::Migration.connection.execute('SET statement_timeout TO 0')
+ end
+
+ after do
+ # Use ActiveRecord::Migration.connection instead of model.execute
+ # so that this call is not counted below
+ ActiveRecord::Migration.connection.execute('RESET statement_timeout')
+ end
+
+ it 'yields control without disabling the timeout or resetting' do
+ expect(model).not_to receive(:execute).with('SET statement_timeout TO 0')
+ expect(model).not_to receive(:execute).with('RESET statement_timeout')
+
+ expect { |block| model.disable_statement_timeout(&block) }.to yield_control
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
index 0e804b4feac..cd3a94f5737 100644
--- a/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/convert_table_to_first_list_partition_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
let(:referenced_table_name) { '_test_referenced_table' }
let(:other_referenced_table_name) { '_test_other_referenced_table' }
let(:parent_table_name) { "#{table_name}_parent" }
+ let(:lock_tables) { [] }
let(:model) { define_batchable_model(table_name, connection: connection) }
@@ -27,7 +28,8 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
table_name: table_name,
partitioning_column: partitioning_column,
parent_table_name: parent_table_name,
- zero_partition_value: partitioning_default
+ zero_partition_value: partitioning_default,
+ lock_tables: lock_tables
)
end
@@ -168,6 +170,16 @@ RSpec.describe Gitlab::Database::Partitioning::ConvertTableToFirstListPartition
end
end
+ context 'with locking tables' do
+ let(:lock_tables) { [table_name] }
+
+ it 'locks the table' do
+ recorder = ActiveRecord::QueryRecorder.new { partition }
+
+ expect(recorder.log).to include(/LOCK "_test_table_to_partition" IN ACCESS EXCLUSIVE MODE/)
+ end
+ end
+
context 'when an error occurs during the conversion' do
def fail_first_time
# We can't directly use a boolean here, as we need something that will be passed by-reference to the proc
diff --git a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
index 2ef873e8adb..336dec3a8a0 100644
--- a/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
+++ b/spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb
@@ -92,11 +92,11 @@ RSpec.describe Gitlab::Database::Partitioning::DetachedPartitionDropper do
context 'removing foreign keys' do
it 'removes foreign keys from the table before dropping it' do
- expect(dropper).to receive(:drop_detached_partition).and_wrap_original do |drop_method, partition_name|
- expect(partition_name).to eq('test_partition')
- expect(foreign_key_exists_by_name(partition_name, 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_falsey
+ expect(dropper).to receive(:drop_detached_partition).and_wrap_original do |drop_method, partition|
+ expect(partition.table_name).to eq('test_partition')
+ expect(foreign_key_exists_by_name(partition.table_name, 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_falsey
- drop_method.call(partition_name)
+ drop_method.call(partition)
end
expect(foreign_key_exists_by_name('test_partition', 'fk_referenced', schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA)).to be_truthy
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
index 7465f69b87c..a81c8a5a49c 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb
@@ -65,8 +65,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
end
def expect_add_concurrent_index_and_call_original(table, column, index)
- expect(migration).to receive(:add_concurrent_index).ordered.with(table, column, { name: index })
- .and_wrap_original { |_, table, column, options| connection.add_index(table, column, **options) }
+ expect(migration).to receive(:add_concurrent_index).ordered.with(table, column, { name: index, allow_partition: true })
+ .and_wrap_original do |_, table, column, options|
+ options.delete(:allow_partition)
+ connection.add_index(table, column, **options)
+ end
end
end
@@ -91,7 +94,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
it 'forwards them to the index helper methods', :aggregate_failures do
expect(migration).to receive(:add_concurrent_index)
- .with(partition1_identifier, column_name, { name: partition1_index, where: 'x > 0', unique: true })
+ .with(partition1_identifier, column_name, { name: partition1_index, where: 'x > 0', unique: true, allow_partition: true })
expect(migration).to receive(:add_index)
.with(table_name, column_name, { name: index_name, where: 'x > 0', unique: true })
@@ -231,4 +234,165 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
end
end
end
+
+ describe '#indexes_by_definition_for_table' do
+ context 'when a partitioned table has indexes' do
+ subject do
+ migration.indexes_by_definition_for_table(table_name)
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE INDEX #{index_name} ON #{table_name} (#{column_name});
+ SQL
+ end
+
+ it 'captures partitioned index names by index definition' do
+ expect(subject).to match(a_hash_including({ "CREATE _ btree (#{column_name})" => index_name }))
+ end
+ end
+
+ context 'when a non-partitioned table has indexes' do
+ let(:regular_table_name) { '_test_regular_table' }
+ let(:regular_index_name) { '_test_regular_index_name' }
+
+ subject do
+ migration.indexes_by_definition_for_table(regular_table_name)
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{regular_table_name} (
+ #{column_name} timestamptz NOT NULL
+ );
+
+ CREATE INDEX #{regular_index_name} ON #{regular_table_name} (#{column_name});
+ SQL
+ end
+
+ it 'captures index names by index definition' do
+ expect(subject).to match(a_hash_including({ "CREATE _ btree (#{column_name})" => regular_index_name }))
+ end
+ end
+
+ context 'when a non-partitioned table has duplicate indexes' do
+ let(:regular_table_name) { '_test_regular_table' }
+ let(:regular_index_name) { '_test_regular_index_name' }
+ let(:duplicate_index_name) { '_test_duplicate_index_name' }
+
+ subject do
+ migration.indexes_by_definition_for_table(regular_table_name)
+ end
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{regular_table_name} (
+ #{column_name} timestamptz NOT NULL
+ );
+
+ CREATE INDEX #{regular_index_name} ON #{regular_table_name} (#{column_name});
+ CREATE INDEX #{duplicate_index_name} ON #{regular_table_name} (#{column_name});
+ SQL
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error { described_class::DuplicatedIndexesError }
+ end
+ end
+ end
+
+ describe '#rename_indexes_for_table' do
+ let(:original_table_name) { '_test_rename_indexes_table' }
+ let(:first_partition_name) { '_test_rename_indexes_table_1' }
+ let(:transient_table_name) { '_test_rename_indexes_table_child' }
+ let(:custom_column_name) { 'created_at' }
+ let(:generated_column_name) { 'updated_at' }
+ let(:custom_index_name) { 'index_test_rename_indexes_table_on_created_at' }
+ let(:custom_index_name_regenerated) { '_test_rename_indexes_table_created_at_idx' }
+ let(:generated_index_name) { '_test_rename_indexes_table_updated_at_idx' }
+ let(:generated_index_name_collided) { '_test_rename_indexes_table_updated_at_idx1' }
+
+ before do
+ connection.execute(<<~SQL)
+ CREATE TABLE #{original_table_name} (
+ #{custom_column_name} timestamptz NOT NULL,
+ #{generated_column_name} timestamptz NOT NULL
+ );
+
+ CREATE INDEX #{custom_index_name} ON #{original_table_name} (#{custom_column_name});
+ CREATE INDEX ON #{original_table_name} (#{generated_column_name});
+ SQL
+ end
+
+ context 'when changing a table within the current schema' do
+ let!(:identifiers) { migration.indexes_by_definition_for_table(original_table_name) }
+
+ before do
+ connection.execute(<<~SQL)
+ ALTER TABLE #{original_table_name} RENAME TO #{first_partition_name};
+ CREATE TABLE #{original_table_name} (LIKE #{first_partition_name} INCLUDING ALL);
+ DROP TABLE #{first_partition_name};
+ SQL
+ end
+
+ it 'maps index names after they are changed' do
+ migration.rename_indexes_for_table(original_table_name, identifiers)
+
+ expect_index_to_exist(custom_index_name)
+ expect_index_to_exist(generated_index_name)
+ end
+
+ it 'does not rename an index which does not exist in the to_hash' do
+ partial_identifiers = identifiers.reject { |_, name| name == custom_index_name }
+
+ migration.rename_indexes_for_table(original_table_name, partial_identifiers)
+
+ expect_index_not_to_exist(custom_index_name)
+ expect_index_to_exist(generated_index_name)
+ end
+ end
+
+ context 'when partitioning an existing table' do
+ before do
+ connection.execute(<<~SQL)
+ /* Create new parent table */
+ CREATE TABLE #{first_partition_name} (LIKE #{original_table_name} INCLUDING ALL);
+ SQL
+ end
+
+ it 'renames indexes across schemas' do
+ # Capture index names generated by postgres
+ generated_index_names = migration.indexes_by_definition_for_table(first_partition_name)
+
+ # Capture index names from original table
+ original_index_names = migration.indexes_by_definition_for_table(original_table_name)
+
+ connection.execute(<<~SQL)
+ /* Rename original table out of the way */
+ ALTER TABLE #{original_table_name} RENAME TO #{transient_table_name};
+
+ /* Rename new parent table to original name */
+ ALTER TABLE #{first_partition_name} RENAME TO #{original_table_name};
+
+ /* Move original table to gitlab_partitions_dynamic schema */
+ ALTER TABLE #{transient_table_name} SET SCHEMA #{partition_schema};
+
+ /* Rename original table to be the first partition */
+ ALTER TABLE #{partition_schema}.#{transient_table_name} RENAME TO #{first_partition_name};
+ SQL
+
+ # Apply index names generated by postgres to first partition
+ migration.rename_indexes_for_table(first_partition_name, generated_index_names, schema_name: partition_schema)
+
+ expect_index_to_exist('_test_rename_indexes_table_1_created_at_idx')
+ expect_index_to_exist('_test_rename_indexes_table_1_updated_at_idx')
+
+ # Apply index names from original table to new parent table
+ migration.rename_indexes_for_table(original_table_name, original_index_names)
+
+ expect_index_to_exist(custom_index_name)
+ expect_index_to_exist(generated_index_name)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb
index 8bb9ad2737a..e76b1da3834 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
@@ -43,6 +43,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
context 'list partitioning conversion helpers' do
shared_examples_for 'delegates to ConvertTableToFirstListPartition' do
+ let(:extra_options) { {} }
it 'throws an error if in a transaction' do
allow(migration).to receive(:transaction_open?).and_return(true)
expect { migrate }.to raise_error(/cannot be run inside a transaction/)
@@ -54,7 +55,8 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
table_name: source_table,
parent_table_name: partitioned_table,
partitioning_column: partition_column,
- zero_partition_value: min_date) do |converter|
+ zero_partition_value: min_date,
+ **extra_options) do |converter|
expect(converter).to receive(expected_method)
end
@@ -64,12 +66,15 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe
describe '#convert_table_to_first_list_partition' do
it_behaves_like 'delegates to ConvertTableToFirstListPartition' do
+ let(:lock_tables) { [source_table] }
+ let(:extra_options) { { lock_tables: lock_tables } }
let(:expected_method) { :partition }
let(:migrate) do
migration.convert_table_to_first_list_partition(table_name: source_table,
partitioning_column: partition_column,
parent_table_name: partitioned_table,
- initial_partitioning_value: min_date)
+ initial_partitioning_value: min_date,
+ lock_tables: lock_tables)
end
end
end
diff --git a/spec/lib/gitlab/database/postgres_partition_spec.rb b/spec/lib/gitlab/database/postgres_partition_spec.rb
index 5a44090d5ae..14a4d405621 100644
--- a/spec/lib/gitlab/database/postgres_partition_spec.rb
+++ b/spec/lib/gitlab/database/postgres_partition_spec.rb
@@ -72,4 +72,36 @@ RSpec.describe Gitlab::Database::PostgresPartition, type: :model do
expect(find(identifier).condition).to eq("FOR VALUES FROM ('2020-01-01 00:00:00+00') TO ('2020-02-01 00:00:00+00')")
end
end
+
+ describe '.partition_exists?' do
+ subject { described_class.partition_exists?(table_name) }
+
+ context 'when the partition exists' do
+ let(:table_name) { "ci_builds_metadata" }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when the partition does not exist' do
+ let(:table_name) { 'partition_does_not_exist' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.legacy_partition_exists?' do
+ subject { described_class.legacy_partition_exists?(table_name) }
+
+ context 'when the partition exists' do
+ let(:table_name) { "ci_builds_metadata" }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when the partition does not exist' do
+ let(:table_name) { 'partition_does_not_exist' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/query_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzer_spec.rb
index 0b849063562..6dc9ffc4aba 100644
--- a/spec/lib/gitlab/database/query_analyzer_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzer_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do
before do
allow(described_class.instance).to receive(:all_analyzers).and_return([analyzer, disabled_analyzer])
allow(analyzer).to receive(:enabled?).and_return(true)
+ allow(analyzer).to receive(:raw?).and_return(false)
allow(analyzer).to receive(:suppressed?).and_return(false)
allow(analyzer).to receive(:begin!)
allow(analyzer).to receive(:end!)
@@ -181,6 +182,13 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do
expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
end
+ it 'does call analyze with raw sql when raw? is true' do
+ expect(analyzer).to receive(:raw?).and_return(true)
+ expect(analyzer).to receive(:analyze).with('SELECT 1 FROM projects')
+
+ expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
+ end
+
def process_sql(sql)
described_class.instance.within do
ApplicationRecord.load_balancer.read_write do |connection|
diff --git a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb
new file mode 100644
index 00000000000..0fe19041b6d
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningIdAnalyzer, query_analyzers: false do
+ let(:analyzer) { described_class }
+
+ before do
+ allow(Gitlab::Database::QueryAnalyzer.instance).to receive(:all_analyzers).and_return([analyzer])
+ end
+
+ context 'when ci_partitioning_analyze_queries_partition_id_check is disabled' do
+ before do
+ stub_feature_flags(ci_partitioning_analyze_queries_partition_id_check: false)
+ end
+
+ it 'does not analyze the query' do
+ expect(analyzer).not_to receive(:analyze)
+
+ process_sql(Ci::BuildMetadata, "SELECT 1 FROM ci_builds_metadata")
+ end
+ end
+
+ context 'when ci_partitioning_analyze_queries_partition_id_check is enabled' do
+ context 'when querying a routing table' do
+ shared_examples 'a good query' do |sql|
+ it 'does not raise error' do
+ expect { process_sql(Ci::BuildMetadata, sql) }.not_to raise_error
+ end
+ end
+
+ shared_examples 'a bad query' do |sql|
+ it 'raises PartitionIdMissingError' do
+ expect { process_sql(Ci::BuildMetadata, sql) }.to raise_error(described_class::PartitionIdMissingError)
+ end
+ end
+
+ context 'when partition_id is present' do
+ context 'when selecting data' do
+ it_behaves_like 'a good query', 'SELECT * FROM p_ci_builds_metadata WHERE partition_id = 100'
+ end
+
+ context 'with a join query' do
+ sql = <<~SQL
+ SELECT ci_builds.id
+ FROM p_ci_builds
+ JOIN p_ci_builds_metadata ON p_ci_builds_metadata.build_id = ci_builds.id
+ WHERE ci_builds.type = 'Ci::Build'
+ AND ci_builds.partition_id = 100
+ AND (NOT p_ci_builds_metadata.id IN
+ (SELECT p_ci_builds_metadata.id
+ FROM p_ci_builds_metadata
+ WHERE p_ci_builds_metadata.build_id = ci_builds.id
+ AND p_ci_builds_metadata.interruptible = TRUE
+ AND p_ci_builds_metadata.partition_id = 100 ));
+ SQL
+
+ it_behaves_like 'a good query', sql
+ end
+
+ context 'when removing data' do
+ it_behaves_like 'a good query', 'DELETE FROM p_ci_builds_metadata WHERE partition_id = 100'
+ end
+
+ context 'when updating data' do
+ sql = 'UPDATE p_ci_builds_metadata SET interruptible = false WHERE partition_id = 100'
+
+ it_behaves_like 'a good query', sql
+ end
+
+ context 'when inserting a record' do
+ it_behaves_like 'a good query', 'INSERT INTO p_ci_builds_metadata (id, partition_id) VALUES(1, 1)'
+ end
+ end
+
+ context 'when partition_id is missing' do
+ context 'when inserting a record' do
+ it_behaves_like 'a bad query', 'INSERT INTO p_ci_builds_metadata (id) VALUES(1)'
+ end
+
+ context 'when selecting data' do
+ it_behaves_like 'a bad query', 'SELECT * FROM p_ci_builds_metadata WHERE id = 1'
+ end
+
+ context 'when removing data' do
+ it_behaves_like 'a bad query', 'DELETE FROM p_ci_builds_metadata WHERE id = 1'
+ end
+
+ context 'when updating data' do
+ it_behaves_like 'a bad query', 'UPDATE p_ci_builds_metadata SET interruptible = false WHERE id = 1'
+ end
+
+ context 'with a join query' do
+ sql = <<~SQL
+ SELECT ci_builds.id
+ FROM ci_builds
+ JOIN p_ci_builds_metadata ON p_ci_builds_metadata.build_id = ci_builds.id
+ WHERE ci_builds.type = 'Ci::Build'
+ AND ci_builds.partition_id = 100
+ AND (NOT p_ci_builds_metadata.id IN
+ (SELECT p_ci_builds_metadata.id
+ FROM p_ci_builds_metadata
+ WHERE p_ci_builds_metadata.build_id = ci_builds.id
+ AND p_ci_builds_metadata.interruptible = TRUE ));
+ SQL
+
+ it_behaves_like 'a bad query', sql
+ end
+ end
+ end
+ end
+
+ private
+
+ def process_sql(model, sql)
+ Gitlab::Database::QueryAnalyzer.instance.within do
+ # Skip load balancer and retrieve connection assigned to model
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb
index ef7c7965c09..1f86c2ccbb0 100644
--- a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_analyzer_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningAnalyzer, query_analyzers: false do
+RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningRoutingAnalyzer, query_analyzers: false do
let(:analyzer) { described_class }
before do
@@ -54,15 +54,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningAnalyzer, query
context 'when analyzing non targeted table' do
it 'does not raise error' do
- expect { process_sql(Ci::BuildMetadata, "SELECT 1 FROM projects") }
- .not_to raise_error
- end
- end
-
- context 'when querying a routing table' do
- it 'does not raise error' do
- expect { process_sql(Ci::BuildMetadata, "SELECT 1 FROM p_ci_builds_metadata") }
- .not_to raise_error
+ expect { process_sql(Ci::BuildMetadata, "SELECT 1 FROM projects") }.not_to raise_error
end
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb b/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb
new file mode 100644
index 00000000000..ec01ae623ae
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/query_recorder_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::QueryRecorder, query_analyzers: false do
+ # We keep only the QueryRecorder analyzer running
+ around do |example|
+ described_class.with_suppressed(false) do
+ example.run
+ end
+ end
+
+ context 'when analyzer is enabled for tests' do
+ let(:query) { 'SELECT 1 FROM projects' }
+ let(:log_path) { Rails.root.join(described_class::LOG_FILE) }
+
+ before do
+ stub_env('CI', 'true')
+
+ # This is needed to be able to stub_env the CI variable
+ ::Gitlab::Database::QueryAnalyzer.instance.begin!([described_class])
+ end
+
+ after do
+ ::Gitlab::Database::QueryAnalyzer.instance.end!([described_class])
+ end
+
+ it 'logs queries to a file' do
+ allow(FileUtils).to receive(:mkdir_p)
+ .with(File.dirname(log_path))
+ expect(File).to receive(:write)
+ .with(log_path, /^{"sql":"#{query}/, mode: 'a')
+ expect(described_class).to receive(:analyze).with(/^#{query}/).and_call_original
+
+ expect { ApplicationRecord.connection.execute(query) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/tables_truncate_spec.rb b/spec/lib/gitlab/database/tables_truncate_spec.rb
index 01af9efd782..4f68cd93a8e 100644
--- a/spec/lib/gitlab/database/tables_truncate_spec.rb
+++ b/spec/lib/gitlab/database/tables_truncate_spec.rb
@@ -233,6 +233,26 @@ RSpec.describe Gitlab::Database::TablesTruncate, :reestablished_active_record_ba
it_behaves_like 'truncating legacy tables on a database'
end
+ context 'when running with multiple shared databases' do
+ before do
+ skip_if_multiple_databases_not_setup
+ ci_db_config = Ci::ApplicationRecord.connection_db_config
+ allow(::Gitlab::Database).to receive(:db_config_share_with).with(ci_db_config).and_return('main')
+ end
+
+ it 'raises an error when truncating the main database that it is a single database setup' do
+ expect do
+ described_class.new(database_name: 'main', min_batch_size: min_batch_size).execute
+ end.to raise_error(/Cannot truncate legacy tables in single-db setup/)
+ end
+
+ it 'raises an error when truncating the ci database that it is a single database setup' do
+ expect do
+ described_class.new(database_name: 'ci', min_batch_size: min_batch_size).execute
+ end.to raise_error(/Cannot truncate legacy tables in single-db setup/)
+ end
+ end
+
context 'when running in a single database mode' do
before do
skip_if_multiple_databases_are_setup
diff --git a/spec/lib/gitlab/database/type/symbolized_jsonb_spec.rb b/spec/lib/gitlab/database/type/symbolized_jsonb_spec.rb
new file mode 100644
index 00000000000..a8401667b34
--- /dev/null
+++ b/spec/lib/gitlab/database/type/symbolized_jsonb_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::Type::SymbolizedJsonb do
+ let(:type) { described_class.new }
+
+ describe '#deserialize' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { type.deserialize(json) }
+
+ where(:json, :value) do
+ nil | nil
+ '{"key":"value"}' | { key: 'value' }
+ '{"key":[1,2,3]}' | { key: [1, 2, 3] }
+ '{"key":{"subkey":"value"}}' | { key: { subkey: 'value' } }
+ '{"key":{"a":[{"b":"c"},{"d":"e"}]}}' | { key: { a: [{ b: 'c' }, { d: 'e' }] } }
+ end
+
+ with_them do
+ it { is_expected.to match(value) }
+ end
+ end
+
+ context 'when used by a model' do
+ let(:model) do
+ Class.new(ApplicationRecord) do
+ self.table_name = :_test_symbolized_jsonb
+
+ attribute :options, :sym_jsonb
+ end
+ end
+
+ let(:record) do
+ model.create!(name: 'test', options: { key: 'value' })
+ end
+
+ before do
+ ApplicationRecord.connection.execute(<<~SQL)
+ CREATE TABLE _test_symbolized_jsonb(
+ id serial NOT NULL PRIMARY KEY,
+ name text,
+ options jsonb);
+ SQL
+
+ model.reset_column_information
+ end
+
+ it { expect(record.options).to match({ key: 'value' }) }
+
+ it 'ignores changes to other attributes' do
+ record.name = 'other test'
+
+ expect(record.changes).to match('name' => ['test', 'other test'])
+ end
+
+ it 'tracks changes to options' do
+ record.options = { key: 'other value' }
+
+ expect(record.changes).to match('options' => [{ 'key' => 'value' }, { 'key' => 'other value' }])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb
index 9d514bcc661..d67e50a50d4 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/delete_service_spec.rb
@@ -11,13 +11,13 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::DeleteService
it 'returns error' do
expect(result).to eq(
status: :error,
- message: 'Self monitoring project does not exist',
+ message: 'Self-monitoring project does not exist',
last_step: :validate_self_monitoring_project_exists
)
end
end
- context 'when self monitoring project exists' do
+ context 'when self-monitoring project exists' do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index eb42734d044..c788022bd3a 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -242,13 +242,9 @@ RSpec.describe Gitlab::Database do
pool&.disconnect!
end
- context "when there's CI connection", :request_store do
+ context "when there's CI connection" do
before do
skip_if_multiple_databases_not_setup
-
- # FF due to lib/gitlab/database/load_balancing/configuration.rb:92
- # Requires usage of `:request_store`
- stub_feature_flags(force_no_sharing_primary_model: true)
end
context 'when CI uses database_tasks: false does indicate that ci: is subset of main:' do
diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
index c24d6a44d9b..02fac96a02f 100644
--- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
@@ -40,7 +40,8 @@ RSpec.describe Gitlab::DependencyLinker::ComposerJsonLinker do
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~4.0",
"symfony/css-selector": "2.8.*|3.0.*",
- "symfony/dom-crawler": "2.8.*|3.0.*"
+ "symfony/dom-crawler": "2.8.*|3.0.*",
+ "drupal/bootstrap": "3.x-dev"
}
}
CONTENT
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index d623a390dc8..ad2524e40c5 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -55,22 +55,8 @@ RSpec.describe Gitlab::Diff::File do
let(:commit) { project.commit("532c837") }
context 'when file is ipynb' do
- let(:ipynb_semantic_diff) { false }
-
- before do
- stub_feature_flags(ipynb_semantic_diff: ipynb_semantic_diff)
- end
-
- subject { diff_file.rendered }
-
- context 'when ipynb_semantic_diff is off' do
- it { is_expected.to be_nil }
- end
-
- context 'and rendered_viewer is on' do
- let(:ipynb_semantic_diff) { true }
-
- it { is_expected.not_to be_nil }
+ it 'creates a rendered diff file' do
+ expect(diff_file.rendered).not_to be_nil
end
end
end
@@ -152,20 +138,6 @@ RSpec.describe Gitlab::Diff::File do
expect(diff_file.rendered).to be_nil
end
end
-
- context 'when semantic ipynb is off' do
- before do
- stub_feature_flags(ipynb_semantic_diff: false)
- end
-
- it 'returns nil' do
- expect(diff_file).not_to receive(:modified_file?)
- expect(diff_file).not_to receive(:ipynb?)
- expect(diff).not_to receive(:too_large?)
-
- expect(diff_file.rendered).to be_nil
- end
- end
end
end
diff --git a/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb
index c73744cd481..e267d27ed13 100644
--- a/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb
+++ b/spec/lib/gitlab/doorkeeper_secret_storing/token/pbkdf2_sha512_spec.rb
@@ -10,16 +10,6 @@ RSpec.describe Gitlab::DoorkeeperSecretStoring::Token::Pbkdf2Sha512 do
expect(described_class.transform_secret(plaintext_token))
.to eq("$pbkdf2-sha512$20000$$.c0G5XJVEew1TyeJk5TrkvB0VyOaTmDzPrsdNRED9vVeZlSyuG3G90F0ow23zUCiWKAVwmNnR/ceh.nJG3MdpQ") # rubocop:disable Layout/LineLength
end
-
- context 'when hash_oauth_tokens is disabled' do
- before do
- stub_feature_flags(hash_oauth_tokens: false)
- end
-
- it 'returns a plaintext token' do
- expect(described_class.transform_secret(plaintext_token)).to eq(plaintext_token)
- end
- end
end
describe 'STRETCHES' do
diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
index 2c1badbd113..2bc3cd81b48 100644
--- a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
stub_config_setting(host: 'localhost')
end
- let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}") }
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}") }
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:noteable) { create(:issue, project: project) }
@@ -21,19 +21,19 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
let(:mail) { Mail::Message.new(email_raw) }
it "matches the new format" do
- handler = described_class.new(mail, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}")
+ handler = described_class.new(mail, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}")
expect(handler.can_handle?).to be_truthy
end
it "matches the legacy format" do
- handler = described_class.new(mail, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY}")
+ handler = described_class.new(mail, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}")
expect(handler.can_handle?).to be_truthy
end
it "doesn't match either format" do
- handler = described_class.new(mail, "+#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}")
+ handler = described_class.new(mail, "+#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}")
expect(handler.can_handle?).to be_falsey
end
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Email::Handler::UnsubscribeHandler do
end
context 'when using old style unsubscribe link' do
- let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY}") }
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}") }
it 'unsubscribes user from notable' do
expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false)
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
index eff6fb63a5f..d38b7d9c85c 100644
--- a/spec/lib/gitlab/email/handler_spec.rb
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Gitlab::Email::Handler do
describe 'regexps are set properly' do
let(:addresses) do
- %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY}) +
+ %W(sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX} sent_notification_key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY}) +
%w(sent_notification_key path-to-project-123-user_email_token-merge-request) +
%w(path-to-project-123-user_email_token-issue path-to-project-123-user_email_token-issue-123) +
%w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 79476c63e66..9240d07fd59 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -5,11 +5,10 @@ require 'spec_helper'
RSpec.describe Gitlab::Email::Receiver do
include_context :email_shared_context
+ let_it_be(:project) { create(:project) }
let(:metric_transaction) { instance_double(Gitlab::Metrics::WebTransaction) }
shared_examples 'successful receive' do
- let_it_be(:project) { create(:project) }
-
let(:handler) { double(:handler, project: project, execute: true, metrics_event: nil, metrics_params: nil) }
let(:client_id) { 'email/jake@example.com' }
@@ -39,7 +38,7 @@ RSpec.describe Gitlab::Email::Receiver do
end
end
- shared_examples 'failed receive' do
+ shared_examples 'failed receive with event' do
it 'adds metric event' do
expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
expect(metric_transaction).to receive(:add_event).with('email_receiver_error', { error: expected_error.name })
@@ -48,6 +47,14 @@ RSpec.describe Gitlab::Email::Receiver do
end
end
+ shared_examples 'failed receive without event' do
+ it 'adds metric event' do
+ expect(::Gitlab::Metrics::BackgroundTransaction).not_to receive(:current)
+
+ expect { receiver.execute }.to raise_error(expected_error)
+ end
+ end
+
context 'when the email contains a valid email address in a header' do
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com")
@@ -100,21 +107,21 @@ RSpec.describe Gitlab::Email::Receiver do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, '!!!') }
let(:expected_error) { Gitlab::Email::UnknownIncomingEmail }
- it_behaves_like 'failed receive'
+ it_behaves_like 'failed receive with event'
end
context 'when the email is blank' do
let(:email_raw) { '' }
let(:expected_error) { Gitlab::Email::EmptyEmailError }
- it_behaves_like 'failed receive'
+ it_behaves_like 'failed receive without event'
end
context 'when the email was auto generated with Auto-Submitted header' do
let(:email_raw) { fixture_file('emails/auto_submitted.eml') }
let(:expected_error) { Gitlab::Email::AutoGeneratedEmailError }
- it_behaves_like 'failed receive'
+ it_behaves_like 'failed receive without event'
end
context "when the email's To field is blank" do
@@ -164,7 +171,48 @@ RSpec.describe Gitlab::Email::Receiver do
let(:email_raw) { fixture_file('emails/auto_reply.eml') }
let(:expected_error) { Gitlab::Email::AutoGeneratedEmailError }
- it_behaves_like 'failed receive'
+ it_behaves_like 'failed receive without event'
+ end
+
+ describe 'event raising via errors' do
+ let(:handler) { double(:handler, project: project, execute: true, metrics_event: nil, metrics_params: nil) }
+ let(:email_raw) { "arbitrary text. could be anything really. we're going to raise an error anyway." }
+
+ before do
+ allow(receiver).to receive(:handler).and_return(handler)
+ allow(handler).to receive(:execute).and_raise(expected_error)
+ end
+
+ describe 'handling errors which do not raise events' do
+ where(:expected_error) do
+ [
+ Gitlab::Email::AutoGeneratedEmailError,
+ Gitlab::Email::ProjectNotFound,
+ Gitlab::Email::EmptyEmailError,
+ Gitlab::Email::UserNotFoundError,
+ Gitlab::Email::UserBlockedError,
+ Gitlab::Email::UserNotAuthorizedError,
+ Gitlab::Email::NoteableNotFoundError,
+ Gitlab::Email::InvalidAttachment,
+ Gitlab::Email::InvalidRecordError,
+ Gitlab::Email::EmailTooLarge
+ ]
+ end
+
+ with_them do
+ it_behaves_like 'failed receive without event'
+ end
+ end
+
+ describe 'handling errors which do raise events' do
+ where(:expected_error) do
+ [Gitlab::Email::EmailUnparsableError, Gitlab::Email::UnknownIncomingEmail, ArgumentError, StandardError]
+ end
+
+ with_them do
+ it_behaves_like 'failed receive with event'
+ end
+ end
end
it 'requires all handlers to have a unique metric_event' do
diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb
index fd859ae40fb..4900547e9e9 100644
--- a/spec/lib/gitlab/error_tracking_spec.rb
+++ b/spec/lib/gitlab/error_tracking_spec.rb
@@ -369,6 +369,25 @@ RSpec.describe Gitlab::ErrorTracking do
end
end
+ context 'when exception is excluded' do
+ before do
+ stub_const('SubclassRetryError', Class.new(Gitlab::SidekiqMiddleware::RetryError))
+ end
+
+ ['Gitlab::SidekiqMiddleware::RetryError', 'SubclassRetryError'].each do |ex|
+ let(:exception) { ex.constantize.new }
+
+ it "does not report #{ex} exception to Sentry" do
+ expect(Gitlab::ErrorTracking::Logger).to receive(:error)
+
+ track_exception
+
+ expect(Raven.client.transport.events).to eq([])
+ expect(Sentry.get_current_client.transport.events).to eq([])
+ end
+ end
+ end
+
context 'when processing invalid URI exceptions' do
let(:invalid_uri) { 'http://foo:bar' }
let(:raven_exception_values) { raven_event['exception']['values'] }
diff --git a/spec/lib/gitlab/experimentation/group_types_spec.rb b/spec/lib/gitlab/experimentation/group_types_spec.rb
deleted file mode 100644
index 2b118d76fa4..00000000000
--- a/spec/lib/gitlab/experimentation/group_types_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe Gitlab::Experimentation::GroupTypes do
- it 'defines a GROUP_CONTROL constant' do
- expect(described_class.const_defined?(:GROUP_CONTROL)).to be_truthy
- end
-
- it 'defines a GROUP_EXPERIMENTAL constant' do
- expect(described_class.const_defined?(:GROUP_EXPERIMENTAL)).to be_truthy
- end
-end
diff --git a/spec/lib/gitlab/feature_categories_spec.rb b/spec/lib/gitlab/feature_categories_spec.rb
index 477da900d0a..a35166a4499 100644
--- a/spec/lib/gitlab/feature_categories_spec.rb
+++ b/spec/lib/gitlab/feature_categories_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::FeatureCategories do
let(:fake_categories) { %w(foo bar) }
- subject { described_class.new(fake_categories) }
+ subject(:feature_categories) { described_class.new(fake_categories) }
describe "#valid?" do
it "returns true if category is known", :aggregate_failures do
@@ -14,6 +14,28 @@ RSpec.describe Gitlab::FeatureCategories do
end
end
+ describe '#get!' do
+ subject { feature_categories.get!(category) }
+
+ let(:category) { 'foo' }
+
+ it { is_expected.to eq('foo') }
+
+ context 'when category does not exist' do
+ let(:category) { 'zzz' }
+
+ it { expect { subject }.to raise_error(RuntimeError) }
+
+ context 'when on production' do
+ before do
+ allow(Gitlab).to receive(:dev_or_test_env?).and_return(false)
+ end
+
+ it { is_expected.to eq('unknown') }
+ end
+ end
+ end
+
describe "#from_request" do
let(:request_env) { {} }
let(:verified) { true }
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
index 3b1eb0319f8..b158c7227d4 100644
--- a/spec/lib/gitlab/git/object_pool_spec.rb
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -78,44 +78,40 @@ RSpec.describe Gitlab::Git::ObjectPool do
end
describe '#fetch' do
- let(:commit_count) { source_repository.commit_count }
+ context 'when the object pool repository exists' do
+ let!(:pool_repository) { create(:pool_repository, :ready) }
- context "when the object's pool repository exists" do
- it 'does not raise an error' do
- expect { subject.fetch }.not_to raise_error
+ context 'without changes' do
+ it 'does not raise an error' do
+ expect { subject.fetch }.not_to raise_error
+ end
end
- end
-
- context "when the object's pool repository does not exist" do
- before do
- subject.delete
- end
-
- it "re-creates the object pool's repository" do
- subject.fetch
-
- expect(subject.repository.exists?).to be true
- end
-
- it 'does not raise an error' do
- expect { subject.fetch }.not_to raise_error
- end
-
- it 'fetches objects from the source repository' do
- new_commit_id = source_repository.create_file(
- pool_repository.source_project.owner,
- 'a.file',
- 'This is a file',
- branch_name: source_repository.root_ref,
- message: 'Add a file'
- )
-
- expect(subject.repository.exists?).to be false
-
- subject.fetch
- expect(subject.repository.commit_count('refs/remotes/origin/heads/master')).to eq(commit_count)
- expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id)
+ context 'with new commit in source repository' do
+ let(:branch_name) { Gitlab::Git::Ref.extract_branch_name(source_repository.root_ref) }
+ let(:source_ref_name) { "refs/heads/#{branch_name}" }
+ let(:pool_ref_name) { "refs/remotes/origin/heads/#{branch_name}" }
+
+ let(:new_commit_id) do
+ source_repository.create_file(
+ pool_repository.source_project.owner,
+ 'a.file',
+ 'This is a file',
+ branch_name: branch_name,
+ message: 'Add a file'
+ )
+ end
+
+ it 'fetches objects from the source repository' do
+ # Sanity-check that the commit does not yet exist in the pool repository.
+ expect(subject.repository.commit(new_commit_id)).to be_nil
+
+ subject.fetch
+
+ expect(subject.repository.commit(pool_ref_name).id).to eq(new_commit_id)
+ expect(subject.repository.commit_count(pool_ref_name))
+ .to eq(source_repository.raw_repository.commit_count(source_ref_name))
+ end
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index f3d3fd2034c..5e27979cbf3 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -461,11 +461,7 @@ RSpec.describe Gitlab::Git::Repository do
end
it 'raises an error if it failed' do
- # TODO: Once https://gitlab.com/gitlab-org/gitaly/-/merge_requests/4921
- # is merged, remove the assertion for Gitlab::Git::Repository::GitError
- expect { repository.delete_refs('refs\heads\fix') }.to raise_error do |e|
- expect(e).to be_a(Gitlab::Git::Repository::GitError).or be_a(Gitlab::Git::InvalidRefFormatError)
- end
+ expect { repository.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::InvalidRefFormatError)
end
end
@@ -940,10 +936,8 @@ RSpec.describe Gitlab::Git::Repository do
let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
def commit_files(commit)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- commit.deltas.flat_map do |delta|
- [delta.old_path, delta.new_path].uniq.compact
- end
+ commit.deltas.flat_map do |delta|
+ [delta.old_path, delta.new_path].uniq.compact
end
end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 7c84c737c00..17f802b9f66 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -239,7 +239,7 @@ RSpec.describe Gitlab::Git::Tree do
let(:pagination_params) { { limit: 5, page_token: 'aabbccdd' } }
it 'raises a command error' do
- expect { entries }.to raise_error(Gitlab::Git::CommandError, 'could not find starting OID: aabbccdd')
+ expect { entries }.to raise_error(Gitlab::Git::CommandError, /could not find starting OID: aabbccdd/)
end
end
diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index 6938ad51189..03dd4e7b89b 100644
--- a/spec/lib/gitlab/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
@@ -35,6 +35,8 @@ RSpec.describe Gitlab::GitRefValidator do
it { expect(described_class.validate('.tag')).to be false }
it { expect(described_class.validate('my branch')).to be false }
it { expect(described_class.validate("\xA0\u0000\xB0")).to be false }
+ it { expect(described_class.validate("")).to be false }
+ it { expect(described_class.validate(nil)).to be false }
end
describe '.validate_merge_request_branch' do
@@ -67,5 +69,7 @@ RSpec.describe Gitlab::GitRefValidator do
it { expect(described_class.validate_merge_request_branch('.tag')).to be false }
it { expect(described_class.validate_merge_request_branch('my branch')).to be false }
it { expect(described_class.validate_merge_request_branch("\xA0\u0000\xB0")).to be false }
+ it { expect(described_class.validate_merge_request_branch("")).to be false }
+ it { expect(described_class.validate_merge_request_branch(nil)).to be false }
end
end
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
index 9c3bc935acc..baf7076c718 100644
--- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::ObjectPoolService do
let(:pool_repository) { create(:pool_repository) }
- let(:project) { create(:project, :repository) }
+ let(:project) { pool_repository.source_project }
let(:raw_repository) { project.repository.raw }
let(:object_pool) { pool_repository.object_pool }
@@ -45,21 +45,32 @@ RSpec.describe Gitlab::GitalyClient::ObjectPoolService do
end
describe '#fetch' do
- before do
- subject.delete
+ context 'without changes' do
+ it 'fetches changes' do
+ expect(subject.fetch(project.repository)).to eq(Gitaly::FetchIntoObjectPoolResponse.new)
+ end
end
- it 'restores the pool repository objects' do
- subject.fetch(project.repository)
+ context 'with new reference in source repository' do
+ let(:branch) { 'ref-to-be-fetched' }
+ let(:source_ref) { "refs/heads/#{branch}" }
+ let(:pool_ref) { "refs/remotes/origin/heads/#{branch}" }
- expect(object_pool.repository.exists?).to be(true)
- end
+ before do
+ # Create a new reference in the source repository that we can fetch.
+ project.repository.write_ref(source_ref, 'refs/heads/master')
+ end
- context 'when called twice' do
- it "doesn't raise an error" do
- subject.delete
+ it 'fetches changes' do
+ # Sanity-check to verify that the reference only exists in the source repository now, but not in the
+ # object pool.
+ expect(project.repository.ref_exists?(source_ref)).to be(true)
+ expect(object_pool.repository.ref_exists?(pool_ref)).to be(false)
+
+ subject.fetch(project.repository)
- expect { subject.fetch(project.repository) }.not_to raise_error
+ # The fetch should've created the reference in the object pool.
+ expect(object_pool.repository.ref_exists?(pool_ref)).to be(true)
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 7e8aaa3cdf4..604feeea325 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -830,32 +830,225 @@ RSpec.describe Gitlab::GitalyClient::OperationService do
'master', repository)
end
- before do
- expect_any_instance_of(Gitaly::OperationService::Stub)
- .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
- .and_return(response)
- end
+ context 'with unstructured errors' do
+ before do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
+ .and_return(response)
+ end
- context 'when a pre_receive_error is present' do
- let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "GitLab: something failed") }
+ context 'when a pre_receive_error is present' do
+ let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "GitLab: something failed") }
- it 'raises a PreReceiveError' do
- expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed")
+ it 'raises a PreReceiveError' do
+ expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed")
+ end
end
- end
- context 'when an index_error is present' do
- let(:response) { Gitaly::UserCommitFilesResponse.new(index_error: "something failed") }
+ context 'when an index_error is present' do
+ let(:response) { Gitaly::UserCommitFilesResponse.new(index_error: "something failed") }
- it 'raises a PreReceiveError' do
- expect { subject }.to raise_error(Gitlab::Git::Index::IndexError, "something failed")
+ it 'raises an IndexError' do
+ expect { subject }.to raise_error(Gitlab::Git::Index::IndexError, "something failed")
+ end
+ end
+
+ context 'when branch_update is nil' do
+ let(:response) { Gitaly::UserCommitFilesResponse.new }
+
+ it { expect(subject).to be_nil }
end
end
- context 'when branch_update is nil' do
- let(:response) { Gitaly::UserCommitFilesResponse.new }
+ context 'with structured errors' do
+ context 'with AccessCheckError' do
+ before do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
+ .and_raise(raised_error)
+ end
- it { expect(subject).to be_nil }
+ let(:raised_error) do
+ new_detailed_error(
+ GRPC::Core::StatusCodes::PERMISSION_DENIED,
+ "error updating file",
+ Gitaly::UserCommitFilesError.new(
+ access_check: Gitaly::AccessCheckError.new(
+ error_message: "something went wrong"
+ )))
+ end
+
+ it 'raises a PreReceiveError' do
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_a(Gitlab::Git::PreReceiveError)
+ expect(error.message).to eq("something went wrong")
+ end
+ end
+ end
+
+ context 'with IndexError' do
+ let(:status_code) { nil }
+ let(:expected_error) { nil }
+
+ let(:structured_error) do
+ new_detailed_error(
+ status_code,
+ "unused error message",
+ expected_error)
+ end
+
+ shared_examples '#user_commit_files failure' do
+ it 'raises a PreReceiveError' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
+ .and_raise(structured_error)
+
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_a(Gitlab::Git::Index::IndexError)
+ expect(error.message).to eq(expected_message)
+ end
+ end
+ end
+
+ context 'with missing file' do
+ let(:status_code) { GRPC::Core::StatusCodes::NOT_FOUND }
+ let(:expected_message) { "File not found: README.md" }
+ let(:expected_error) do
+ Gitaly::UserCommitFilesError.new(
+ index_update: Gitaly::IndexError.new(
+ path: "README.md",
+ error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_FILE_NOT_FOUND
+ ))
+ end
+
+ it_behaves_like '#user_commit_files failure'
+ end
+
+ context 'with existing directory' do
+ let(:status_code) { GRPC::Core::StatusCodes::ALREADY_EXISTS }
+ let(:expected_message) { "Directory already exists: dir1" }
+ let(:expected_error) do
+ Gitaly::UserCommitFilesError.new(
+ index_update: Gitaly::IndexError.new(
+ path: "dir1",
+ error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_DIRECTORY_EXISTS
+ ))
+ end
+
+ it_behaves_like '#user_commit_files failure'
+ end
+
+ context 'with existing file' do
+ let(:status_code) { GRPC::Core::StatusCodes::ALREADY_EXISTS }
+ let(:expected_message) { "File already exists: README.md" }
+ let(:expected_error) do
+ Gitaly::UserCommitFilesError.new(
+ index_update: Gitaly::IndexError.new(
+ path: "README.md",
+ error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_FILE_EXISTS
+ ))
+ end
+
+ it_behaves_like '#user_commit_files failure'
+ end
+
+ context 'with invalid path' do
+ let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT }
+ let(:expected_message) { "Invalid path: invalid://file/name" }
+ let(:expected_error) do
+ Gitaly::UserCommitFilesError.new(
+ index_update: Gitaly::IndexError.new(
+ path: "invalid://file/name",
+ error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_INVALID_PATH
+ ))
+ end
+
+ it_behaves_like '#user_commit_files failure'
+ end
+
+ context 'with directory traversal' do
+ let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT }
+ let(:expected_message) { "Directory traversal in path escapes repository: ../../../../etc/shadow" }
+ let(:expected_error) do
+ Gitaly::UserCommitFilesError.new(
+ index_update: Gitaly::IndexError.new(
+ path: "../../../../etc/shadow",
+ error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_DIRECTORY_TRAVERSAL
+ ))
+ end
+
+ it_behaves_like '#user_commit_files failure'
+ end
+
+ context 'with empty path' do
+ let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT }
+ let(:expected_message) { "Received empty path" }
+ let(:expected_error) do
+ Gitaly::UserCommitFilesError.new(
+ index_update: Gitaly::IndexError.new(
+ path: "",
+ error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_EMPTY_PATH
+ ))
+ end
+
+ it_behaves_like '#user_commit_files failure'
+ end
+
+ context 'with unspecified error' do
+ let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT }
+ let(:expected_message) { "Unknown error performing git operation" }
+ let(:expected_error) do
+ Gitaly::UserCommitFilesError.new(
+ index_update: Gitaly::IndexError.new(
+ path: "",
+ error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_UNSPECIFIED
+ ))
+ end
+
+ it_behaves_like '#user_commit_files failure'
+ end
+
+ context 'with an exception without the detailed error' do
+ let(:permission_error) do
+ GRPC::PermissionDenied.new
+ end
+
+ it 'raises PermissionDenied' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
+ .and_raise(permission_error)
+
+ expect { subject }.to raise_error(GRPC::PermissionDenied)
+ end
+ end
+ end
+
+ context 'with CustomHookError' do
+ before do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash))
+ .and_raise(raised_error)
+ end
+
+ let(:raised_error) do
+ new_detailed_error(
+ GRPC::Core::StatusCodes::PERMISSION_DENIED,
+ "error updating file",
+ Gitaly::UserCommitFilesError.new(
+ custom_hook: Gitaly::CustomHookError.new(
+ stdout: "some stdout",
+ stderr: "GitLab: some custom hook error message",
+ hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE
+ )))
+ end
+
+ it 'raises a PreReceiveError' do
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_a(Gitlab::Git::PreReceiveError)
+ expect(error.message).to eq("some custom hook error message")
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 5ce88b06241..bd96e9baf1d 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::RefService do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository, create_tag: 'test') }
let(:storage_name) { project.repository_storage }
let(:relative_path) { project.disk_path + '.git' }
@@ -438,12 +438,28 @@ RSpec.describe Gitlab::GitalyClient::RefService do
it 'sends a find_refs_by_oid message' do
expect_any_instance_of(Gitaly::RefService::Stub)
.to receive(:find_refs_by_oid)
- .with(gitaly_request_with_params(sort_field: 'refname', oid: oid, limit: 1), kind_of(Hash))
+ .with(gitaly_request_with_params(sort_field: 'refname',
+ oid: oid,
+ limit: 1), kind_of(Hash))
.and_call_original
refs = client.find_refs_by_oid(oid: oid, limit: 1)
expect(refs.to_a).to eq([Gitlab::Git::BRANCH_REF_PREFIX + project.repository.root_ref])
end
+
+ it 'filters by ref_patterns' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:find_refs_by_oid)
+ .with(gitaly_request_with_params(sort_field: 'refname',
+ oid: oid,
+ limit: 1,
+ ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]), kind_of(Hash))
+ .and_call_original
+
+ refs = client.find_refs_by_oid(oid: oid, limit: 1, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX])
+
+ expect(refs.to_a).to eq([Gitlab::Git::TAG_REF_PREFIX + 'test'])
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb b/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb
new file mode 100644
index 00000000000..41dce5d76dd
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GitalyClient::WithFeatureFlagActors do
+ let(:user) { create(:user) }
+ let(:service) do
+ Class.new do
+ include Gitlab::GitalyClient::WithFeatureFlagActors
+ end.new
+ end
+
+ describe '#user_actor' do
+ context 'when user is not available in ApplicationContext' do
+ it 'returns nil' do
+ expect(service.user_actor).to be(nil)
+ end
+ end
+
+ context 'when user is available in ApplicationContext' do
+ around do |example|
+ ::Gitlab::ApplicationContext.with_context(user: user) { example.run }
+ end
+
+ it 'returns corresponding user record' do
+ expect(service.user_actor.flipper_id).to eql(user.flipper_id)
+ end
+ end
+
+ context 'when user does not exist' do
+ around do |example|
+ ::Gitlab::ApplicationContext.with_context(user: SecureRandom.uuid) { example.run }
+ end
+
+ it 'returns corresponding user record' do
+ expect(service.user_actor).to be(nil)
+ end
+ end
+ end
+
+ describe '#repository, #project_actor, #group_actor' do
+ context 'when normal project repository' do
+ let_it_be(:project) { create(:project, group: create(:group)) }
+ let(:expected_project) { project }
+ let(:expected_group) { Feature::Gitaly::ActorWrapper.new(::Group, project.group.id) }
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { project.repository }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { project.repository.raw }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { raw_repo_without_container(project.repository) }
+ end
+ end
+
+ context 'when project wiki repository' do
+ let_it_be(:project) { create(:project, :wiki_repo, group: create(:group)) }
+ let(:expected_project) { nil }
+ let(:expected_group) { nil }
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { project.wiki.repository }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { project.wiki.repository.raw }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { raw_repo_without_container(project.wiki.repository) }
+ end
+ end
+
+ context 'when repository of project in user namespace' do
+ let_it_be(:project) { create(:project, namespace: create(:user).namespace) }
+ let(:expected_project) { project }
+ let(:expected_group) { Feature::Gitaly::ActorWrapper.new(::Group, project.namespace_id) }
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { project.repository }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { project.repository.raw }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { raw_repo_without_container(project.repository) }
+ end
+ end
+
+ context 'when personal snippet' do
+ let(:snippet) { create(:personal_snippet) }
+ let(:expected_project) { nil }
+ let(:expected_group) { nil }
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { snippet.repository }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { snippet.repository.raw }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { raw_repo_without_container(snippet.repository) }
+ end
+ end
+
+ context 'when project snippet' do
+ let_it_be(:project) { create(:project, group: create(:group)) }
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:expected_project) { nil }
+ let(:expected_group) { nil }
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { snippet.repository }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { snippet.repository.raw }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { raw_repo_without_container(snippet.repository) }
+ end
+ end
+
+ context 'when project design' do
+ let_it_be(:project) { create(:project, group: create(:group)) }
+ let(:issue) { create(:issue, project: project) }
+ let(:design) { create(:design, issue: issue) }
+
+ let(:expected_project) { project }
+ let(:expected_group) { project.group }
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { design.repository }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { design.repository.raw }
+ end
+
+ it_behaves_like 'Gitaly feature flag actors are inferred from repository' do
+ let(:repository) { raw_repo_without_container(design.repository) }
+ end
+ end
+ end
+
+ describe '#gitaly_client_call' do
+ let(:call_arg_1) { double }
+ let(:call_arg_2) { double }
+ let(:call_arg_3) { double }
+ let(:call_result) { double }
+
+ before do
+ allow(Gitlab::GitalyClient).to receive(:call).and_return(call_result)
+ end
+
+ context 'when actors_aware_gitaly_calls flag is enabled' do
+ let(:repository_actor) { instance_double(::Repository) }
+ let(:user_actor) { instance_double(::User) }
+ let(:project_actor) { instance_double(Project) }
+ let(:group_actor) { instance_double(Group) }
+
+ before do
+ stub_feature_flags(actors_aware_gitaly_calls: true)
+
+ allow(service).to receive(:user_actor).and_return(user_actor)
+ allow(service).to receive(:repository_actor).and_return(repository_actor)
+ allow(service).to receive(:project_actor).and_return(project_actor)
+ allow(service).to receive(:group_actor).and_return(group_actor)
+ allow(Gitlab::GitalyClient).to receive(:with_feature_flag_actors).and_call_original
+ end
+
+ it 'triggers client call with feature flag actors' do
+ result = service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
+
+ expect(Gitlab::GitalyClient).to have_received(:call).with(call_arg_1, call_arg_2, karg: call_arg_3)
+ expect(Gitlab::GitalyClient).to have_received(:with_feature_flag_actors).with(
+ repository: repository_actor,
+ user: user_actor,
+ project: project_actor,
+ group: group_actor
+ )
+ expect(result).to be(call_result)
+ end
+
+ context 'when call without repository_actor' do
+ before do
+ allow(service).to receive(:repository_actor).and_return(nil)
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).and_call_original
+ end
+
+ it 'calls error tracking track_and_raise_for_dev_exception' do
+ expect do
+ service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
+ end.to raise_error /gitaly_client_call called without setting repository_actor/
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with(
+ be_a(Feature::InvalidFeatureFlagError)
+ )
+ end
+ end
+ end
+
+ context 'when actors_aware_gitaly_calls not enabled' do
+ before do
+ stub_feature_flags(actors_aware_gitaly_calls: false)
+ end
+
+ it 'triggers client call without feature flag actors' do
+ expect(Gitlab::GitalyClient).not_to receive(:with_feature_flag_actors)
+
+ result = service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3)
+
+ expect(Gitlab::GitalyClient).to have_received(:call).with(call_arg_1, call_arg_2, karg: call_arg_3)
+ expect(result).to be(call_result)
+ end
+ end
+
+ describe '#gitaly_feature_flag_actors' do
+ let_it_be(:project) { create(:project) }
+ let(:repository_actor) { project.repository }
+
+ context 'when actors_aware_gitaly_calls flag is enabled' do
+ let(:user_actor) { instance_double(::User) }
+ let(:project_actor) { instance_double(Project) }
+ let(:group_actor) { instance_double(Group) }
+
+ before do
+ stub_feature_flags(actors_aware_gitaly_calls: true)
+
+ allow(Feature::Gitaly).to receive(:user_actor).and_return(user_actor)
+ allow(Feature::Gitaly).to receive(:project_actor).with(project).and_return(project_actor)
+ allow(Feature::Gitaly).to receive(:group_actor).with(project).and_return(group_actor)
+ end
+
+ it 'returns a hash with collected feature flag actors' do
+ result = service.gitaly_feature_flag_actors(repository_actor)
+ expect(result).to eql(
+ repository: repository_actor,
+ user: user_actor,
+ project: project_actor,
+ group: group_actor
+ )
+
+ expect(Feature::Gitaly).to have_received(:user_actor).with(no_args)
+ expect(Feature::Gitaly).to have_received(:project_actor).with(project)
+ expect(Feature::Gitaly).to have_received(:group_actor).with(project)
+ end
+ end
+
+ context 'when actors_aware_gitaly_calls not enabled' do
+ before do
+ stub_feature_flags(actors_aware_gitaly_calls: false)
+ end
+
+ it 'returns an empty hash' do
+ expect(Feature::Gitaly).not_to receive(:user_actor)
+ expect(Feature::Gitaly).not_to receive(:project_actor)
+ expect(Feature::Gitaly).not_to receive(:group_actor)
+
+ result = service.gitaly_feature_flag_actors(repository_actor)
+ expect(result).to eql({})
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index a3840ca843f..3d33bf93c23 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -259,6 +259,102 @@ RSpec.describe Gitlab::GitalyClient do
end
end
+ shared_examples 'gitaly feature flags in metadata' do
+ before do
+ allow(Feature::Gitaly).to receive(:server_feature_flags).and_return(
+ 'gitaly-feature-a' => 'true',
+ 'gitaly-feature-b' => 'false'
+ )
+ end
+
+ it 'evaluates Gitaly server feature flags' do
+ metadata = described_class.request_kwargs('default', timeout: 1)[:metadata]
+
+ expect(Feature::Gitaly).to have_received(:server_feature_flags).with(no_args)
+ expect(metadata['gitaly-feature-a']).to be('true')
+ expect(metadata['gitaly-feature-b']).to be('false')
+ end
+
+ context 'when there are actors' do
+ let(:repository_actor) { double(:actor) }
+ let(:project_actor) { double(:actor) }
+ let(:user_actor) { double(:actor) }
+ let(:group_actor) { double(:actor) }
+
+ it 'evaluates Gitaly server feature flags with actors' do
+ metadata = described_class.with_feature_flag_actors(
+ repository: repository_actor,
+ project: project_actor,
+ user: user_actor,
+ group: group_actor
+ ) do
+ described_class.request_kwargs('default', timeout: 1)[:metadata]
+ end
+
+ expect(Feature::Gitaly).to have_received(:server_feature_flags).with(
+ repository: repository_actor,
+ project: project_actor,
+ user: user_actor,
+ group: group_actor
+ )
+ expect(metadata['gitaly-feature-a']).to be('true')
+ expect(metadata['gitaly-feature-b']).to be('false')
+ end
+ end
+ end
+
+ context 'server_feature_flags when RequestStore is activated', :request_store do
+ it_behaves_like 'gitaly feature flags in metadata'
+ end
+
+ context 'server_feature_flags when RequestStore is not activated' do
+ it_behaves_like 'gitaly feature flags in metadata'
+ end
+
+ context 'logging information in metadata' do
+ let(:user) { create(:user) }
+
+ context 'user is added to application context' do
+ it 'injects username and user_id into gRPC metadata' do
+ metadata = {}
+ ::Gitlab::ApplicationContext.with_context(user: user) do
+ metadata = described_class.request_kwargs('default', timeout: 1)[:metadata]
+ end
+
+ expect(metadata['username']).to eql(user.username)
+ expect(metadata['user_id']).to eql(user.id.to_s)
+ end
+ end
+
+ context 'user is not added to application context' do
+ it 'does not inject username and user_id into gRPC metadata' do
+ metadata = described_class.request_kwargs('default', timeout: 1)[:metadata]
+
+ expect(metadata).not_to have_key('username')
+ expect(metadata).not_to have_key('user_id')
+ end
+ end
+
+ context 'remote_ip is added to application context' do
+ it 'injects remote_ip into gRPC metadata' do
+ metadata = {}
+ ::Gitlab::ApplicationContext.with_context(remote_ip: '1.2.3.4') do
+ metadata = described_class.request_kwargs('default', timeout: 1)[:metadata]
+ end
+
+ expect(metadata['remote_ip']).to eql('1.2.3.4')
+ end
+ end
+
+ context 'remote_ip is not added to application context' do
+ it 'does not inject remote_ip into gRPC metadata' do
+ metadata = described_class.request_kwargs('default', timeout: 1)[:metadata]
+
+ expect(metadata).not_to have_key('remote_ip')
+ end
+ end
+ end
+
context 'gitlab_git_env' do
let(:policy) { 'gitaly-route-repository-accessor-policy' }
@@ -585,4 +681,42 @@ RSpec.describe Gitlab::GitalyClient do
end
end
end
+
+ describe '.with_feature_flag_actor', :request_store do
+ shared_examples 'with_feature_flag_actor' do
+ let(:repository_actor) { double(:actor) }
+ let(:project_actor) { double(:actor) }
+ let(:user_actor) { double(:actor) }
+ let(:group_actor) { double(:actor) }
+
+ it 'allows access to feature flag actors inside the block' do
+ expect(described_class.feature_flag_actors).to eql({})
+
+ described_class.with_feature_flag_actors(
+ repository: repository_actor,
+ project: project_actor,
+ user: user_actor,
+ group: group_actor
+ ) do
+ expect(
+ described_class.feature_flag_actors
+ ).to eql(
+ repository: repository_actor,
+ project: project_actor,
+ user: user_actor,
+ group: group_actor)
+ end
+
+ expect(described_class.feature_flag_actors).to eql({})
+ end
+ end
+
+ context 'when RequestStore is activated', :request_store do
+ it_behaves_like 'with_feature_flag_actor'
+ end
+
+ context 'when RequestStore is not activated' do
+ it_behaves_like 'with_feature_flag_actor'
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
index 57391e06192..dc9f939a19b 100644
--- a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
+++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader do
let_it_be(:content_type) { 'application/octet-stream' }
let(:content_length) { 1000 }
- let(:chunk_double) { instance_double(HTTParty::FragmentWithResponse, code: 200) }
+ let(:chunk_double) { instance_double(HTTParty::ResponseFragment, code: 200) }
let(:headers_double) do
instance_double(
HTTParty::Response,
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 3361b039a27..95f7933fbc5 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -3,24 +3,24 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Client do
- describe '#parallel?' do
- it 'returns true when the client is running in parallel mode' do
- client = described_class.new('foo', parallel: true)
+ subject(:client) { described_class.new('foo', parallel: parallel) }
+
+ let(:parallel) { true }
- expect(client).to be_parallel
+ describe '#parallel?' do
+ context 'when the client is running in parallel mode' do
+ it { expect(client).to be_parallel }
end
- it 'returns false when the client is running in sequential mode' do
- client = described_class.new('foo', parallel: false)
+ context 'when the client is running in sequential mode' do
+ let(:parallel) { false }
- expect(client).not_to be_parallel
+ it { expect(client).not_to be_parallel }
end
end
describe '#user' do
it 'returns the details for the given username' do
- client = described_class.new('foo')
-
expect(client.octokit).to receive(:user).with('foo')
expect(client).to receive(:with_rate_limit).and_yield
@@ -30,8 +30,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#pull_request_reviews' do
it 'returns the pull request reviews' do
- client = described_class.new('foo')
-
expect(client)
.to receive(:each_object)
.with(:pull_request_reviews, 'foo/bar', 999)
@@ -40,10 +38,17 @@ RSpec.describe Gitlab::GithubImport::Client do
end
end
+ describe '#pull_request_review_requests' do
+ it 'returns the pull request review requests' do
+ expect(client.octokit).to receive(:pull_request_review_requests).with('foo/bar', 999)
+ expect(client).to receive(:with_rate_limit).and_yield
+
+ client.pull_request_review_requests('foo/bar', 999)
+ end
+ end
+
describe '#repos' do
it 'returns the user\'s repositories as a hash' do
- client = described_class.new('foo')
-
stub_request(:get, 'https://api.github.com/rate_limit')
.to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 })
@@ -58,8 +63,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#repository' do
it 'returns the details of a repository' do
- client = described_class.new('foo')
-
expect(client.octokit).to receive(:repo).with('foo/bar')
expect(client).to receive(:with_rate_limit).and_yield
@@ -67,8 +70,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
it 'returns repository data as a hash' do
- client = described_class.new('foo')
-
stub_request(:get, 'https://api.github.com/rate_limit')
.to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 })
@@ -83,8 +84,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#pull_request' do
it 'returns the details of a pull_request' do
- client = described_class.new('foo')
-
expect(client.octokit).to receive(:pull_request).with('foo/bar', 999)
expect(client).to receive(:with_rate_limit).and_yield
@@ -94,8 +93,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#labels' do
it 'returns the labels' do
- client = described_class.new('foo')
-
expect(client)
.to receive(:each_object)
.with(:labels, 'foo/bar')
@@ -106,8 +103,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#milestones' do
it 'returns the milestones' do
- client = described_class.new('foo')
-
expect(client)
.to receive(:each_object)
.with(:milestones, 'foo/bar')
@@ -118,8 +113,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#releases' do
it 'returns the releases' do
- client = described_class.new('foo')
-
expect(client)
.to receive(:each_object)
.with(:releases, 'foo/bar')
@@ -130,8 +123,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#branches' do
it 'returns the branches' do
- client = described_class.new('foo')
-
expect(client)
.to receive(:each_object)
.with(:branches, 'foo/bar')
@@ -142,8 +133,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#branch_protection' do
it 'returns the protection details for the given branch' do
- client = described_class.new('foo')
-
expect(client.octokit)
.to receive(:branch_protection).with('org/repo', 'bar')
expect(client).to receive(:with_rate_limit).and_yield
@@ -156,8 +145,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#each_object' do
it 'converts each object into a hash' do
- client = described_class.new('foo')
-
stub_request(:get, 'https://api.github.com/rate_limit')
.to_return(status: 200, headers: { 'X-RateLimit-Limit' => 5000, 'X-RateLimit-Remaining' => 5000 })
@@ -171,7 +158,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#each_page' do
- let(:client) { described_class.new('foo') }
let(:object1) { double(:object1) }
let(:object2) { double(:object2) }
@@ -242,8 +228,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#with_rate_limit' do
- let(:client) { described_class.new('foo') }
-
it 'yields the supplied block when enough requests remain' do
expect(client).to receive(:requests_remaining?).and_return(true)
@@ -340,8 +324,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#requests_remaining?' do
- let(:client) { described_class.new('foo') }
-
context 'when default requests limit is set' do
before do
allow(client).to receive(:requests_limit).and_return(5000)
@@ -380,44 +362,43 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#raise_or_wait_for_rate_limit' do
- it 'raises RateLimitError when running in parallel mode' do
- client = described_class.new('foo', parallel: true)
-
- expect { client.raise_or_wait_for_rate_limit }
- .to raise_error(Gitlab::GithubImport::RateLimitError)
+ context 'when running in parallel mode' do
+ it 'raises RateLimitError' do
+ expect { client.raise_or_wait_for_rate_limit }
+ .to raise_error(Gitlab::GithubImport::RateLimitError)
+ end
end
- it 'sleeps when running in sequential mode' do
- client = described_class.new('foo', parallel: false)
-
- expect(client).to receive(:rate_limit_resets_in).and_return(1)
- expect(client).to receive(:sleep).with(1)
+ context 'when running in sequential mode' do
+ let(:parallel) { false }
- client.raise_or_wait_for_rate_limit
- end
+ it 'sleeps' do
+ expect(client).to receive(:rate_limit_resets_in).and_return(1)
+ expect(client).to receive(:sleep).with(1)
- it 'increments the rate limit counter' do
- client = described_class.new('foo', parallel: false)
+ client.raise_or_wait_for_rate_limit
+ end
- expect(client)
- .to receive(:rate_limit_resets_in)
- .and_return(1)
+ it 'increments the rate limit counter' do
+ expect(client)
+ .to receive(:rate_limit_resets_in)
+ .and_return(1)
- expect(client)
- .to receive(:sleep)
- .with(1)
+ expect(client)
+ .to receive(:sleep)
+ .with(1)
- expect(client.rate_limit_counter)
- .to receive(:increment)
- .and_call_original
+ expect(client.rate_limit_counter)
+ .to receive(:increment)
+ .and_call_original
- client.raise_or_wait_for_rate_limit
+ client.raise_or_wait_for_rate_limit
+ end
end
end
describe '#remaining_requests' do
it 'returns the number of remaining requests' do
- client = described_class.new('foo')
rate_limit = double(remaining: 1)
expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
@@ -427,7 +408,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#requests_limit' do
it 'returns requests limit' do
- client = described_class.new('foo')
rate_limit = double(limit: 1)
expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
@@ -437,7 +417,6 @@ RSpec.describe Gitlab::GithubImport::Client do
describe '#rate_limit_resets_in' do
it 'returns the number of seconds after which the rate limit is reset' do
- client = described_class.new('foo')
rate_limit = double(resets_in: 1)
expect(client.octokit).to receive(:rate_limit).and_return(rate_limit)
@@ -447,8 +426,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#api_endpoint' do
- let(:client) { described_class.new('foo') }
-
context 'without a custom endpoint configured in Omniauth' do
it 'returns the default API endpoint' do
expect(client)
@@ -473,8 +450,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#web_endpoint' do
- let(:client) { described_class.new('foo') }
-
context 'without a custom endpoint configured in Omniauth' do
it 'returns the default web endpoint' do
expect(client)
@@ -499,8 +474,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#custom_api_endpoint' do
- let(:client) { described_class.new('foo') }
-
context 'without a custom endpoint' do
it 'returns nil' do
expect(client)
@@ -533,8 +506,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#verify_ssl' do
- let(:client) { described_class.new('foo') }
-
context 'without a custom configuration' do
it 'returns true' do
expect(client)
@@ -553,8 +524,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#github_omniauth_provider' do
- let(:client) { described_class.new('foo') }
-
context 'without a configured provider' do
it 'returns an empty Hash' do
expect(Gitlab.config.omniauth)
@@ -576,8 +545,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe '#rate_limiting_enabled?' do
- let(:client) { described_class.new('foo') }
-
it 'returns true when using GitHub.com' do
expect(client.rate_limiting_enabled?).to eq(true)
end
@@ -592,7 +559,6 @@ RSpec.describe Gitlab::GithubImport::Client do
end
describe 'search' do
- let(:client) { described_class.new('foo') }
let(:user) { { login: 'user' } }
let(:org1) { { login: 'org1' } }
let(:org2) { { login: 'org2' } }
diff --git a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb
index 4476b4123ee..6a409762599 100644
--- a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb
@@ -10,7 +10,9 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:issuable) { create(:issue, project: project) }
- let!(:label) { create(:label, project: project) }
+ let(:label) { create(:label, project: project) }
+ let(:label_title) { label.title }
+ let(:label_id) { label.id }
let(:issue_event) do
Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
@@ -18,7 +20,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
'actor' => { 'id' => user.id, 'login' => user.username },
'event' => event_type,
'commit_id' => nil,
- 'label_title' => label.title,
+ 'label_title' => label_title,
'created_at' => '2022-04-26 18:30:53 UTC',
'issue' => { 'number' => issuable.iid, pull_request: issuable.is_a?(MergeRequest) }
)
@@ -27,7 +29,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
let(:event_attrs) do
{
user_id: user.id,
- label_id: label.id,
+ label_id: label_id,
created_at: issue_event.created_at
}.stringify_keys
end
@@ -42,7 +44,6 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
end
before do
- allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id)
allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder|
allow(finder).to receive(:database_id).and_return(issuable.id)
end
@@ -52,16 +53,35 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
end
context 'with Issue' do
- context 'when importing a labeled event' do
- let(:event_type) { 'labeled' }
- let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'add') }
+ context 'when importing event with associated label' do
+ before do
+ allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id)
+ end
- it_behaves_like 'new event'
+ context 'when importing a labeled event' do
+ let(:event_type) { 'labeled' }
+ let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'add') }
+
+ it_behaves_like 'new event'
+ end
+
+ context 'when importing an unlabeled event' do
+ let(:event_type) { 'unlabeled' }
+ let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'remove') }
+
+ it_behaves_like 'new event'
+ end
end
- context 'when importing an unlabeled event' do
- let(:event_type) { 'unlabeled' }
- let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'remove') }
+ context 'when importing event without associated label' do
+ before do
+ allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(nil)
+ end
+
+ let(:label_title) { 'deleted_label' }
+ let(:label_id) { nil }
+ let(:event_type) { 'labeled' }
+ let(:expected_event_attrs) { event_attrs.merge(issue_id: issuable.id, action: 'add') }
it_behaves_like 'new event'
end
@@ -70,16 +90,35 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
context 'with MergeRequest' do
let(:issuable) { create(:merge_request, source_project: project, target_project: project) }
- context 'when importing a labeled event' do
- let(:event_type) { 'labeled' }
- let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'add') }
+ context 'when importing event with associated label' do
+ before do
+ allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id)
+ end
- it_behaves_like 'new event'
+ context 'when importing a labeled event' do
+ let(:event_type) { 'labeled' }
+ let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'add') }
+
+ it_behaves_like 'new event'
+ end
+
+ context 'when importing an unlabeled event' do
+ let(:event_type) { 'unlabeled' }
+ let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'remove') }
+
+ it_behaves_like 'new event'
+ end
end
- context 'when importing an unlabeled event' do
- let(:event_type) { 'unlabeled' }
- let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'remove') }
+ context 'when importing event without associated label' do
+ before do
+ allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(nil)
+ end
+
+ let(:label_title) { 'deleted_label' }
+ let(:label_id) { nil }
+ let(:event_type) { 'labeled' }
+ let(:expected_event_attrs) { event_attrs.merge(merge_request_id: issuable.id, action: 'add') }
it_behaves_like 'new event'
end
diff --git a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
index 027b2ac422e..d6b7411e640 100644
--- a/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/protected_branch_importer_spec.rb
@@ -6,20 +6,23 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
subject(:importer) { described_class.new(github_protected_branch, project, client) }
let(:branch_name) { 'protection' }
- let(:allow_force_pushes_on_github) { true }
+ let(:allow_force_pushes_on_github) { false }
+ let(:require_code_owner_reviews_on_github) { false }
let(:required_conversation_resolution) { false }
let(:required_signatures) { false }
let(:required_pull_request_reviews) { false }
let(:expected_push_access_level) { Gitlab::Access::MAINTAINER }
let(:expected_merge_access_level) { Gitlab::Access::MAINTAINER }
- let(:expected_allow_force_push) { true }
+ let(:expected_allow_force_push) { false }
+ let(:expected_code_owner_approval_required) { false }
let(:github_protected_branch) do
Gitlab::GithubImport::Representation::ProtectedBranch.new(
id: branch_name,
allow_force_pushes: allow_force_pushes_on_github,
required_conversation_resolution: required_conversation_resolution,
required_signatures: required_signatures,
- required_pull_request_reviews: required_pull_request_reviews
+ required_pull_request_reviews: required_pull_request_reviews,
+ require_code_owner_reviews: require_code_owner_reviews_on_github
)
end
@@ -35,7 +38,8 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
name: 'protection',
push_access_levels_attributes: [{ access_level: expected_push_access_level }],
merge_access_levels_attributes: [{ access_level: expected_merge_access_level }],
- allow_force_push: expected_allow_force_push
+ allow_force_push: expected_allow_force_push,
+ code_owner_approval_required: expected_code_owner_approval_required
}
end
@@ -70,41 +74,35 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
end
context 'when branch is protected on GitLab' do
- before do
- create(
- :protected_branch,
- project: project,
- name: 'protect*',
- allow_force_push: allow_force_pushes_on_gitlab
- )
+ using RSpec::Parameterized::TableSyntax
+
+ where(
+ :allow_force_pushes_on_github,
+ :allow_force_pushes_on_gitlab,
+ :expected_allow_force_push
+ ) do
+ true | true | true
+ true | false | false
+ false | true | false
+ false | false | false
end
- context 'when branch protection rule on Gitlab is stricter than on Github' do
- let(:allow_force_pushes_on_github) { true }
- let(:allow_force_pushes_on_gitlab) { false }
- let(:expected_allow_force_push) { false }
-
- it_behaves_like 'create branch protection by the strictest ruleset'
- end
-
- context 'when branch protection rule on Github is stricter than on Gitlab' do
- let(:allow_force_pushes_on_github) { false }
- let(:allow_force_pushes_on_gitlab) { true }
- let(:expected_allow_force_push) { false }
-
- it_behaves_like 'create branch protection by the strictest ruleset'
- end
-
- context 'when branch protection rules on Github and Gitlab are the same' do
- let(:allow_force_pushes_on_github) { true }
- let(:allow_force_pushes_on_gitlab) { true }
- let(:expected_allow_force_push) { true }
+ with_them do
+ before do
+ create(
+ :protected_branch,
+ project: project,
+ name: 'protect*',
+ allow_force_push: allow_force_pushes_on_gitlab
+ )
+ end
it_behaves_like 'create branch protection by the strictest ruleset'
end
end
context 'when branch is not protected on GitLab' do
+ let(:allow_force_pushes_on_github) { true }
let(:expected_allow_force_push) { true }
it_behaves_like 'create branch protection by the strictest ruleset'
@@ -115,6 +113,30 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
allow(project).to receive(:default_branch).and_return(branch_name)
end
+ context 'when "allow force pushes - everyone" rule is enabled' do
+ let(:allow_force_pushes_on_github) { true }
+
+ context 'when there is any default branch protection' do
+ before do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+ end
+
+ let(:expected_allow_force_push) { false }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+
+ context 'when there is no default branch protection' do
+ before do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ let(:expected_allow_force_push) { allow_force_pushes_on_github }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+ end
+
context 'when required_conversation_resolution rule is enabled' do
let(:required_conversation_resolution) { true }
@@ -241,7 +263,8 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
:protected_branch,
project: project,
name: 'protect*',
- allow_force_push: true
+ allow_force_push: true,
+ code_owner_approval_required: false
)
end
@@ -297,5 +320,67 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchImporter do
it_behaves_like 'create branch protection by the strictest ruleset'
end
end
+
+ context 'when the code_owner_approval_required feature is available', if: Gitlab.ee? do
+ before do
+ stub_licensed_features(code_owner_approval_required: true)
+ end
+
+ context 'when branch is protected on GitLab' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(
+ :require_code_owner_reviews_on_github,
+ :require_code_owner_reviews_on_gitlab,
+ :expected_code_owner_approval_required
+ ) do
+ true | true | true
+ true | false | true
+ false | true | true
+ false | false | false
+ end
+
+ with_them do
+ before do
+ create(
+ :protected_branch,
+ project: project,
+ name: 'protect*',
+ allow_force_push: true,
+ code_owner_approval_required: require_code_owner_reviews_on_gitlab
+ )
+ end
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+ end
+
+ context 'when branch is not protected on GitLab' do
+ context 'when require_code_owner_reviews rule is enabled on GitHub' do
+ let(:require_code_owner_reviews_on_github) { true }
+ let(:expected_code_owner_approval_required) { true }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+
+ context 'when require_code_owner_reviews rule is disabled on GitHub' do
+ let(:require_code_owner_reviews_on_github) { false }
+ let(:expected_code_owner_approval_required) { false }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
+ end
+ end
+
+ context 'when the code_owner_approval_required feature is not available' do
+ before do
+ stub_licensed_features(code_owner_approval_required: false)
+ end
+
+ let(:require_code_owner_reviews_on_github) { true }
+ let(:expected_code_owner_approval_required) { false }
+
+ it_behaves_like 'create branch protection by the strictest ruleset'
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
index a0ced456391..8809d58a252 100644
--- a/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/protected_branches_importer_spec.rb
@@ -29,7 +29,10 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
required_signatures = Struct.new(:url, :enabled, keyword_init: true)
enforce_admins = Struct.new(:url, :enabled, keyword_init: true)
allow_option = Struct.new(:enabled, keyword_init: true)
- required_pull_request_reviews = Struct.new(:url, :dismissal_restrictions, keyword_init: true)
+ required_pull_request_reviews = Struct.new(
+ :url, :dismissal_restrictions, :require_code_owner_reviews,
+ keyword_init: true
+ )
response.new(
name: 'main',
url: 'https://example.com/branches/main/protection',
@@ -58,7 +61,8 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
),
required_pull_request_reviews: required_pull_request_reviews.new(
url: 'https://example.com/branches/main/protection/required_pull_request_reviews',
- dismissal_restrictions: {}
+ dismissal_restrictions: {},
+ require_code_owner_reviews: true
)
)
end
@@ -160,6 +164,7 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
let(:branch_struct) { Struct.new(:protection, :name, :url, keyword_init: true) }
let(:protection_struct) { Struct.new(:enabled, keyword_init: true) }
let(:protected_branch) { branch_struct.new(name: 'main', protection: protection_struct.new(enabled: true)) }
+ let(:second_protected_branch) { branch_struct.new(name: 'fix', protection: protection_struct.new(enabled: true)) }
let(:unprotected_branch) { branch_struct.new(name: 'staging', protection: protection_struct.new(enabled: false)) }
# when user has no admin rights on repo
let(:unknown_protection_branch) { branch_struct.new(name: 'development', protection: nil) }
@@ -168,9 +173,9 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
before do
allow(client).to receive(:branches).with(project.import_source)
- .and_return([protected_branch, unprotected_branch, unknown_protection_branch])
+ .and_return([protected_branch, second_protected_branch, unprotected_branch, unknown_protection_branch])
allow(client).to receive(:branch_protection)
- .with(project.import_source, protected_branch.name).once
+ .with(project.import_source, anything)
.and_return(github_protection_rule)
allow(Gitlab::GithubImport::ObjectCounter).to receive(:increment)
.with(project, :protected_branch, :fetched)
@@ -180,12 +185,13 @@ RSpec.describe Gitlab::GithubImport::Importer::ProtectedBranchesImporter do
subject.each_object_to_import do |object|
expect(object).to eq github_protection_rule
end
- expect(Gitlab::GithubImport::ObjectCounter).to have_received(:increment).once
+ expect(Gitlab::GithubImport::ObjectCounter).to have_received(:increment).twice
end
context 'when protected branch is already processed' do
it "doesn't process this branch" do
subject.mark_as_imported(protected_branch)
+ subject.mark_as_imported(second_protected_branch)
subject.each_object_to_import {}
expect(Gitlab::GithubImport::ObjectCounter).not_to have_received(:increment)
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
index fb6024d0952..49794eceb5a 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb
@@ -8,11 +8,48 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
let_it_be(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
- let(:client_double) { double(user: { id: 999, login: 'author', email: 'author@email.com' }) }
let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc }
+ let(:client_double) do
+ instance_double(
+ 'Gitlab::GithubImport::Client',
+ user: { id: 999, login: 'author', email: 'author@email.com' }
+ )
+ end
subject { described_class.new(review, project, client_double) }
+ shared_examples 'imports a reviewer for the Merge Request' do
+ it 'creates reviewer for the Merge Request' do
+ expect { subject.execute }.to change(MergeRequestReviewer, :count).by(1)
+
+ expect(merge_request.reviewers).to contain_exactly(author)
+ end
+
+ context 'when reviewer already exists' do
+ before do
+ create(
+ :merge_request_reviewer,
+ reviewer: author, merge_request: merge_request, state: 'unreviewed'
+ )
+ end
+
+ it 'does not change Merge Request reviewers' do
+ expect { subject.execute }.not_to change(MergeRequestReviewer, :count)
+
+ expect(merge_request.reviewers).to contain_exactly(author)
+ end
+ end
+ end
+
+ shared_examples 'imports an approval for the Merge Request' do
+ it 'creates an approval for the Merge Request' do
+ expect { subject.execute }.to change(Approval, :count).by(1)
+
+ expect(merge_request.approved_by_users.reload).to include(author)
+ expect(merge_request.approvals.last.created_at).to eq(submitted_at)
+ end
+ end
+
context 'when the review author can be mapped to a gitlab user' do
let_it_be(:author) { create(:user, email: 'author@email.com') }
@@ -20,34 +57,38 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
context 'when the review is "APPROVED"' do
let(:review) { create_review(type: 'APPROVED', note: '') }
- it 'creates a note for the review and approves the Merge Request' do
- expect { subject.execute }
- .to change(Note, :count).by(1)
- .and change(Approval, :count).by(1)
+ it_behaves_like 'imports an approval for the Merge Request'
+ it_behaves_like 'imports a reviewer for the Merge Request'
+
+ it 'creates a note for the review' do
+ expect { subject.execute }.to change(Note, :count).by(1)
last_note = merge_request.notes.last
expect(last_note.note).to eq('approved this merge request')
expect(last_note.author).to eq(author)
expect(last_note.created_at).to eq(submitted_at)
expect(last_note.system_note_metadata.action).to eq('approved')
-
- expect(merge_request.approved_by_users.reload).to include(author)
- expect(merge_request.approvals.last.created_at).to eq(submitted_at)
end
- it 'does nothing if the user already approved the merge request' do
- create(:approval, merge_request: merge_request, user: author)
+ context 'when the user already approved the merge request' do
+ before do
+ create(:approval, merge_request: merge_request, user: author)
+ end
- expect { subject.execute }
- .to change(Note, :count).by(0)
- .and change(Approval, :count).by(0)
+ it 'does not import second approve and note' do
+ expect { subject.execute }
+ .to change(Note, :count).by(0)
+ .and change(Approval, :count).by(0)
+ end
end
end
context 'when the review is "COMMENTED"' do
let(:review) { create_review(type: 'COMMENTED', note: '') }
- it 'creates a note for the review' do
+ it_behaves_like 'imports a reviewer for the Merge Request'
+
+ it 'does not create note for the review' do
expect { subject.execute }.not_to change(Note, :count)
end
end
@@ -55,7 +96,9 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
context 'when the review is "CHANGES_REQUESTED"' do
let(:review) { create_review(type: 'CHANGES_REQUESTED', note: '') }
- it 'creates a note for the review' do
+ it_behaves_like 'imports a reviewer for the Merge Request'
+
+ it 'does not create a note for the review' do
expect { subject.execute }.not_to change(Note, :count)
end
end
@@ -65,10 +108,11 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
context 'when the review is "APPROVED"' do
let(:review) { create_review(type: 'APPROVED') }
+ it_behaves_like 'imports an approval for the Merge Request'
+ it_behaves_like 'imports a reviewer for the Merge Request'
+
it 'creates a note for the review' do
- expect { subject.execute }
- .to change(Note, :count).by(2)
- .and change(Approval, :count).by(1)
+ expect { subject.execute }.to change(Note, :count).by(2)
note = merge_request.notes.where(system: false).last
expect(note.note).to eq("**Review:** Approved\n\nnote")
@@ -80,9 +124,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean
expect(system_note.author).to eq(author)
expect(system_note.created_at).to eq(submitted_at)
expect(system_note.system_note_metadata.action).to eq('approved')
-
- expect(merge_request.approved_by_users.reload).to include(author)
- expect(merge_request.approvals.last.created_at).to eq(submitted_at)
end
end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb
new file mode 100644
index 00000000000..6dcbc4e32e6
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImporter, :clean_gitlab_redis_cache do
+ subject(:importer) { described_class.new(review_request, project, client) }
+
+ let(:project) { instance_double('Project') }
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:merge_request) { create(:merge_request) }
+ let(:reviewer) { create(:user, username: 'alice') }
+ let(:review_request) do
+ Gitlab::GithubImport::Representation::PullRequests::ReviewRequests.from_json_hash(
+ merge_request_id: merge_request.id,
+ users: [
+ { 'id' => 1, 'login' => reviewer.username },
+ { 'id' => 2, 'login' => 'foo' }
+ ]
+ )
+ end
+
+ before do
+ allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+ allow(finder).to receive(:find).with(1, reviewer.username).and_return(reviewer.id)
+ allow(finder).to receive(:find).with(2, 'foo').and_return(nil)
+ end
+ end
+
+ it 'imports merge request reviewers that were found' do
+ importer.execute
+
+ expect(merge_request.reviewers.size).to eq 1
+ expect(merge_request.reviewers[0].id).to eq reviewer.id
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
new file mode 100644
index 00000000000..6c7fc4d5b15
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImporter, :clean_gitlab_redis_cache do
+ subject(:importer) { described_class.new(project, client) }
+
+ let_it_be(:project) { create(:project, import_source: 'foo') }
+
+ let(:client) { instance_double(Gitlab::GithubImport::Client) }
+ let(:review_request_struct) { Struct.new(:merge_request_id, :users, keyword_init: true) }
+ let(:user_struct) { Struct.new(:id, :login, keyword_init: true) }
+
+ shared_context 'when project with merge requests' do
+ let_it_be(:merge_request_1) { create(:merge_request, source_project: project, target_branch: 'feature1') }
+ let_it_be(:merge_request_2) { create(:merge_request, source_project: project, target_branch: 'feature2') }
+
+ let(:importer_stub) { instance_double('Gitlab::GithubImport::Importer::NoteAttachmentsImporter') }
+ let(:importer_attrs) do
+ [instance_of(Gitlab::GithubImport::Representation::PullRequests::ReviewRequests), project, client]
+ end
+
+ let(:review_requests_1) do
+ {
+ users: [
+ { id: 4, login: 'alice' },
+ { id: 5, login: 'bob' }
+ ]
+ }
+ end
+
+ let(:review_requests_2) do
+ {
+ users: [{ id: 4, login: 'alice' }]
+ }
+ end
+
+ before do
+ allow(client).to receive(:pull_request_review_requests)
+ .with(project.import_source, merge_request_1.iid).and_return(review_requests_1)
+ allow(client).to receive(:pull_request_review_requests)
+ .with(project.import_source, merge_request_2.iid).and_return(review_requests_2)
+ end
+ end
+
+ describe '#sequential_import' do
+ include_context 'when project with merge requests'
+
+ it 'imports each project merge request reviewers' do
+ expect_next_instances_of(
+ Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImporter, 2, false, *importer_attrs
+ ) do |note_attachments_importer|
+ expect(note_attachments_importer).to receive(:execute)
+ end
+
+ importer.sequential_import
+ end
+
+ context 'when merge request is already processed' do
+ before do
+ Gitlab::Cache::Import::Caching.set_add(
+ "github-importer/pull_requests/pull_request_review_requests/already-imported/#{project.id}",
+ merge_request_1.iid
+ )
+ end
+
+ it "doesn't import this merge request reviewers" do
+ expect_next_instance_of(
+ Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImporter, *importer_attrs
+ ) do |note_attachments_importer|
+ expect(note_attachments_importer).to receive(:execute)
+ end
+
+ importer.sequential_import
+ end
+ end
+ end
+
+ describe '#parallel_import' do
+ include_context 'when project with merge requests'
+
+ let(:expected_worker_payload) do
+ [
+ [
+ project.id,
+ {
+ merge_request_id: merge_request_1.id,
+ users: [
+ { id: 4, login: 'alice' },
+ { id: 5, login: 'bob' }
+ ]
+ },
+ instance_of(String)
+ ],
+ [
+ project.id,
+ {
+ merge_request_id: merge_request_2.id,
+ users: [
+ { id: 4, login: 'alice' }
+ ]
+ },
+ instance_of(String)
+ ]
+ ]
+ end
+
+ it 'schedule import for each merge request reviewers' do
+ expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
+ .to receive(:bulk_perform_in).with(
+ 1.second,
+ expected_worker_payload,
+ batch_size: 1000,
+ batch_delay: 1.minute
+ )
+
+ importer.parallel_import
+ end
+
+ context 'when merge request is already processed' do
+ before do
+ Gitlab::Cache::Import::Caching.set_add(
+ "github-importer/pull_requests/pull_request_review_requests/already-imported/#{project.id}",
+ merge_request_1.iid
+ )
+ end
+
+ it "doesn't schedule import this merge request reviewers" do
+ expect(Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker)
+ .to receive(:bulk_perform_in).with(
+ 1.second,
+ expected_worker_payload.slice(1, 1),
+ batch_size: 1000,
+ batch_delay: 1.minute
+ )
+
+ importer.parallel_import
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
index 30b29659eee..60cae79459e 100644
--- a/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/protected_branch_spec.rb
@@ -24,6 +24,10 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do
it 'includes the protected branch required_pull_request_reviews' do
expect(protected_branch.required_pull_request_reviews).to eq true
end
+
+ it 'includes the protected branch require_code_owner_reviews' do
+ expect(protected_branch.require_code_owner_reviews).to eq true
+ end
end
end
@@ -35,7 +39,10 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do
keyword_init: true
)
enabled_setting = Struct.new(:enabled, keyword_init: true)
- required_pull_request_reviews = Struct.new(:url, :dismissal_restrictions, keyword_init: true)
+ required_pull_request_reviews = Struct.new(
+ :url, :dismissal_restrictions, :require_code_owner_reviews,
+ keyword_init: true
+ )
response.new(
url: 'https://example.com/branches/main/protection',
allow_force_pushes: enabled_setting.new(
@@ -49,7 +56,8 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do
),
required_pull_request_reviews: required_pull_request_reviews.new(
url: 'https://example.com/branches/main/protection/required_pull_request_reviews',
- dismissal_restrictions: {}
+ dismissal_restrictions: {},
+ require_code_owner_reviews: true
)
)
end
@@ -67,7 +75,8 @@ RSpec.describe Gitlab::GithubImport::Representation::ProtectedBranch do
'allow_force_pushes' => true,
'required_conversation_resolution' => true,
'required_signatures' => true,
- 'required_pull_request_reviews' => true
+ 'required_pull_request_reviews' => true,
+ 'require_code_owner_reviews' => true
}
end
diff --git a/spec/lib/gitlab/github_import/representation/pull_requests/review_requests_spec.rb b/spec/lib/gitlab/github_import/representation/pull_requests/review_requests_spec.rb
new file mode 100644
index 00000000000..0393f692a69
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/pull_requests/review_requests_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Representation::PullRequests::ReviewRequests do
+ shared_examples 'Review requests' do
+ it 'returns an instance of Review Request' do
+ expect(review_requests).to be_an_instance_of(described_class)
+ end
+
+ context 'for returned Review Requests' do
+ it 'includes merge request id' do
+ expect(review_requests.merge_request_id).to eq(merge_request_id)
+ end
+
+ it 'includes reviewers' do
+ expect(review_requests.users.size).to eq 2
+
+ user = review_requests.users[0]
+ expect(user).to be_an_instance_of(Gitlab::GithubImport::Representation::User)
+ expect(user.id).to eq(4)
+ expect(user.login).to eq('alice')
+ end
+ end
+ end
+
+ let(:merge_request_id) { 6501124486 }
+ let(:response) do
+ {
+ 'merge_request_id' => merge_request_id,
+ 'users' => [
+ { 'id' => 4, 'login' => 'alice' },
+ { 'id' => 5, 'login' => 'bob' }
+ ]
+ }
+ end
+
+ describe '.from_api_response' do
+ it_behaves_like 'Review requests' do
+ let(:review_requests) { described_class.from_api_response(response) }
+ end
+ end
+
+ describe '.from_json_hash' do
+ it_behaves_like 'Review requests' do
+ let(:review_requests) { described_class.from_json_hash(response) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb
index dd4dcca809b..5a1fcc5e2dc 100644
--- a/spec/lib/gitlab/gon_helper_spec.rb
+++ b/spec/lib/gitlab/gon_helper_spec.rb
@@ -39,6 +39,58 @@ RSpec.describe Gitlab::GonHelper do
helper.add_gon_variables
end
end
+
+ describe 'sentry configuration' do
+ let(:clientside_dsn) { 'https://xxx@sentry.example.com/1' }
+ let(:environment) { 'staging' }
+
+ describe 'sentry integration' do
+ before do
+ stub_config(sentry: { enabled: true, clientside_dsn: clientside_dsn, environment: environment })
+ end
+
+ it 'sets sentry dsn and environment from config' do
+ expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
+ expect(gon).to receive(:sentry_environment=).with(environment)
+
+ helper.add_gon_variables
+ end
+ end
+
+ describe 'new sentry integration' do
+ before do
+ stub_application_setting(sentry_enabled: true)
+ stub_application_setting(sentry_clientside_dsn: clientside_dsn)
+ stub_application_setting(sentry_environment: environment)
+ end
+
+ context 'when enable_new_sentry_clientside_integration is disabled' do
+ before do
+ stub_feature_flags(enable_new_sentry_clientside_integration: false)
+ end
+
+ it 'does not set sentry dsn and environment from config' do
+ expect(gon).not_to receive(:sentry_dsn=).with(clientside_dsn)
+ expect(gon).not_to receive(:sentry_environment=).with(environment)
+
+ helper.add_gon_variables
+ end
+ end
+
+ context 'when enable_new_sentry_clientside_integration is enabled' do
+ before do
+ stub_feature_flags(enable_new_sentry_clientside_integration: true)
+ end
+
+ it 'sets sentry dsn and environment from config' do
+ expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
+ expect(gon).to receive(:sentry_environment=).with(environment)
+
+ helper.add_gon_variables
+ end
+ end
+ end
+ end
end
describe '#push_frontend_feature_flag' do
diff --git a/spec/lib/gitlab/grape_logging/loggers/filter_parameters_spec.rb b/spec/lib/gitlab/grape_logging/loggers/filter_parameters_spec.rb
new file mode 100644
index 00000000000..15c842c9f44
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/filter_parameters_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GrapeLogging::Loggers::FilterParameters do
+ subject { described_class.new }
+
+ describe ".parameters" do
+ let(:route) { instance_double('Grape::Router::Route', settings: settings) }
+ let(:endpoint) { instance_double('Grape::Endpoint', route: route) }
+
+ let(:env) do
+ { 'rack.input' => '', Grape::Env::API_ENDPOINT => endpoint }
+ end
+
+ let(:mock_request) { ActionDispatch::Request.new(env) }
+
+ before do
+ mock_request.params['key'] = 'some key'
+ mock_request.params['foo'] = 'wibble'
+ mock_request.params['value'] = 'some value'
+ mock_request.params['oof'] = 'wobble'
+ mock_request.params['other'] = 'Unaffected'
+ end
+
+ context 'when the log_safety setting is provided' do
+ let(:settings) { { log_safety: { safe: %w[foo bar key], unsafe: %w[oof rab value] } } }
+
+ it 'includes safe parameters, and filters unsafe ones' do
+ data = subject.parameters(mock_request, nil)
+
+ expect(data).to eq(
+ params: {
+ 'key' => 'some key',
+ 'foo' => 'wibble',
+ 'value' => '[FILTERED]',
+ 'oof' => '[FILTERED]',
+ 'other' => 'Unaffected'
+ }
+ )
+ end
+ end
+
+ context 'when the log_safety is not provided' do
+ let(:settings) { {} }
+
+ it 'behaves like the normal parameter filter' do
+ data = subject.parameters(mock_request, nil)
+
+ expect(data).to eq(
+ params: {
+ 'key' => '[FILTERED]',
+ 'foo' => 'wibble',
+ 'value' => 'some value',
+ 'oof' => 'wobble',
+ 'other' => 'Unaffected'
+ }
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 000b8eff661..948452c0b58 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -40,9 +40,9 @@ RSpec.describe Gitlab::HealthChecks::GitalyCheck do
end
let(:healthy_check) { double(check: { success: true }) }
- let(:ready_check) { double(readiness_check: { success: false, message: 'Clock is out of sync' }) }
+ let(:ready_check) { double(readiness_check: { success: false, message: 'A readiness check has failed' }) }
- it { is_expected.to match_array([result_class.new('gitaly_check', false, 'Clock is out of sync', shard: 'default')]) }
+ it { is_expected.to match_array([result_class.new('gitaly_check', false, 'A readiness check has failed', shard: 'default')]) }
end
end
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 cb8fef60ab2..f9a6c25b786 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -78,6 +78,7 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
state
blocking_discussions_resolved
first_contribution
+ detailed_merge_status
].freeze
expect(data).to include(*expected_additional_attributes)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ccc4f1f7149..e9dde1c6180 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -52,6 +52,7 @@ issues:
- user_mentions
- system_note_metadata
- alert_management_alert
+- alert_management_alerts
- status_page_published_incident
- namespace
- note_authors
@@ -361,10 +362,12 @@ hooks:
- web_hook_logs
protected_branches:
- project
+- group
- merge_access_levels
- push_access_levels
- unprotect_access_levels
- approval_project_rules
+- external_status_checks
- required_code_owners_sections
protected_tags:
- project
@@ -538,6 +541,7 @@ project:
- jenkins_integration
- index_status
- feature_usage
+- regular_or_any_approver_approval_rules
- approval_rules
- approval_merge_request_rules
- approval_merge_request_rule_sources
@@ -548,6 +552,7 @@ project:
- path_locks
- approver_groups
- repository_state
+- wiki_repository
- wiki_repository_state
- source_pipelines
- sourced_pipelines
@@ -643,6 +648,7 @@ project:
- build_artifacts_size_refresh
- project_callouts
- pipeline_metadata
+- disable_download_button
award_emoji:
- awardable
- user
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 9af72cc0dea..a6cb74c3c9f 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
@@ -112,7 +112,7 @@ RSpec.describe Gitlab::ImportExport::DecompressedArchiveSizeValidator do
context 'when archive path is not a string' do
let(:filepath) { 123 }
- let(:error_message) { 'Archive path is not a string' }
+ let(:error_message) { 'Invalid path' }
it 'returns false' do
expect(subject.valid?).to eq(false)
diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
index 346f653acd4..5ef9eb78d3b 100644
--- a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
- include GitHelpers
-
describe 'bundle a design Git repo' do
let(:user) { create(:user) }
let!(:project_with_design_repo) { create(:project, :design_repo) }
@@ -29,10 +27,8 @@ RSpec.describe Gitlab::ImportExport::DesignRepoRestorer do
after do
FileUtils.rm_rf(export_path)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- FileUtils.rm_rf(project_with_design_repo.design_repository.path_to_repo)
- FileUtils.rm_rf(project.design_repository.path_to_repo)
- end
+ project_with_design_repo.design_repository.remove
+ project.design_repository.remove
end
it 'restores the repo successfully' do
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index 25c82588c13..9d766eb3af1 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -47,10 +47,8 @@ RSpec.describe 'forked project import' do
after do
FileUtils.rm_rf(export_path)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
- FileUtils.rm_rf(project.repository.path_to_repo)
- end
+ project_with_repo.repository.remove
+ project.repository.remove
end
it 'can access the MR', :sidekiq_might_not_need_inline 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 85d07e3fe63..79ab1913e7e 100644
--- a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeSaver do
members
milestones
].each do |association|
- path = exported_path_for("#{g.id}", "#{association}.ndjson")
+ path = exported_path_for(g.id.to_s, "#{association}.ndjson")
expect(File.exist?(path)).to eq(true), "#{path} does not exist"
end
end
diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
index 51c0008b2b4..b1f5574fba1 100644
--- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
+++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
@@ -96,11 +96,11 @@ RSpec.describe 'Test coverage of the Project Import' do
case item
when Hash
item.each do |k, v|
- if (v.is_a?(Array) || v.is_a?(Hash)) && v.present?
- new_path = path + [k]
- res << new_path
- gather_relations(v, res, new_path)
- end
+ next unless (v.is_a?(Array) || v.is_a?(Hash)) && v.present?
+
+ new_path = path + [k]
+ res << new_path
+ gather_relations(v, res, new_path)
end
when Array
item.each { |i| gather_relations(i, res, path) }
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 550cefea805..3ca9f727033 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -23,9 +23,7 @@ RSpec.describe Gitlab::ImportExport::MergeRequestParser do
end
after do
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- FileUtils.rm_rf(project.repository.path_to_repo)
- end
+ project.repository.remove
end
it 'has a source branch' do
diff --git a/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
new file mode 100644
index 00000000000..a781139acab
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ImportExport::Project::ExportedRelationsMerger do
+ let(:export_job) { create(:project_export_job) }
+
+ let(:shared) { Gitlab::ImportExport::Shared.new(export_job.project) }
+
+ before do
+ create(:project_relation_export_upload,
+ relation_export: create(:project_relation_export, relation: 'project', project_export_job: export_job),
+ export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/project.tar.gz")
+ )
+
+ create(:project_relation_export_upload,
+ relation_export: create(:project_relation_export, relation: 'labels', project_export_job: export_job),
+ export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz")
+ )
+
+ create(:project_relation_export_upload,
+ relation_export: create(:project_relation_export, relation: 'uploads', project_export_job: export_job),
+ export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/uploads.tar.gz")
+ )
+ end
+
+ describe '#save' do
+ subject(:service) { described_class.new(export_job: export_job, shared: shared) }
+
+ it 'downloads, extracts, and merges all files into export_path' do
+ Dir.mktmpdir do |dirpath|
+ allow(shared).to receive(:export_path).and_return(dirpath)
+
+ result = service.save
+
+ expect(result).to eq(true)
+ expect(Dir.glob("#{dirpath}/**/*")).to match_array(
+ [
+ "#{dirpath}/project",
+ "#{dirpath}/project/project.json",
+ "#{dirpath}/project/labels.ndjson",
+ "#{dirpath}/uploads",
+ "#{dirpath}/uploads/70edb596c34ad7795baa6a0f0aa03d44",
+ "#{dirpath}/uploads/70edb596c34ad7795baa6a0f0aa03d44/file1.txt",
+ "#{dirpath}/uploads/c8c93c6f546b002cbce4cb8d05d0dfb8",
+ "#{dirpath}/uploads/c8c93c6f546b002cbce4cb8d05d0dfb8/file2.txt"
+ ]
+ )
+ end
+ end
+
+ context 'when exception occurs' do
+ before do
+ create(:project_relation_export, relation: 'releases', project_export_job: export_job)
+ create(:project_relation_export, relation: 'issues', project_export_job: export_job)
+ end
+
+ it 'registers the exception messages and returns false' do
+ Dir.mktmpdir do |dirpath|
+ allow(shared).to receive(:export_path).and_return(dirpath)
+
+ result = service.save
+
+ expect(result).to eq(false)
+ expect(shared.errors).to match_array(
+ [
+ "undefined method `export_file' for nil:NilClass",
+ "undefined method `export_file' for nil:NilClass"
+ ]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project/relation_saver_spec.rb b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb
index dec51b3afd1..0467b63e918 100644
--- a/spec/lib/gitlab/import_export/project/relation_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb
@@ -28,14 +28,14 @@ RSpec.describe Gitlab::ImportExport::Project::RelationSaver do
it 'serializes the root node as a json file in the export path' do
relation_saver.save # rubocop:disable Rails/SaveBang
- json = read_json(File.join(shared.export_path, 'project.json'))
+ json = read_json(File.join(shared.export_path, 'tree', 'project.json'))
expect(json).to include({ 'description' => 'Project description' })
end
it 'serializes only allowed attributes' do
relation_saver.save # rubocop:disable Rails/SaveBang
- json = read_json(File.join(shared.export_path, 'project.json'))
+ json = read_json(File.join(shared.export_path, 'tree', 'project.json'))
expect(json).to include({ 'description' => 'Project description' })
expect(json.keys).not_to include('name')
end
@@ -54,7 +54,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationSaver do
it 'serializes the child node as a ndjson file in the export path inside the project folder' do
relation_saver.save # rubocop:disable Rails/SaveBang
- ndjson = read_ndjson(File.join(shared.export_path, 'project', "#{relation}.ndjson"))
+ ndjson = read_ndjson(File.join(shared.export_path, 'tree', 'project', "#{relation}.ndjson"))
expect(ndjson.first).to include({ 'title' => 'Label 1' })
expect(ndjson.second).to include({ 'title' => 'Label 2' })
end
@@ -62,7 +62,7 @@ RSpec.describe Gitlab::ImportExport::Project::RelationSaver do
it 'serializes only allowed attributes' do
relation_saver.save # rubocop:disable Rails/SaveBang
- ndjson = read_ndjson(File.join(shared.export_path, 'project', "#{relation}.ndjson"))
+ ndjson = read_ndjson(File.join(shared.export_path, 'tree', 'project', "#{relation}.ndjson"))
expect(ndjson.first.keys).not_to include('description_html')
end
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index fae94a3b544..b753746cd8c 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -160,7 +160,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do
pipeline = Ci::Pipeline.find_by_sha('sha-notes')
pipeline_metadata = pipeline.pipeline_metadata
- expect(pipeline_metadata.title).to eq('Build pipeline')
+ expect(pipeline_metadata.name).to eq('Build pipeline')
expect(pipeline_metadata.pipeline_id).to eq(pipeline.id)
expect(pipeline_metadata.project_id).to eq(pipeline.project_id)
end
diff --git a/spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb b/spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb
new file mode 100644
index 00000000000..6e5be0b2829
--- /dev/null
+++ b/spec/lib/gitlab/import_export/recursive_merge_folders_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::ImportExport::RecursiveMergeFolders do
+ describe '.merge' do
+ it 'merge folder and ignore symlinks' do
+ Dir.mktmpdir do |tmpdir|
+ source = "#{tmpdir}/source"
+ FileUtils.mkdir_p("#{source}/folder/folder")
+ FileUtils.touch("#{source}/file1.txt")
+ FileUtils.touch("#{source}/folder/file2.txt")
+ FileUtils.touch("#{source}/folder/folder/file3.txt")
+ FileUtils.ln_s("#{source}/file1.txt", "#{source}/symlink-file1.txt")
+ FileUtils.ln_s("#{source}/folder", "#{source}/symlink-folder")
+
+ target = "#{tmpdir}/target"
+ FileUtils.mkdir_p("#{target}/folder/folder")
+ FileUtils.mkdir_p("#{target}/folderA")
+ FileUtils.touch("#{target}/fileA.txt")
+
+ described_class.merge(source, target)
+
+ expect(Dir.children("#{tmpdir}/target")).to match_array(%w[folder file1.txt folderA fileA.txt])
+ expect(Dir.children("#{tmpdir}/target/folder")).to match_array(%w[folder file2.txt])
+ expect(Dir.children("#{tmpdir}/target/folder/folder")).to match_array(%w[file3.txt])
+ end
+ end
+
+ it 'raises an error for invalid source path' do
+ Dir.mktmpdir do |tmpdir|
+ expect do
+ described_class.merge("#{tmpdir}/../", tmpdir)
+ end.to raise_error(Gitlab::Utils::PathTraversalAttackError)
+ end
+ end
+
+ it 'raises an error for source path outside temp dir' do
+ Dir.mktmpdir do |tmpdir|
+ expect do
+ described_class.merge('/', tmpdir )
+ end.to raise_error(StandardError, 'path / is not allowed')
+ end
+ end
+
+ it 'raises an error for invalid target path' do
+ Dir.mktmpdir do |tmpdir|
+ expect do
+ described_class.merge(tmpdir, "#{tmpdir}/../")
+ end.to raise_error(Gitlab::Utils::PathTraversalAttackError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index c0215ff5843..727ca4f630b 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::RepoRestorer do
- include GitHelpers
-
let_it_be(:project_with_repo) do
create(:project, :repository, :wiki_repo, name: 'test-repo-restorer', path: 'test-repo-restorer').tap do |p|
p.wiki.create_page('page', 'foobar', :markdown, 'created page')
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 23eb93a1bce..75d980cd5f4 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -336,7 +336,7 @@ Ci::PipelineMetadata:
- id
- project_id
- pipeline_id
-- title
+- name
Ci::Stage:
- id
- name
@@ -568,6 +568,7 @@ Project:
- suggestion_commit_message
- merge_commit_template
- squash_commit_template
+- issue_branch_template
Author:
- name
ProjectFeature:
@@ -592,6 +593,7 @@ ProjectFeature:
- feature_flags_access_level
- releases_access_level
- monitor_access_level
+- infrastructure_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
diff --git a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
index e529d36fd11..ebb0d62afa0 100644
--- a/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::SnippetsRepoRestorer do
- include GitHelpers
-
describe 'bundle a snippet Git repo' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
diff --git a/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb b/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb
index 7afb80488d8..cb4fdeed1a1 100644
--- a/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb
+++ b/spec/lib/gitlab/inactive_projects_deletion_warning_tracker_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::InactiveProjectsDeletionWarningTracker, :freeze_time do
end
it 'returns the list of projects for which deletion warning email has been sent' do
- expected_hash = { "project:1" => "#{Date.current}" }
+ expected_hash = { "project:1" => Date.current.to_s }
expect(Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects).to eq(expected_hash)
end
@@ -57,7 +57,7 @@ RSpec.describe Gitlab::InactiveProjectsDeletionWarningTracker, :freeze_time do
end
it 'returns the date if a deletion warning email has been sent for a given project' do
- expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notification_date).to eq("#{Date.current}")
+ expect(Gitlab::InactiveProjectsDeletionWarningTracker.new(project_id).notification_date).to eq(Date.current.to_s)
end
it 'returns nil if a deletion warning email has not been sent for a given project' do
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 1545de6d8fd..acd6634058f 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -1,87 +1,17 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::IncomingEmail do
- describe "self.enabled?" do
- context "when reply by email is enabled" do
- before do
- stub_incoming_email_setting(enabled: true)
- end
-
- it 'returns true' do
- expect(described_class.enabled?).to be(true)
- end
- end
-
- context "when reply by email is disabled" do
- before do
- stub_incoming_email_setting(enabled: false)
- end
+ let(:setting_name) { :incoming_email }
- it "returns false" do
- expect(described_class.enabled?).to be(false)
- end
- end
- end
+ it_behaves_like 'common email methods'
- describe 'self.supports_wildcard?' do
- context 'address contains the wildcard placeholder' do
- before do
- stub_incoming_email_setting(address: 'replies+%{key}@example.com')
- end
-
- it 'confirms that wildcard is supported' do
- expect(described_class.supports_wildcard?).to be(true)
- end
- end
-
- context "address doesn't contain the wildcard placeholder" do
- before do
- stub_incoming_email_setting(address: 'replies@example.com')
- end
-
- it 'returns that wildcard is not supported' do
- expect(described_class.supports_wildcard?).to be(false)
- end
- end
-
- context 'address is not set' do
- before do
- stub_incoming_email_setting(address: nil)
- end
-
- it 'returns that wildcard is not supported' do
- expect(described_class.supports_wildcard?).to be(false)
- end
- end
- end
-
- context 'self.unsubscribe_address' do
+ describe 'self.key_from_address' do
before do
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
end
- it 'returns the address with interpolated reply key and unsubscribe suffix' do
- expect(described_class.unsubscribe_address('key')).to eq("replies+key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX}@example.com")
- end
- end
-
- context "self.reply_address" do
- before do
- stub_incoming_email_setting(address: "replies+%{key}@example.com")
- end
-
- it "returns the address with an interpolated reply key" do
- expect(described_class.reply_address("key")).to eq("replies+key@example.com")
- end
- end
-
- context "self.key_from_address" do
- before do
- stub_incoming_email_setting(address: "replies+%{key}@example.com")
- end
-
it "returns reply key" do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end
@@ -101,25 +31,4 @@ RSpec.describe Gitlab::IncomingEmail do
end
end
end
-
- context 'self.key_from_fallback_message_id' do
- it 'returns reply key' do
- expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
- end
- end
-
- context 'self.scan_fallback_references' do
- let(:references) do
- '<issue_1@localhost>' \
- ' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' \
- ',<exchange@microsoft.com>'
- end
-
- it 'returns reply key' do
- expect(described_class.scan_fallback_references(references))
- .to eq(%w[issue_1@localhost
- reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost
- exchange@microsoft.com])
- end
- end
end
diff --git a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
index e4af3f77d5d..58c75bff9dd 100644
--- a/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_cluster_validator_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
it do
stub_rails_env(env)
- args = [:mget, 'foo', 'bar']
+ args = [[:mget, 'foo', 'bar']]
if should_raise
expect { described_class.validate!(args) }
@@ -58,7 +58,7 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
with_them do
it do
- args = [command] + arguments
+ args = [[command] + arguments]
if should_raise
expect { described_class.validate!(args) }
@@ -68,13 +68,32 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
end
end
end
+
+ where(:arguments, :should_raise) do
+ [[:get, "foo"], [:get, "bar"]] | true
+ [[:get, "foo"], [:mget, "foo", "bar"]] | true # mix of single-key and multi-key cmds
+ [[:get, "{foo}:name"], [:get, "{foo}:profile"]] | false
+ [[:del, "foo"], [:del, "bar"]] | true
+ [] | false # pipeline or transaction opened and closed without ops
+ end
+
+ with_them do
+ it do
+ if should_raise
+ expect { described_class.validate!(arguments) }
+ .to raise_error(described_class::CrossSlotError)
+ else
+ expect { described_class.validate!(arguments) }.not_to raise_error
+ end
+ end
+ end
end
describe '.allow_cross_slot_commands' do
it 'does not raise for invalid arguments' do
expect do
described_class.allow_cross_slot_commands do
- described_class.validate!([:mget, 'foo', 'bar'])
+ described_class.validate!([[:mget, 'foo', 'bar']])
end
end.not_to raise_error
end
@@ -83,10 +102,10 @@ RSpec.describe Gitlab::Instrumentation::RedisClusterValidator do
expect do
described_class.allow_cross_slot_commands do
described_class.allow_cross_slot_commands do
- described_class.validate!([:mget, 'foo', 'bar'])
+ described_class.validate!([[:mget, 'foo', 'bar']])
end
- described_class.validate!([:mget, 'foo', 'bar'])
+ described_class.validate!([[:mget, 'foo', 'bar']])
end
end.not_to raise_error
end
diff --git a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
index 5b5516f100b..02c5dfb7521 100644
--- a/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
+++ b/spec/lib/gitlab/instrumentation/redis_interceptor_spec.rb
@@ -57,8 +57,8 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do |pipeline|
- pipeline.call(:get, 'foobar')
- pipeline.call(:get, 'foobarbaz')
+ pipeline.call(:get, '{foobar}buz')
+ pipeline.call(:get, '{foobar}baz')
end
end
end
@@ -103,11 +103,22 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do |pipeline|
- pipeline.call(:get, 'foobar')
- pipeline.call(:get, 'foobarbaz')
+ pipeline.call(:get, '{foobar}:buz')
+ pipeline.call(:get, '{foobar}baz')
end
end
end
+
+ it 'raises error when keys are not from the same slot' do
+ expect do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.pipelined do |pipeline|
+ pipeline.call(:get, 'foo')
+ pipeline.call(:get, 'bar')
+ end
+ end
+ end.to raise_error(instance_of(Gitlab::Instrumentation::RedisClusterValidator::CrossSlotError))
+ end
end
end
diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb
index 23f7191454a..801de357ddc 100644
--- a/spec/lib/gitlab/json_logger_spec.rb
+++ b/spec/lib/gitlab/json_logger_spec.rb
@@ -7,6 +7,26 @@ RSpec.describe Gitlab::JsonLogger do
let(:now) { Time.now }
+ describe '#file_name' do
+ let(:subclass) do
+ Class.new(Gitlab::JsonLogger) do
+ def self.file_name_noext
+ 'testlogger'
+ end
+ end
+ end
+
+ it 'raises error when file_name_noext not implemented' do
+ expect { described_class.file_name }.to raise_error(
+ 'JsonLogger implementations must provide file_name_noext implementation'
+ )
+ end
+
+ it 'returns log file name when file_name_noext is implemented' do
+ expect(subclass.file_name).to eq('testlogger.log')
+ end
+ end
+
describe '#format_message' do
before do
allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb
index 73276288765..cbfab7e8884 100644
--- a/spec/lib/gitlab/json_spec.rb
+++ b/spec/lib/gitlab/json_spec.rb
@@ -2,6 +2,10 @@
require "spec_helper"
+# We can disable the cop that enforces the use of this class
+# as we need to test around it.
+#
+# rubocop: disable Gitlab/Json
RSpec.describe Gitlab::Json do
before do
stub_feature_flags(json_wrapper_legacy_mode: true)
@@ -429,4 +433,56 @@ RSpec.describe Gitlab::Json do
end
end
end
+
+ describe Gitlab::Json::RailsEncoder do
+ let(:obj) do
+ { foo: "<span>bar</span>" }
+ end
+
+ it "is used by ActiveSupport::JSON" do
+ expect_next_instance_of(described_class) do |encoder|
+ expect(encoder).to receive(:encode).with(obj)
+ end
+
+ ActiveSupport::JSON.encode(obj)
+ end
+
+ it "is used by .to_json calls" do
+ expect_next_instance_of(described_class) do |encoder|
+ expect(encoder).to receive(:encode).with(obj)
+ end
+
+ obj.to_json
+ end
+
+ it "is consistent with the original JSON implementation" do
+ default_encoder = ActiveSupport::JSON::Encoding::JSONGemEncoder
+
+ original_result = ActiveSupport::JSON::Encoding.use_encoder(default_encoder) do
+ ActiveSupport::JSON.encode(obj)
+ end
+
+ new_result = ActiveSupport::JSON::Encoding.use_encoder(described_class) do
+ ActiveSupport::JSON.encode(obj)
+ end
+
+ expect(new_result).to eq(original_result)
+ end
+
+ it "behaves the same when processing invalid unicode data" do
+ invalid_obj = { test: "Gr\x80\x81e" }
+ default_encoder = ActiveSupport::JSON::Encoding::JSONGemEncoder
+
+ original_result = ActiveSupport::JSON::Encoding.use_encoder(default_encoder) do
+ expect { ActiveSupport::JSON.encode(invalid_obj) }.to raise_error(JSON::GeneratorError)
+ end
+
+ new_result = ActiveSupport::JSON::Encoding.use_encoder(described_class) do
+ expect { ActiveSupport::JSON.encode(invalid_obj) }.to raise_error(JSON::GeneratorError)
+ end
+
+ expect(new_result).to eq(original_result)
+ end
+ end
end
+# rubocop: enable Gitlab/Json
diff --git a/spec/lib/gitlab/kas_spec.rb b/spec/lib/gitlab/kas_spec.rb
index 0fbb5f31210..34eb48a3221 100644
--- a/spec/lib/gitlab/kas_spec.rb
+++ b/spec/lib/gitlab/kas_spec.rb
@@ -125,6 +125,18 @@ RSpec.describe Gitlab::Kas do
end
end
+ describe '.version_info' do
+ let(:version) { '15.6.0-rc1' }
+
+ before do
+ allow(described_class).to receive(:version).and_return(version)
+ end
+
+ it 'returns gitlab_kas version config, including suffix' do
+ expect(described_class.version_info.to_s).to eq(version)
+ end
+ end
+
describe '.ensure_secret!' do
context 'secret file exists' do
before do
diff --git a/spec/lib/gitlab/kroki_spec.rb b/spec/lib/gitlab/kroki_spec.rb
index 7d29d018ff1..3d6ecf20377 100644
--- a/spec/lib/gitlab/kroki_spec.rb
+++ b/spec/lib/gitlab/kroki_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe Gitlab::Kroki do
describe '.formats' do
def default_formats
- %w[bytefield c4plantuml ditaa erd graphviz nomnoml pikchr plantuml svgbob umlet vega vegalite wavedrom].freeze
+ %w[bytefield c4plantuml ditaa erd graphviz nomnoml pikchr plantuml
+ structurizr svgbob umlet vega vegalite wavedrom].freeze
end
subject { described_class.formats(Gitlab::CurrentSettings) }
diff --git a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
index 892a4b06ad0..38a39f6a33a 100644
--- a/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog/configuration_spec.rb
@@ -78,36 +78,53 @@ RSpec.describe Gitlab::Memory::Watchdog::Configuration do
end
end
- context 'when two monitors are configured to be used' do
- before do
- configuration.monitors.use monitor_class_1, false, { message: 'monitor_1_text' }, max_strikes: 5
- configuration.monitors.use monitor_class_2, true, { message: 'monitor_2_text' }, max_strikes: 0
+ context 'when two different monitor class are configured' do
+ shared_examples 'executes monitors and returns correct results' do
+ it 'calls each monitor and returns correct results', :aggregate_failures do
+ payloads = []
+ thresholds = []
+ strikes = []
+ monitor_names = []
+
+ configuration.monitors.call_each do |result|
+ payloads << result.payload
+ thresholds << result.threshold_violated?
+ strikes << result.strikes_exceeded?
+ monitor_names << result.monitor_name
+ end
+
+ expect(payloads).to eq([payload1, payload2])
+ expect(thresholds).to eq([false, true])
+ expect(strikes).to eq([false, true])
+ expect(monitor_names).to eq([:monitor1, :monitor2])
+ end
+ end
+
+ context 'when monitors are configured inline' do
+ before do
+ configuration.monitors.push monitor_class_1, false, { message: 'monitor_1_text' }, max_strikes: 5
+ configuration.monitors.push monitor_class_2, true, { message: 'monitor_2_text' }, max_strikes: 0
+ end
+
+ include_examples 'executes monitors and returns correct results'
end
- it 'calls each monitor and returns correct results', :aggregate_failures do
- payloads = []
- thresholds = []
- strikes = []
- monitor_names = []
-
- configuration.monitors.call_each do |result|
- payloads << result.payload
- thresholds << result.threshold_violated?
- strikes << result.strikes_exceeded?
- monitor_names << result.monitor_name
+ context 'when monitors are configured in a block' do
+ before do
+ configuration.monitors do |stack|
+ stack.push monitor_class_1, false, { message: 'monitor_1_text' }, max_strikes: 5
+ stack.push monitor_class_2, true, { message: 'monitor_2_text' }, max_strikes: 0
+ end
end
- expect(payloads).to eq([payload1, payload2])
- expect(thresholds).to eq([false, true])
- expect(strikes).to eq([false, true])
- expect(monitor_names).to eq([:monitor1, :monitor2])
+ include_examples 'executes monitors and returns correct results'
end
end
- context 'when same monitor class is configured to be used twice' do
+ context 'when same monitor class is configured twice' do
before do
- configuration.monitors.use monitor_class_1, max_strikes: 1
- configuration.monitors.use monitor_class_1, max_strikes: 1
+ configuration.monitors.push monitor_class_1, max_strikes: 1
+ configuration.monitors.push monitor_class_1, max_strikes: 1
end
it 'calls same monitor only once' do
diff --git a/spec/lib/gitlab/memory/watchdog/configurator_spec.rb b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
new file mode 100644
index 00000000000..e6f2d57e9e6
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog/configurator_spec.rb
@@ -0,0 +1,199 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'prometheus/client'
+require 'sidekiq'
+require_dependency 'gitlab/cluster/lifecycle_events'
+
+RSpec.describe Gitlab::Memory::Watchdog::Configurator do
+ shared_examples 'as configurator' do |handler_class, sleep_time_env, sleep_time|
+ it 'configures the correct handler' do
+ configurator.call(configuration)
+
+ expect(configuration.handler).to be_an_instance_of(handler_class)
+ end
+
+ it 'configures the correct logger' do
+ configurator.call(configuration)
+
+ expect(configuration.logger).to eq(logger)
+ end
+
+ context 'when sleep_time_seconds is not passed through the environment' do
+ let(:sleep_time_seconds) { sleep_time }
+
+ it 'configures the correct sleep time' do
+ configurator.call(configuration)
+
+ expect(configuration.sleep_time_seconds).to eq(sleep_time_seconds)
+ end
+ end
+
+ context 'when sleep_time_seconds is passed through the environment' do
+ let(:sleep_time_seconds) { sleep_time - 1 }
+
+ before do
+ stub_env(sleep_time_env, sleep_time - 1)
+ end
+
+ it 'configures the correct sleep time' do
+ configurator.call(configuration)
+
+ expect(configuration.sleep_time_seconds).to eq(sleep_time_seconds)
+ end
+ end
+ end
+
+ shared_examples 'as monitor configurator' do
+ it 'executes monitors and returns correct results' do
+ configurator.call(configuration)
+
+ payloads = {}
+ configuration.monitors.call_each do |result|
+ payloads[result.monitor_name] = result.payload
+ end
+
+ expect(payloads).to eq(expected_payloads)
+ end
+ end
+
+ let(:configuration) { Gitlab::Memory::Watchdog::Configuration.new }
+
+ # In tests, the Puma constant does not exist so we cannot use a verified double.
+ # rubocop: disable RSpec/VerifiedDoubles
+ describe '.configure_for_puma' do
+ let(:logger) { Gitlab::AppLogger }
+ let(:puma) do
+ Class.new do
+ def self.cli_config
+ Struct.new(:options).new
+ end
+ end
+ end
+
+ subject(:configurator) { described_class.configure_for_puma }
+
+ def stub_prometheus_metrics
+ gauge = instance_double(::Prometheus::Client::Gauge)
+ allow(Gitlab::Metrics).to receive(:gauge).and_return(gauge)
+ allow(gauge).to receive(:set)
+ end
+
+ before do
+ stub_const('Puma', puma)
+ stub_const('Puma::Cluster::WorkerHandle', double.as_null_object)
+ stub_prometheus_metrics
+ end
+
+ it_behaves_like 'as configurator',
+ Gitlab::Memory::Watchdog::PumaHandler,
+ 'GITLAB_MEMWD_SLEEP_TIME_SEC',
+ 60
+
+ context 'with DISABLE_PUMA_WORKER_KILLER set to true' do
+ let(:primary_memory) { 2048 }
+ let(:worker_memory) { max_mem_growth * primary_memory + 1 }
+ let(:expected_payloads) do
+ {
+ heap_fragmentation: {
+ message: 'heap fragmentation limit exceeded',
+ memwd_cur_heap_frag: max_heap_fragmentation + 0.1,
+ memwd_max_heap_frag: max_heap_fragmentation,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: 1
+
+ },
+ unique_memory_growth: {
+ message: 'memory limit exceeded',
+ memwd_uss_bytes: worker_memory,
+ memwd_ref_uss_bytes: primary_memory,
+ memwd_max_uss_bytes: max_mem_growth * primary_memory,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: 1
+ }
+ }
+ end
+
+ before do
+ stub_env('DISABLE_PUMA_WORKER_KILLER', true)
+ allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(max_heap_fragmentation + 0.1)
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return({ uss: worker_memory })
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).with(
+ pid: Gitlab::Cluster::PRIMARY_PID
+ ).and_return({ uss: primary_memory })
+ end
+
+ context 'when settings are set via environment variables' do
+ let(:max_heap_fragmentation) { 0.4 }
+ let(:max_mem_growth) { 4.0 }
+ let(:max_strikes) { 4 }
+
+ before do
+ stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 0.4)
+ stub_env('GITLAB_MEMWD_MAX_MEM_GROWTH', 4.0)
+ stub_env('GITLAB_MEMWD_MAX_STRIKES', 4)
+ end
+
+ it_behaves_like 'as monitor configurator'
+ end
+
+ context 'when settings are not set via environment variables' do
+ let(:max_heap_fragmentation) { 0.5 }
+ let(:max_mem_growth) { 3.0 }
+ let(:max_strikes) { 5 }
+
+ it_behaves_like 'as monitor configurator'
+ end
+ end
+
+ context 'with DISABLE_PUMA_WORKER_KILLER set to false' do
+ let(:expected_payloads) do
+ {
+ rss_memory_limit: {
+ message: 'rss memory limit exceeded',
+ memwd_rss_bytes: memory_limit + 1,
+ memwd_max_rss_bytes: memory_limit,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: 1
+ }
+ }
+ end
+
+ before do
+ stub_env('DISABLE_PUMA_WORKER_KILLER', false)
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: memory_limit + 1 })
+ end
+
+ context 'when settings are set via environment variables' do
+ let(:memory_limit) { 1300 }
+ let(:max_strikes) { 4 }
+
+ before do
+ stub_env('PUMA_WORKER_MAX_MEMORY', 1300)
+ stub_env('GITLAB_MEMWD_MAX_STRIKES', 4)
+ end
+
+ it_behaves_like 'as monitor configurator'
+ end
+
+ context 'when settings are not set via environment variables' do
+ let(:memory_limit) { 1200 }
+ let(:max_strikes) { 5 }
+
+ it_behaves_like 'as monitor configurator'
+ end
+ end
+ end
+ # rubocop: enable RSpec/VerifiedDoubles
+
+ describe '.configure_for_sidekiq' do
+ let(:logger) { ::Sidekiq.logger }
+
+ subject(:configurator) { described_class.configure_for_sidekiq }
+
+ it_behaves_like 'as configurator',
+ Gitlab::Memory::Watchdog::TermProcessHandler,
+ 'SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL',
+ 3
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
new file mode 100644
index 00000000000..9e25cfda782
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog/monitor/rss_memory_limit_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'support/shared_examples/lib/gitlab/memory/watchdog/monitor_result_shared_examples'
+
+RSpec.describe Gitlab::Memory::Watchdog::Monitor::RssMemoryLimit do
+ let(:memory_limit) { 2048 }
+ let(:worker_memory) { 1024 }
+
+ subject(:monitor) do
+ described_class.new(memory_limit: memory_limit)
+ end
+
+ before do
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: worker_memory })
+ end
+
+ describe '#call' do
+ context 'when process exceeds threshold' do
+ let(:worker_memory) { memory_limit + 1 }
+ let(:payload) do
+ {
+ message: 'rss memory limit exceeded',
+ memwd_rss_bytes: worker_memory,
+ memwd_max_rss_bytes: memory_limit
+ }
+ end
+
+ include_examples 'returns Watchdog Monitor result', threshold_violated: true
+ end
+
+ context 'when process does not exceed threshold' do
+ let(:worker_memory) { memory_limit - 1 }
+ let(:payload) { {} }
+
+ include_examples 'returns Watchdog Monitor result', threshold_violated: false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb
index 84e9a577afb..5d9599d6eab 100644
--- a/spec/lib/gitlab/memory/watchdog_spec.rb
+++ b/spec/lib/gitlab/memory/watchdog_spec.rb
@@ -60,14 +60,16 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
describe '#call' do
before do
stub_prometheus_metrics
- allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
+ allow(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(
+ total: 1024
+ )
allow(::Prometheus::PidProvider).to receive(:worker_id).and_return('worker_1')
watchdog.configure do |config|
config.handler = handler
config.logger = logger
config.sleep_time_seconds = sleep_time_seconds
- config.monitors.use monitor_class, threshold_violated, payload, max_strikes: max_strikes
+ config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
end
allow(handler).to receive(:call).and_return(true)
@@ -203,8 +205,8 @@ RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures do
config.handler = handler
config.logger = logger
config.sleep_time_seconds = sleep_time_seconds
- config.monitors.use monitor_class, threshold_violated, payload, max_strikes: max_strikes
- config.monitors.use monitor_class, threshold_violated, payload, max_strikes: max_strikes
+ config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
+ config.monitors.push monitor_class, threshold_violated, payload, max_strikes: max_strikes
end
end
diff --git a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
index 50cfa6b64ea..4f437e57600 100644
--- a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
+++ b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb
@@ -70,8 +70,8 @@ RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do
let(:payload) { { test: 'test' } }
let(:hash) do
{
- 'status' => status,
- 'payload' => payload
+ status: status,
+ payload: payload
}
end
diff --git a/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb
index 2471faf76b2..787ac2874d3 100644
--- a/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb
+++ b/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Gitlab::MergeRequests::Mergeability::RedisInterface, :clean_gitla
subject(:redis_interface) { described_class.new }
let(:merge_check) { double(cache_key: '13') }
- let(:result_hash) { { 'test' => 'test' } }
+ let(:result_hash) { { test: 'test' } }
let(:expected_key) { "#{merge_check.cache_key}:#{described_class::VERSION}" }
describe '#save_check' do
diff --git a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb
index 0e8b598730c..e4211c6dfd7 100644
--- a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb
+++ b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb
@@ -10,15 +10,15 @@ RSpec.describe Gitlab::MergeRequests::Mergeability::ResultsStore do
let(:merge_request) { double }
describe '#read' do
- let(:result_hash) { { 'status' => 'success', 'payload' => {} } }
+ let(:result_hash) { { status: 'success', payload: {} } }
it 'calls #retrieve_check on the interface' do
expect(interface).to receive(:retrieve_check).with(merge_check: merge_check).and_return(result_hash)
cached_result = results_store.read(merge_check: merge_check)
- expect(cached_result.status).to eq(result_hash['status'].to_sym)
- expect(cached_result.payload).to eq(result_hash['payload'])
+ expect(cached_result.status).to eq(result_hash[:status].to_sym)
+ expect(cached_result.payload).to eq(result_hash[:payload])
end
context 'when #retrieve_check returns nil' do
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
index 730a31346d7..f922eff2980 100644
--- a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
it_behaves_like 'valid dashboard service response'
end
- context 'when the self monitoring dashboard is specified' do
+ context 'when the self-monitoring dashboard is specified' do
let(:dashboard_path) { self_monitoring_dashboard_path }
it_behaves_like 'valid dashboard service response'
@@ -181,7 +181,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
end
end
- context 'when the project is self monitoring' do
+ context 'when the project is self-monitoring' do
let(:self_monitoring_dashboard) do
{
path: self_monitoring_dashboard_path,
@@ -199,7 +199,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store
stub_application_setting(self_monitoring_project_id: project.id)
end
- it 'includes self monitoring and project dashboards' do
+ it 'includes self-monitoring and project dashboards' do
project_dashboard = {
path: dashboard_path,
display_name: 'test.yml',
diff --git a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb b/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
index f3c8209e0b6..b41b51f53c3 100644
--- a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::ServiceSelector do
end
end
- context 'when the path is for the self monitoring dashboard' do
+ context 'when the path is for the self-monitoring dashboard' do
let(:arguments) { { dashboard_path: self_monitoring_dashboard_path } }
it { is_expected.to be Metrics::Dashboard::SelfMonitoringDashboardService }
diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
index 830d43169a9..d49200f87cc 100644
--- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe Gitlab::Metrics::Dashboard::Url do
'url' => url,
'namespace' => 'namespace1',
'project' => 'project1',
- 'environment' => "#{environment_id}",
+ 'environment' => environment_id.to_s,
'query' => "?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&environment=#{environment_id}&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z",
'anchor' => '#title'
}
diff --git a/spec/lib/gitlab/metrics/global_search_slis_spec.rb b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
index 0c09cf6dd71..c10d83664ea 100644
--- a/spec/lib/gitlab/metrics/global_search_slis_spec.rb
+++ b/spec/lib/gitlab/metrics/global_search_slis_spec.rb
@@ -47,10 +47,10 @@ RSpec.describe Gitlab::Metrics::GlobalSearchSlis do
describe '#record_apdex' do
where(:search_type, :code_search, :duration_target) do
- 'basic' | false | 7.031
- 'basic' | true | 21.903
- 'advanced' | false | 4.865
- 'advanced' | true | 13.546
+ 'basic' | false | 8.812
+ 'basic' | true | 27.538
+ 'advanced' | false | 2.452
+ 'advanced' | true | 15.52
end
with_them do
diff --git a/spec/lib/gitlab/metrics/loose_foreign_keys_slis_spec.rb b/spec/lib/gitlab/metrics/loose_foreign_keys_slis_spec.rb
new file mode 100644
index 00000000000..58740278425
--- /dev/null
+++ b/spec/lib/gitlab/metrics/loose_foreign_keys_slis_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Gitlab::Metrics::LooseForeignKeysSlis do
+ # This needs to be dynamic because db_config_names depends on
+ # config/database.yml and the specs need to work for all configurations. That
+ # means this assertion is a copy of the implementation.
+ let(:possible_labels) do
+ ::Gitlab::Database.db_config_names.map do |db_config_name|
+ {
+ db_config_name: db_config_name,
+ feature_category: :database
+ }
+ end
+ end
+
+ describe '#initialize_slis!' do
+ it 'initializes Apdex and ErrorRate SLIs for loose_foreign_key_clean_ups' do
+ expect(::Gitlab::Metrics::Sli::Apdex).to receive(:initialize_sli).with(
+ :loose_foreign_key_clean_ups,
+ possible_labels
+ )
+
+ expect(::Gitlab::Metrics::Sli::ErrorRate).to receive(:initialize_sli).with(
+ :loose_foreign_key_clean_ups,
+ possible_labels
+ )
+
+ described_class.initialize_slis!
+ end
+ end
+
+ describe '#record_apdex' do
+ context 'with success: true' do
+ it 'increments the loose_foreign_key_clean_ups Apdex as a success' do
+ expect(Gitlab::Metrics::Sli::Apdex[:loose_foreign_key_clean_ups]).to receive(:increment).with(
+ labels: { feature_category: :database, db_config_name: 'main' },
+ success: true
+ )
+
+ described_class.record_apdex(success: true, db_config_name: 'main')
+ end
+ end
+
+ context 'with success: false' do
+ it 'increments the loose_foreign_key_clean_ups Apdex as not a success' do
+ expect(Gitlab::Metrics::Sli::Apdex[:loose_foreign_key_clean_ups]).to receive(:increment).with(
+ labels: { feature_category: :database, db_config_name: 'main' },
+ success: false
+ )
+
+ described_class.record_apdex(success: false, db_config_name: 'main')
+ end
+ end
+ end
+
+ describe '#record_error_rate' do
+ context 'with error: true' do
+ it 'increments the loose_foreign_key_clean_ups ErrorRate as an error' do
+ expect(Gitlab::Metrics::Sli::ErrorRate[:loose_foreign_key_clean_ups]).to receive(:increment).with(
+ labels: { feature_category: :database, db_config_name: 'main' },
+ error: true
+ )
+
+ described_class.record_error_rate(error: true, db_config_name: 'main')
+ end
+ end
+
+ context 'with error: false' do
+ it 'increments the loose_foreign_key_clean_ups ErrorRate as not an error' do
+ expect(Gitlab::Metrics::Sli::ErrorRate[:loose_foreign_key_clean_ups]).to receive(:increment).with(
+ labels: { feature_category: :database, db_config_name: 'main' },
+ error: false
+ )
+
+ described_class.record_error_rate(error: false, db_config_name: 'main')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 6aa89c7cb05..091f35bfbcc 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -24,47 +24,22 @@ RSpec.describe Gitlab::Metrics::MethodCall do
allow(method_call).to receive(:above_threshold?).and_return(true)
end
- context 'prometheus instrumentation is enabled' do
- before do
- stub_feature_flags(prometheus_metrics_method_instrumentation: true)
- end
-
- around do |example|
- freeze_time do
- example.run
- end
- end
-
- it 'metric is not a NullMetric' do
- method_call.measure { 'foo' }
- expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).not_to be_instance_of(Gitlab::Metrics::NullMetric)
- end
-
- it 'observes the performance of the supplied block' do
- expect(transaction)
- .to receive(:observe).with(:gitlab_method_call_duration_seconds, be_a_kind_of(Numeric), { method: "#bar", module: :Foo })
-
- method_call.measure { 'foo' }
+ around do |example|
+ freeze_time do
+ example.run
end
end
- context 'prometheus instrumentation is disabled' do
- before do
- stub_feature_flags(prometheus_metrics_method_instrumentation: false)
- end
-
- it 'observes the performance of the supplied block' do
- expect(transaction)
- .to receive(:observe).with(:gitlab_method_call_duration_seconds, be_a_kind_of(Numeric), { method: "#bar", module: :Foo })
-
- method_call.measure { 'foo' }
- end
+ it 'metric is not a NullMetric' do
+ method_call.measure { 'foo' }
+ expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).not_to be_instance_of(Gitlab::Metrics::NullMetric)
+ end
- it 'observes using NullMetric' do
- method_call.measure { 'foo' }
+ it 'observes the performance of the supplied block' do
+ expect(transaction)
+ .to receive(:observe).with(:gitlab_method_call_duration_seconds, be_a_kind_of(Numeric), { method: "#bar", module: :Foo })
- expect(::Gitlab::Metrics::WebTransaction.prometheus_metric(:gitlab_method_call_duration_seconds, :histogram)).to be_instance_of(Gitlab::Metrics::NullMetric)
- end
+ method_call.measure { 'foo' }
end
end
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index b1566ffa7b4..8c46c881ef0 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -35,14 +35,30 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do
end
describe '#sample' do
- it 'adds a metric containing the process resident memory bytes' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return(9000)
+ it 'adds a metric containing the process total resident memory bytes' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ total: 9000 })
expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
sampler.sample
end
+ it 'adds a metric containing the process anonymous resident memory bytes' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ anon: 9000 })
+
+ expect(sampler.metrics[:process_resident_anon_memory_bytes]).to receive(:set).with({}, 9000)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process file backed resident memory bytes' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return({ file: 9000 })
+
+ expect(sampler.metrics[:process_resident_file_memory_bytes]).to receive(:set).with({}, 9000)
+
+ sampler.sample
+ end
+
it 'adds a metric containing the process unique and proportional memory bytes' do
expect(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return(uss: 9000, pss: 10_000)
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index b86469eacd1..e4f53ab3f49 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe Gitlab::Metrics::System do
VmHWM: 2468 kB
VmRSS: 2468 kB
RssAnon: 260 kB
+ RssFile: 1024 kB
SNIP
end
@@ -132,18 +133,26 @@ RSpec.describe Gitlab::Metrics::System do
describe '.memory_usage_rss' do
context 'without PID' do
- it "returns the current process' resident set size (RSS) in bytes" do
+ it "returns a hash containing RSS metrics in bytes for current process" do
mock_existing_proc_file('/proc/self/status', proc_status)
- expect(described_class.memory_usage_rss).to eq(2527232)
+ expect(described_class.memory_usage_rss).to eq(
+ total: 2527232,
+ anon: 266240,
+ file: 1048576
+ )
end
end
context 'with PID' do
- it "returns the given process' resident set size (RSS) in bytes" do
+ it "returns a hash containing RSS metrics in bytes for given process" do
mock_existing_proc_file('/proc/7/status', proc_status)
- expect(described_class.memory_usage_rss(pid: 7)).to eq(2527232)
+ expect(described_class.memory_usage_rss(pid: 7)).to eq(
+ total: 2527232,
+ anon: 266240,
+ file: 1048576
+ )
end
end
end
@@ -241,8 +250,12 @@ RSpec.describe Gitlab::Metrics::System do
end
describe '.memory_usage_rss' do
- it 'returns 0' do
- expect(described_class.memory_usage_rss).to eq(0)
+ it 'returns 0 for all components' do
+ expect(described_class.memory_usage_rss).to eq(
+ total: 0,
+ anon: 0,
+ file: 0
+ )
end
end
diff --git a/spec/lib/gitlab/observability_spec.rb b/spec/lib/gitlab/observability_spec.rb
new file mode 100644
index 00000000000..2b1d22d9019
--- /dev/null
+++ b/spec/lib/gitlab/observability_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Gitlab::Observability do
+ describe '.observability_url' do
+ let(:gitlab_url) { 'https://example.com' }
+
+ subject { described_class.observability_url }
+
+ before do
+ stub_config_setting(url: gitlab_url)
+ end
+
+ it { is_expected.to eq('https://observe.gitlab.com') }
+
+ context 'when on staging.gitlab.com' do
+ let(:gitlab_url) { Gitlab::Saas.staging_com_url }
+
+ it { is_expected.to eq('https://observe.staging.gitlab.com') }
+ end
+
+ context 'when overriden via ENV' do
+ let(:observe_url) { 'https://example.net' }
+
+ before do
+ stub_env('OVERRIDE_OBSERVABILITY_URL', observe_url)
+ end
+
+ it { is_expected.to eq(observe_url) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/octokit/middleware_spec.rb b/spec/lib/gitlab/octokit/middleware_spec.rb
index 92e424978ff..7bce0788327 100644
--- a/spec/lib/gitlab/octokit/middleware_spec.rb
+++ b/spec/lib/gitlab/octokit/middleware_spec.rb
@@ -66,5 +66,13 @@ RSpec.describe Gitlab::Octokit::Middleware do
it_behaves_like 'Public URL'
end
end
+
+ context 'when a non HTTP/HTTPS URL is provided' do
+ let(:env) { { url: 'ssh://172.16.0.0' } }
+
+ it 'raises an error' do
+ expect { middleware.call(env) }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
index 0bafd436bd0..b5ed583b1f1 100644
--- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
+++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do
before do
allow(request_context).to receive(:request).and_return(fake_request)
- allow(finder).to receive(:is_a?).with(BranchesFinder) { true }
+ allow(BranchesFinder).to receive(:===).with(finder).and_return(true)
expect(finder).to receive(:execute).with(gitaly_pagination: true).and_return(branches)
end
diff --git a/spec/lib/gitlab/pagination_delegate_spec.rb b/spec/lib/gitlab/pagination_delegate_spec.rb
new file mode 100644
index 00000000000..7693decd881
--- /dev/null
+++ b/spec/lib/gitlab/pagination_delegate_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::PaginationDelegate do
+ context 'when there is no data' do
+ let(:delegate) do
+ described_class.new(page: 1,
+ per_page: 10,
+ count: 0)
+ end
+
+ it 'shows the correct total count' do
+ expect(delegate.total_count).to eq(0)
+ end
+
+ it 'shows the correct total pages' do
+ expect(delegate.total_pages).to eq(0)
+ end
+
+ it 'shows the correct next page' do
+ expect(delegate.next_page).to be_nil
+ end
+
+ it 'shows the correct previous page' do
+ expect(delegate.prev_page).to be_nil
+ end
+
+ it 'shows the correct current page' do
+ expect(delegate.current_page).to eq(1)
+ end
+
+ it 'shows the correct limit value' do
+ expect(delegate.limit_value).to eq(10)
+ end
+
+ it 'shows the correct first page' do
+ expect(delegate.first_page?).to be true
+ end
+
+ it 'shows the correct last page' do
+ expect(delegate.last_page?).to be true
+ end
+
+ it 'shows the correct offset' do
+ expect(delegate.offset).to eq(0)
+ end
+ end
+
+ context 'with data' do
+ let(:delegate) do
+ described_class.new(page: 5,
+ per_page: 100,
+ count: 1000)
+ end
+
+ it 'shows the correct total count' do
+ expect(delegate.total_count).to eq(1000)
+ end
+
+ it 'shows the correct total pages' do
+ expect(delegate.total_pages).to eq(10)
+ end
+
+ it 'shows the correct next page' do
+ expect(delegate.next_page).to eq(6)
+ end
+
+ it 'shows the correct previous page' do
+ expect(delegate.prev_page).to eq(4)
+ end
+
+ it 'shows the correct current page' do
+ expect(delegate.current_page).to eq(5)
+ end
+
+ it 'shows the correct limit value' do
+ expect(delegate.limit_value).to eq(100)
+ end
+
+ it 'shows the correct first page' do
+ expect(delegate.first_page?).to be false
+ end
+
+ it 'shows the correct last page' do
+ expect(delegate.last_page?).to be false
+ end
+
+ it 'shows the correct offset' do
+ expect(delegate.offset).to eq(400)
+ end
+ end
+
+ context 'for last page' do
+ let(:delegate) do
+ described_class.new(page: 10,
+ per_page: 100,
+ count: 1000)
+ end
+
+ it 'shows the correct total count' do
+ expect(delegate.total_count).to eq(1000)
+ end
+
+ it 'shows the correct total pages' do
+ expect(delegate.total_pages).to eq(10)
+ end
+
+ it 'shows the correct next page' do
+ expect(delegate.next_page).to be_nil
+ end
+
+ it 'shows the correct previous page' do
+ expect(delegate.prev_page).to eq(9)
+ end
+
+ it 'shows the correct current page' do
+ expect(delegate.current_page).to eq(10)
+ end
+
+ it 'shows the correct limit value' do
+ expect(delegate.limit_value).to eq(100)
+ end
+
+ it 'shows the correct first page' do
+ expect(delegate.first_page?).to be false
+ end
+
+ it 'shows the correct last page' do
+ expect(delegate.last_page?).to be true
+ end
+
+ it 'shows the correct offset' do
+ expect(delegate.offset).to eq(900)
+ end
+ end
+
+ context 'with limits and defaults' do
+ it 'has a maximum limit per page' do
+ expect(described_class.new(page: nil,
+ per_page: 1000,
+ count: 0).limit_value).to eq(described_class::MAX_PER_PAGE)
+ end
+
+ it 'has a default per page' do
+ expect(described_class.new(page: nil,
+ per_page: nil,
+ count: 0).limit_value).to eq(described_class::DEFAULT_PER_PAGE)
+ end
+
+ it 'has a maximum page' do
+ expect(described_class.new(page: 100,
+ per_page: 10,
+ count: 1).current_page).to eq(1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb
index 05cdc5bb79b..d42cef8bcba 100644
--- a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb
+++ b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do
it 'stores request id and enqueues stats job' do
expect_to_obtain_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid)
expect(GitlabPerformanceBarStatsWorker).to receive(:perform_in).with(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid)
- expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
+ expect(client).to receive(:sadd?).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
expect(client).to receive(:expire).with(GitlabPerformanceBarStatsWorker::STATS_KEY, GitlabPerformanceBarStatsWorker::STATS_KEY_EXPIRE)
peek_adapter.new(client).save('foo')
@@ -56,7 +56,7 @@ RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do
it 'stores request id but does not enqueue any job' do
expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in)
- expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
+ expect(client).to receive(:sadd?).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
peek_adapter.new(client).save('foo')
end
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 630369977ff..998fff12e94 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -12,6 +12,20 @@ RSpec.describe Gitlab::ProjectTemplate do
end
end
+ describe '#project_host' do
+ context "when `preview` is valid" do
+ subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').project_host }
+
+ it { is_expected.to eq 'https://gitlab.com' }
+ end
+
+ context "when `preview` is `nil`" do
+ subject { described_class.new('name', 'title', 'description', nil).project_host }
+
+ it { is_expected.to eq nil }
+ end
+ end
+
describe '#project_path' do
subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').project_path }
diff --git a/spec/lib/gitlab/qa_spec.rb b/spec/lib/gitlab/qa_spec.rb
new file mode 100644
index 00000000000..c26f4c89fec
--- /dev/null
+++ b/spec/lib/gitlab/qa_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Qa do
+ describe '.request?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:dot_com, :request_user_agent, :qa_user_agent, :result) do
+ false | 'qa_user_agent' | 'qa_user_agent' | false
+ true | nil | 'qa_user_agent' | false
+ true | '' | 'qa_user_agent' | false
+ true | 'qa_user_agent' | '' | false
+ true | 'qa_user_agent' | nil | false
+ true | 'qa_user_agent' | 'qa_user_agent' | true
+ end
+
+ with_them do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(dot_com)
+ stub_env('GITLAB_QA_USER_AGENT', qa_user_agent)
+ end
+
+ subject { described_class.request?(request_user_agent) }
+
+ it { is_expected.to eq(result) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb
index d8eb2040ccc..c11d0a7c18d 100644
--- a/spec/lib/gitlab/query_limiting/transaction_spec.rb
+++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb
@@ -91,6 +91,9 @@ RSpec.describe Gitlab::QueryLimiting::Transaction do
SELECT a.attname, a.other_column
FROM pg_attribute a
SQL
+ transaction.increment(
+ "SELECT a.attnum, a.attname\nFROM pg_attribute a\nWHERE a.attrelid = 10605202\nAND a.attnum IN (3)\n"
+ )
end.not_to change(transaction, :count)
end
end
diff --git a/spec/lib/gitlab/redis/multi_store_spec.rb b/spec/lib/gitlab/redis/multi_store_spec.rb
index 8b73b5e03c0..207fe28e84e 100644
--- a/spec/lib/gitlab/redis/multi_store_spec.rb
+++ b/spec/lib/gitlab/redis/multi_store_spec.rb
@@ -127,19 +127,15 @@ RSpec.describe Gitlab::Redis::MultiStore do
end
before(:all) do
- primary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
+ primary_store.set(key1, value1)
+ primary_store.set(key2, value2)
+ primary_store.sadd?(skey, value1)
+ primary_store.sadd?(skey, value2)
- secondary_store.multi do |multi|
- multi.set(key1, value1)
- multi.set(key2, value2)
- multi.sadd(skey, value1)
- multi.sadd(skey, value2)
- end
+ secondary_store.set(key1, value1)
+ secondary_store.set(key2, value2)
+ secondary_store.sadd?(skey, value1)
+ secondary_store.sadd?(skey, value2)
end
RSpec.shared_examples_for 'reads correct value' do
@@ -211,126 +207,86 @@ RSpec.describe Gitlab::Redis::MultiStore do
end
with_them do
- describe "#{name}" do
+ describe name.to_s do
before do
allow(primary_store).to receive(name).and_call_original
allow(secondary_store).to receive(name).and_call_original
end
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when reading from the primary is successful' do
- it 'returns the correct value' do
- expect(primary_store).to receive(name).with(*args).and_call_original
-
- subject
- end
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
+ context 'when reading from the primary is successful' do
+ it 'returns the correct value' do
+ expect(primary_store).to receive(name).with(*args).and_call_original
- subject
- end
-
- include_examples 'reads correct value'
+ subject
end
- context 'when reading from primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
+ it 'does not execute on the secondary store' do
+ expect(secondary_store).not_to receive(name)
- it 'logs the exception' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, instance_name: instance_name, command_name: name))
+ subject
+ end
- subject
- end
+ include_examples 'reads correct value'
+ end
- include_examples 'fallback read from the secondary store'
+ context 'when reading from primary instance is raising an exception' do
+ before do
+ allow(primary_store).to receive(name).with(*args).and_raise(StandardError)
+ allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
- context 'when reading from primary instance return no value' do
- before do
- allow(primary_store).to receive(name).and_return(nil)
- end
+ it 'logs the exception' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
+ hash_including(:multi_store_error_message, instance_name: instance_name, command_name: name))
- include_examples 'fallback read from the secondary store'
+ subject
end
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do |pipeline|
- pipeline.send(name, *args)
- end
- end
+ include_examples 'fallback read from the secondary store'
+ end
- it 'is executed only 1 time on primary and secondary instance' do
- expect(primary_store).to receive(:pipelined).and_call_original
- expect(secondary_store).to receive(:pipelined).and_call_original
+ context 'when reading from primary instance return no value' do
+ before do
+ allow(primary_store).to receive(name).and_return(nil)
+ end
- 2.times do
- expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*args).once.and_call_original
- end
- end
+ include_examples 'fallback read from the secondary store'
+ end
- subject
+ context 'when the command is executed within pipelined block' do
+ subject do
+ multi_store.pipelined do |pipeline|
+ pipeline.send(name, *args)
end
end
- if params[:block]
- subject do
- multi_store.send(name, *args, &block)
- end
-
- context 'when block is provided' do
- it 'yields to the block' do
- expect(primary_store).to receive(name).and_yield(value)
+ it 'is executed only 1 time on primary and secondary instance' do
+ expect(primary_store).to receive(:pipelined).and_call_original
+ expect(secondary_store).to receive(:pipelined).and_call_original
- subject
+ 2.times do
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(name).with(*args).once.and_call_original
end
-
- include_examples 'reads correct value'
end
- end
- end
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
+ subject
end
+ end
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it_behaves_like 'secondary store'
+ if params[:block]
+ subject do
+ multi_store.send(name, *args, &block)
end
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'execute on the primary instance' do
- expect(primary_store).to receive(name).with(*args).and_call_original
+ context 'when block is provided' do
+ it 'yields to the block' do
+ expect(primary_store).to receive(name).and_yield(value)
subject
end
include_examples 'reads correct value'
-
- it 'does not execute on the secondary store' do
- expect(secondary_store).not_to receive(name)
-
- subject
- end
end
end
@@ -372,8 +328,9 @@ RSpec.describe Gitlab::Redis::MultiStore do
let_it_be(:skey) { "redis:set:key" }
let_it_be(:svalues1) { [value2, value1] }
let_it_be(:svalues2) { [value1] }
- let_it_be(:skey_value1) { [skey, value1] }
- let_it_be(:skey_value2) { [skey, value2] }
+ let_it_be(:skey_value1) { [skey, [value1]] }
+ let_it_be(:skey_value2) { [skey, [value2]] }
+ let_it_be(:script) { %(redis.call("set", "#{key1}", "#{value1}")) }
where(:case_name, :name, :args, :expected_value, :verification_name, :verification_args) do
'execute :set command' | :set | ref(:key1_value1) | ref(:value1) | :get | ref(:key1)
@@ -383,25 +340,22 @@ RSpec.describe Gitlab::Redis::MultiStore do
'execute :srem command' | :srem | ref(:skey_value1) | [] | :smembers | ref(:skey)
'execute :del command' | :del | ref(:key2) | nil | :get | ref(:key2)
'execute :flushdb command' | :flushdb | nil | 0 | :dbsize | nil
+ 'execute :eval command' | :eval | ref(:script) | ref(:value1) | :get | ref(:key1)
end
before do
primary_store.flushdb
secondary_store.flushdb
- primary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
+ primary_store.set(key2, value1)
+ primary_store.sadd?(skey, value1)
- secondary_store.multi do |multi|
- multi.set(key2, value1)
- multi.sadd(skey, value1)
- end
+ secondary_store.set(key2, value1)
+ secondary_store.sadd?(skey, value1)
end
with_them do
- describe "#{name}" do
+ describe name.to_s do
let(:expected_args) { args || no_args }
before do
@@ -409,100 +363,58 @@ RSpec.describe Gitlab::Redis::MultiStore do
allow(secondary_store).to receive(name).and_call_original
end
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when executing on primary instance is successful' do
- it 'executes on both primary and secondary redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args).and_call_original
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
-
- context 'when executing on the primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception and execute on secondary instance', :aggregate_errors do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, command_name: name, instance_name: instance_name))
- expect(secondary_store).to receive(name).with(*expected_args).and_call_original
-
- subject
- end
+ context 'when executing on primary instance is successful' do
+ it 'executes on both primary and secondary redis store', :aggregate_errors do
+ expect(primary_store).to receive(name).with(*expected_args).and_call_original
+ expect(secondary_store).to receive(name).with(*expected_args).and_call_original
- include_examples 'verify that store contains values', :secondary_store
+ subject
end
- context 'when the command is executed within pipelined block' do
- subject do
- multi_store.pipelined do |pipeline|
- pipeline.send(name, *args)
- end
- end
-
- it 'is executed only 1 time on each instance', :aggregate_errors do
- expect(primary_store).to receive(:pipelined).and_call_original
- expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
- end
-
- expect(secondary_store).to receive(:pipelined).and_call_original
- expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
- expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
- end
-
- subject
- end
-
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
- end
+ include_examples 'verify that store contains values', :primary_store
+ include_examples 'verify that store contains values', :secondary_store
end
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
+ context 'when executing on the primary instance is raising an exception' do
before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
+ allow(primary_store).to receive(name).with(*expected_args).and_raise(StandardError)
+ allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
+ it 'logs the exception and execute on secondary instance', :aggregate_errors do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
+ hash_including(:multi_store_error_message, command_name: name, instance_name: instance_name))
+ expect(secondary_store).to receive(name).with(*expected_args).and_call_original
+
+ subject
+ end
- it 'executes only on the secondary redis store', :aggregate_errors do
- expect(secondary_store).to receive(name).with(*expected_args)
- expect(primary_store).not_to receive(name).with(*expected_args)
+ include_examples 'verify that store contains values', :secondary_store
+ end
- subject
+ context 'when the command is executed within pipelined block' do
+ subject do
+ multi_store.pipelined do |pipeline|
+ pipeline.send(name, *args)
end
-
- include_examples 'verify that store contains values', :secondary_store
end
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
+ it 'is executed only 1 time on each instance', :aggregate_errors do
+ expect(primary_store).to receive(:pipelined).and_call_original
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
end
- it 'executes only on the primary_redis redis store', :aggregate_errors do
- expect(primary_store).to receive(name).with(*expected_args)
- expect(secondary_store).not_to receive(name).with(*expected_args)
-
- subject
+ expect(secondary_store).to receive(:pipelined).and_call_original
+ expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
+ expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
end
- include_examples 'verify that store contains values', :primary_store
+ subject
end
+
+ include_examples 'verify that store contains values', :primary_store
+ include_examples 'verify that store contains values', :secondary_store
end
end
end
@@ -537,151 +449,109 @@ RSpec.describe Gitlab::Redis::MultiStore do
end
end
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- context 'when executing on primary instance is successful' do
- it 'executes on both primary and secondary redis store', :aggregate_errors do
- expect(primary_store).to receive(name).and_call_original
- expect(secondary_store).to receive(name).and_call_original
-
- subject
- end
+ context 'when executing on primary instance is successful' do
+ it 'executes on both primary and secondary redis store', :aggregate_errors do
+ expect(primary_store).to receive(name).and_call_original
+ expect(secondary_store).to receive(name).and_call_original
- include_examples 'verify that store contains values', :primary_store
- include_examples 'verify that store contains values', :secondary_store
+ subject
end
- context 'when executing on the primary instance is raising an exception' do
- before do
- allow(primary_store).to receive(name).and_raise(StandardError)
- allow(Gitlab::ErrorTracking).to receive(:log_exception)
- end
-
- it 'logs the exception and execute on secondary instance', :aggregate_errors do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
- hash_including(:multi_store_error_message, command_name: name))
- expect(secondary_store).to receive(name).and_call_original
-
- subject
- end
+ include_examples 'verify that store contains values', :primary_store
+ include_examples 'verify that store contains values', :secondary_store
+ end
- include_examples 'verify that store contains values', :secondary_store
+ context 'when executing on the primary instance is raising an exception' do
+ before do
+ allow(primary_store).to receive(name).and_raise(StandardError)
+ allow(Gitlab::ErrorTracking).to receive(:log_exception)
end
- describe 'return values from a pipelined command' do
- RSpec::Matchers.define :pipeline_diff_error_with_stacktrace do |message|
- match do |object|
- expect(object).to be_a(Gitlab::Redis::MultiStore::PipelinedDiffError)
- expect(object.backtrace).not_to be_nil
- expect(object.message).to eq(message)
- end
- end
+ it 'logs the exception and execute on secondary instance', :aggregate_errors do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(an_instance_of(StandardError),
+ hash_including(:multi_store_error_message, command_name: name))
+ expect(secondary_store).to receive(name).and_call_original
- subject do
- multi_store.send(name) do |redis|
- redis.get(key1)
- end
- end
-
- context 'when the value exists on both and are equal' do
- before do
- primary_store.set(key1, value1)
- secondary_store.set(key1, value1)
- end
+ subject
+ end
- it 'returns the value' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ include_examples 'verify that store contains values', :secondary_store
+ end
- expect(subject).to eq([value1])
- end
+ describe 'return values from a pipelined command' do
+ RSpec::Matchers.define :pipeline_diff_error_with_stacktrace do |message|
+ match do |object|
+ expect(object).to be_a(Gitlab::Redis::MultiStore::PipelinedDiffError)
+ expect(object.backtrace).not_to be_nil
+ expect(object.message).to eq(message)
end
+ end
- context 'when the value exists on both but differ' do
- before do
- primary_store.set(key1, value1)
- secondary_store.set(key1, value2)
- end
-
- it 'returns the value from the secondary store, logging an error' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
- pipeline_diff_error_with_stacktrace(
- 'Pipelined command executed on both stores successfully but results differ between them. ' \
- "Result from the primary: [#{value1.inspect}]. Result from the secondary: [#{value2.inspect}]."
- ),
- hash_including(command_name: name, instance_name: instance_name)
- ).and_call_original
- expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
-
- expect(subject).to eq([value2])
- end
+ subject do
+ multi_store.send(name) do |redis|
+ redis.get(key1)
end
+ end
- context 'when the value does not exist on the primary but it does on the secondary' do
- before do
- secondary_store.set(key1, value2)
- end
-
- it 'returns the value from the secondary store, logging an error' do
- expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
- pipeline_diff_error_with_stacktrace(
- 'Pipelined command executed on both stores successfully but results differ between them. ' \
- "Result from the primary: [nil]. Result from the secondary: [#{value2.inspect}]."
- ),
- hash_including(command_name: name, instance_name: instance_name)
- )
- expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
-
- expect(subject).to eq([value2])
- end
+ context 'when the value exists on both and are equal' do
+ before do
+ primary_store.set(key1, value1)
+ secondary_store.set(key1, value1)
end
- context 'when the value does not exist in either' do
- it 'returns nil without logging an error' do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- expect(counter).not_to receive(:increment)
+ it 'returns the value' do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
- expect(subject).to eq([nil])
- end
+ expect(subject).to eq([value1])
end
end
- end
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
+ context 'when the value exists on both but differ' do
before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
+ primary_store.set(key1, value1)
+ secondary_store.set(key1, value2)
end
- it 'executes only on the secondary redis store', :aggregate_errors do
- expect(secondary_store).to receive(name)
- expect(primary_store).not_to receive(name)
-
- subject
+ it 'returns the value from the secondary store, logging an error' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ pipeline_diff_error_with_stacktrace(
+ 'Pipelined command executed on both stores successfully but results differ between them. ' \
+ "Result from the primary: [#{value1.inspect}]. Result from the secondary: [#{value2.inspect}]."
+ ),
+ hash_including(command_name: name, instance_name: instance_name)
+ ).and_call_original
+ expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
+
+ expect(subject).to eq([value2])
end
-
- include_examples 'verify that store contains values', :secondary_store
end
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
+ context 'when the value does not exist on the primary but it does on the secondary' do
before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
+ secondary_store.set(key1, value2)
end
- it 'executes only on the primary_redis redis store', :aggregate_errors do
- expect(primary_store).to receive(name)
- expect(secondary_store).not_to receive(name)
-
- subject
+ it 'returns the value from the secondary store, logging an error' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ pipeline_diff_error_with_stacktrace(
+ 'Pipelined command executed on both stores successfully but results differ between them. ' \
+ "Result from the primary: [nil]. Result from the secondary: [#{value2.inspect}]."
+ ),
+ hash_including(command_name: name, instance_name: instance_name)
+ )
+ expect(counter).to receive(:increment).with(command: name, instance_name: instance_name)
+
+ expect(subject).to eq([value2])
end
+ end
- include_examples 'verify that store contains values', :primary_store
+ context 'when the value does not exist in either' do
+ it 'returns nil without logging an error' do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+ expect(counter).not_to receive(:increment)
+
+ expect(subject).to eq([nil])
+ end
end
end
end
@@ -825,40 +695,8 @@ RSpec.describe Gitlab::Redis::MultiStore do
describe '#to_s' do
subject { multi_store.to_s }
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(primary_store.to_s)
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'returns same value as primary_store' do
- is_expected.to eq(secondary_store.to_s)
- end
- end
+ it 'returns same value as primary_store' do
+ is_expected.to eq(primary_store.to_s)
end
end
@@ -869,24 +707,8 @@ RSpec.describe Gitlab::Redis::MultiStore do
end
describe '#use_primary_and_secondary_stores?' do
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be true
- end
- end
-
- context 'with feature flag :use_primary_and_secondary_stores_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_and_secondary_stores_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_and_secondary_stores?).to be false
- end
+ it 'multi store is enabled' do
+ expect(multi_store.use_primary_and_secondary_stores?).to be true
end
context 'with empty DB' do
@@ -911,24 +733,8 @@ RSpec.describe Gitlab::Redis::MultiStore do
end
describe '#use_primary_store_as_default?' do
- context 'with feature flag :use_primary_store_as_default_for_test_store is enabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: true)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be true
- end
- end
-
- context 'with feature flag :use_primary_store_as_default_for_test_store is disabled' do
- before do
- stub_feature_flags(use_primary_store_as_default_for_test_store: false)
- end
-
- it 'multi store is disabled' do
- expect(multi_store.use_primary_store_as_default?).to be false
- end
+ it 'multi store is disabled' do
+ expect(multi_store.use_primary_store_as_default?).to be true
end
context 'with empty DB' do
diff --git a/spec/lib/gitlab/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb
index a7b777cf4f2..10842173365 100644
--- a/spec/lib/gitlab/request_forgery_protection_spec.rb
+++ b/spec/lib/gitlab/request_forgery_protection_spec.rb
@@ -13,6 +13,12 @@ RSpec.describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do
}
end
+ it 'logs to /dev/null' do
+ expect(ActiveSupport::Logger).to receive(:new).with(File::NULL)
+
+ described_class::Controller.new.logger
+ end
+
describe '.call' do
context 'when the request method is GET' do
before do
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 86640efed5a..181a911c667 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe Gitlab::Runtime do
before do
stub_const('::Sidekiq', sidekiq_type)
allow(sidekiq_type).to receive(:server?).and_return(true)
- allow(sidekiq_type).to receive(:options).and_return(concurrency: 2)
+ allow(sidekiq_type).to receive(:[]).with(:concurrency).and_return(2)
end
it_behaves_like "valid runtime", :sidekiq, 5
diff --git a/spec/lib/gitlab/service_desk_email_spec.rb b/spec/lib/gitlab/service_desk_email_spec.rb
index 6667b61c02b..69569c0f194 100644
--- a/spec/lib/gitlab/service_desk_email_spec.rb
+++ b/spec/lib/gitlab/service_desk_email_spec.rb
@@ -1,39 +1,11 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::ServiceDeskEmail do
- describe '.enabled?' do
- context 'when service_desk_email is enabled and address is set' do
- before do
- stub_service_desk_email_setting(enabled: true, address: 'foo')
- end
+ let(:setting_name) { :service_desk_email }
- it 'returns true' do
- expect(described_class.enabled?).to be_truthy
- end
- end
-
- context 'when service_desk_email is disabled' do
- before do
- stub_service_desk_email_setting(enabled: false, address: 'foo')
- end
-
- it 'returns false' do
- expect(described_class.enabled?).to be_falsey
- end
- end
-
- context 'when service desk address is not set' do
- before do
- stub_service_desk_email_setting(enabled: true, address: nil)
- end
-
- it 'returns false' do
- expect(described_class.enabled?).to be_falsey
- end
- end
- end
+ it_behaves_like 'common email methods'
describe '.key_from_address' do
context 'when service desk address is set' do
@@ -78,10 +50,4 @@ RSpec.describe Gitlab::ServiceDeskEmail do
end
end
end
-
- context 'self.key_from_fallback_message_id' do
- it 'returns reply key' do
- expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
- end
- end
end
diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb
index c5b00afe672..5f72a3feba7 100644
--- a/spec/lib/gitlab/sidekiq_config_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config_spec.rb
@@ -157,7 +157,7 @@ RSpec.describe Gitlab::SidekiqConfig do
allow(::Gitlab::SidekiqConfig::WorkerRouter)
.to receive(:global).and_return(::Gitlab::SidekiqConfig::WorkerRouter.new(test_routes))
- allow(Sidekiq).to receive(:options).and_return(queues: %w[default background_migration])
+ allow(Sidekiq).to receive(:[]).with(:queues).and_return(%w[default background_migration])
mappings = described_class.current_worker_queue_mappings
diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
index 62681b21756..8c9a1abba5a 100644
--- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
stub_const("#{described_class}::CHECK_INTERVAL_SECONDS", check_interval_seconds)
stub_const("#{described_class}::GRACE_BALLOON_SECONDS", grace_balloon_seconds)
allow(Process).to receive(:getpgrp).and_return(pid)
- allow(Sidekiq).to receive(:options).and_return(timeout: 9)
+ allow(Sidekiq).to receive(:[]).with(:timeout).and_return(9)
end
it 'return true when everything is within limit', :aggregate_failures do
@@ -257,7 +257,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do
before do
stub_const("#{described_class}::SHUTDOWN_TIMEOUT_SECONDS", shutdown_timeout_seconds)
stub_feature_flags(sidekiq_memory_killer_read_only_mode: false)
- allow(Sidekiq).to receive(:options).and_return(timeout: 9)
+ allow(Sidekiq).to receive(:[]).with(:timeout).and_return(9)
allow(memory_killer).to receive(:get_rss_kb).and_return(100)
allow(memory_killer).to receive(:get_soft_limit_rss_kb).and_return(200)
allow(memory_killer).to receive(:get_hard_limit_rss_kb).and_return(300)
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
index 4d12e4b3f6f..44c8df73463 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/client_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Client, :clean_gitlab_redis_queues do
+RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::Client, :clean_gitlab_redis_queues,
+:clean_gitlab_redis_shared_state do
shared_context 'deduplication worker class' do |strategy, including_scheduled|
let(:worker_class) do
Class.new 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 d240bf51e67..b6748d49739 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
@@ -11,8 +11,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
let(:wal_locations) do
{
- main: '0/D525E3A8',
- ci: 'AB/12345'
+ 'main' => '0/D525E3A8',
+ 'ci' => 'AB/12345'
}
end
@@ -24,10 +24,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
"#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:duplicate:#{queue}:#{hash}"
end
- let(:deduplicated_flag_key) do
- "#{idempotency_key}:deduplicate_flag"
- end
-
describe '#schedule' do
shared_examples 'scheduling with deduplication class' do |strategy_class|
it 'calls schedule on the strategy' do
@@ -81,29 +77,26 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
end
- shared_examples 'tracking duplicates in redis' do
+ shared_examples 'with Redis cookies' do
+ let(:cookie_key) { "#{idempotency_key}:cookie:v2" }
+ let(:cookie) { get_redis_msgpack(cookie_key) }
+
describe '#check!' do
context 'when there was no job in the queue yet' do
it { expect(duplicate_job.check!).to eq('123') }
shared_examples 'sets Redis keys with correct TTL' do
it "adds an idempotency key with correct ttl" do
- expect { duplicate_job.check! }
- .to change { read_idempotency_key_with_ttl(idempotency_key) }
- .from([nil, -2])
- .to(['123', be_within(1).of(expected_ttl)])
- end
+ expected_cookie = {
+ 'jid' => '123',
+ 'offsets' => {},
+ 'wal_locations' => {},
+ 'existing_wal_locations' => wal_locations
+ }
- context 'when wal locations is not empty' do
- it "adds an existing wal locations key with correct ttl" do
- expect { duplicate_job.check! }
- .to change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :main)) }
- .from([nil, -2])
- .to([wal_locations[:main], be_within(1).of(expected_ttl)])
- .and change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :ci)) }
- .from([nil, -2])
- .to([wal_locations[:ci], be_within(1).of(expected_ttl)])
- end
+ duplicate_job.check!
+ expect(cookie).to eq(expected_cookie)
+ expect(redis_ttl(cookie_key)).to be_within(1).of(expected_ttl)
end
end
@@ -130,32 +123,23 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'when there was already a job with same arguments in the same queue' do
before do
- set_idempotency_key(idempotency_key, 'existing-key')
- wal_locations.each do |config_name, location|
- set_idempotency_key(existing_wal_location_key(idempotency_key, config_name), location)
- end
+ set_idempotency_key(cookie_key, existing_cookie.to_msgpack)
end
- it { expect(duplicate_job.check!).to eq('existing-key') }
+ let(:existing_cookie) { { 'jid' => 'existing-jid' } }
- it "does not change the existing key's TTL" do
- expect { duplicate_job.check! }
- .not_to change { read_idempotency_key_with_ttl(idempotency_key) }
- .from(['existing-key', -1])
- end
+ it { expect(duplicate_job.check!).to eq('existing-jid') }
- it "does not change the existing wal locations key's TTL" do
+ it "does not change the existing key's TTL" do
expect { duplicate_job.check! }
- .to not_change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :main)) }
- .from([wal_locations[:main], -1])
- .and not_change { read_idempotency_key_with_ttl(existing_wal_location_key(idempotency_key, :ci)) }
- .from([wal_locations[:ci], -1])
+ .not_to change { redis_ttl(cookie_key) }
+ .from(-1)
end
it 'sets the existing jid' do
duplicate_job.check!
- expect(duplicate_job.existing_jid).to eq('existing-key')
+ expect(duplicate_job.existing_jid).to eq('existing-jid')
end
end
end
@@ -166,115 +150,90 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
{ main: ::ActiveRecord::Base,
ci: ::ActiveRecord::Base })
- set_idempotency_key(existing_wal_location_key(idempotency_key, :main), existing_wal[:main])
- set_idempotency_key(existing_wal_location_key(idempotency_key, :ci), existing_wal[:ci])
+ with_redis { |r| r.set(cookie_key, initial_cookie.to_msgpack, ex: expected_ttl) }
# read existing_wal_locations
duplicate_job.check!
end
- context "when the key doesn't exists in redis" do
- let(:existing_wal) do
- {
- main: '0/D525E3A0',
- ci: 'AB/12340'
- }
- end
+ let(:initial_cookie) do
+ {
+ 'jid' => 'foobar',
+ 'existing_wal_locations' => { 'main' => '0/D525E3A0', 'ci' => 'AB/12340' },
+ 'offsets' => {},
+ 'wal_locations' => {}
+ }
+ end
- let(:new_wal_location_with_offset) do
- {
- # offset is relative to `existing_wal`
- main: ['0/D525E3A8', '8'],
- ci: ['AB/12345', '5']
- }
- end
+ let(:expected_ttl) { 123 }
+ let(:new_wal) do
+ {
+ # offset is relative to `existing_wal`
+ 'main' => { location: '0/D525E3A8', offset: '8' },
+ 'ci' => { location: 'AB/12345', offset: '5' }
+ }
+ end
- let(:wal_locations) { new_wal_location_with_offset.transform_values(&:first) }
+ let(:wal_locations) { new_wal.transform_values { |v| v[:location] } }
- it 'stores a wal location to redis with an offset relative to existing wal location' do
- expect { duplicate_job.update_latest_wal_location! }
- .to change { read_range_from_redis(wal_location_key(idempotency_key, :main)) }
- .from([])
- .to(new_wal_location_with_offset[:main])
- .and change { read_range_from_redis(wal_location_key(idempotency_key, :ci)) }
- .from([])
- .to(new_wal_location_with_offset[:ci])
- end
+ it 'stores a wal location to redis with an offset relative to existing wal location' do
+ duplicate_job.update_latest_wal_location!
+
+ expect(cookie['wal_locations']).to eq(wal_locations)
+ expect(cookie['offsets']).to eq(new_wal.transform_values { |v| v[:offset].to_i })
+ expect(redis_ttl(cookie_key)).to be_within(1).of(expected_ttl)
end
+ end
- context "when the key exists in redis" do
- before do
- rpush_to_redis_key(wal_location_key(idempotency_key, :main), *stored_wal_location_with_offset[:main])
- rpush_to_redis_key(wal_location_key(idempotency_key, :ci), *stored_wal_location_with_offset[:ci])
+ describe 'UPDATE_WAL_COOKIE_SCRIPT' do
+ subject do
+ with_redis do |redis|
+ redis.eval(described_class::UPDATE_WAL_COOKIE_SCRIPT, keys: [cookie_key], argv: argv)
end
+ end
- let(:wal_locations) { new_wal_location_with_offset.transform_values(&:first) }
+ let(:argv) { ['c1', 1, 'loc1', 'c2', 2, 'loc2', 'c3', 3, 'loc3'] }
- context "when the new offset is bigger then the existing one" do
- let(:existing_wal) do
- {
- main: '0/D525E3A0',
- ci: 'AB/12340'
- }
- end
+ it 'does not create the key' do
+ subject
- let(:stored_wal_location_with_offset) do
- {
- # offset is relative to `existing_wal`
- main: ['0/D525E3A3', '3'],
- ci: ['AB/12342', '2']
- }
- end
+ expect(with_redis { |r| r.get(cookie_key) }).to eq(nil)
+ end
- let(:new_wal_location_with_offset) do
- {
- # offset is relative to `existing_wal`
- main: ['0/D525E3A8', '8'],
- ci: ['AB/12345', '5']
- }
- end
+ context 'when the key exists' do
+ let(:existing_cookie) { { 'offsets' => {}, 'wal_locations' => {} } }
+ let(:expected_ttl) { 123 }
- it 'updates a wal location to redis with an offset' do
- expect { duplicate_job.update_latest_wal_location! }
- .to change { read_range_from_redis(wal_location_key(idempotency_key, :main)) }
- .from(stored_wal_location_with_offset[:main])
- .to(new_wal_location_with_offset[:main])
- .and change { read_range_from_redis(wal_location_key(idempotency_key, :ci)) }
- .from(stored_wal_location_with_offset[:ci])
- .to(new_wal_location_with_offset[:ci])
- end
+ before do
+ with_redis { |r| r.set(cookie_key, existing_cookie.to_msgpack, ex: expected_ttl) }
end
- context "when the old offset is not bigger then the existing one" do
- let(:existing_wal) do
- {
- main: '0/D525E3A0',
- ci: 'AB/12340'
- }
- end
+ it 'updates all connections' do
+ subject
- let(:stored_wal_location_with_offset) do
- {
- # offset is relative to `existing_wal`
- main: ['0/D525E3A8', '8'],
- ci: ['AB/12345', '5']
- }
- end
+ expect(cookie['wal_locations']).to eq({ 'c1' => 'loc1', 'c2' => 'loc2', 'c3' => 'loc3' })
+ expect(cookie['offsets']).to eq({ 'c1' => 1, 'c2' => 2, 'c3' => 3 })
+ end
+
+ it 'preserves the ttl' do
+ subject
- let(:new_wal_location_with_offset) do
+ expect(redis_ttl(cookie_key)).to be_within(1).of(expected_ttl)
+ end
+
+ context 'and low offsets' do
+ let(:existing_cookie) do
{
- # offset is relative to `existing_wal`
- main: ['0/D525E3A2', '2'],
- ci: ['AB/12342', '2']
+ 'offsets' => { 'c1' => 0, 'c2' => 2 },
+ 'wal_locations' => { 'c1' => 'loc1old', 'c2' => 'loc2old' }
}
end
- it "does not update a wal location to redis with an offset" do
- expect { duplicate_job.update_latest_wal_location! }
- .to not_change { read_range_from_redis(wal_location_key(idempotency_key, :main)) }
- .from(stored_wal_location_with_offset[:main])
- .and not_change { read_range_from_redis(wal_location_key(idempotency_key, :ci)) }
- .from(stored_wal_location_with_offset[:ci])
+ it 'updates only some connections' do
+ subject
+
+ expect(cookie['wal_locations']).to eq({ 'c1' => 'loc1', 'c2' => 'loc2old', 'c3' => 'loc3' })
+ expect(cookie['offsets']).to eq({ 'c1' => 1, 'c2' => 2, 'c3' => 3 })
end
end
end
@@ -283,11 +242,11 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
describe '#latest_wal_locations' do
context 'when job was deduplicated and wal locations were already persisted' do
before do
- rpush_to_redis_key(wal_location_key(idempotency_key, :main), wal_locations[:main], 1024)
- rpush_to_redis_key(wal_location_key(idempotency_key, :ci), wal_locations[:ci], 1024)
+ cookie = { 'wal_locations' => { 'main' => 'abc', 'ci' => 'def' } }.to_msgpack
+ set_idempotency_key(cookie_key, cookie)
end
- it { expect(duplicate_job.latest_wal_locations).to eq(wal_locations) }
+ it { expect(duplicate_job.latest_wal_locations).to eq({ 'main' => 'abc', 'ci' => 'def' }) }
end
context 'when job is not deduplication and wal locations were not persisted' do
@@ -302,60 +261,22 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'when the key exists in redis' do
before do
- set_idempotency_key(idempotency_key, 'existing-jid')
- set_idempotency_key(deduplicated_flag_key, 1)
- wal_locations.each do |config_name, location|
- set_idempotency_key(existing_wal_location_key(idempotency_key, config_name), location)
- set_idempotency_key(wal_location_key(idempotency_key, config_name), location)
- end
+ set_idempotency_key(cookie_key, "garbage")
end
shared_examples 'deleting the duplicate job' do
shared_examples 'deleting keys from redis' do |key_name|
it "removes the #{key_name} from redis" do
expect { duplicate_job.delete! }
- .to change { read_idempotency_key_with_ttl(key) }
- .from([from_value, -1])
- .to([nil, -2])
+ .to change { with_redis { |r| r.get(key) } }
+ .from(from_value)
+ .to(nil)
end
end
- shared_examples 'does not delete key from redis' do |key_name|
- it "does not remove the #{key_name} from redis" do
- expect { duplicate_job.delete! }
- .to not_change { read_idempotency_key_with_ttl(key) }
- .from([from_value, -1])
- end
- end
-
- it_behaves_like 'deleting keys from redis', 'idempotent key' do
- let(:key) { idempotency_key }
- let(:from_value) { 'existing-jid' }
- end
-
- it_behaves_like 'deleting keys from redis', 'deduplication counter key' do
- let(:key) { deduplicated_flag_key }
- let(:from_value) { '1' }
- end
-
- it_behaves_like 'deleting keys from redis', 'existing wal location keys for main database' do
- let(:key) { existing_wal_location_key(idempotency_key, :main) }
- let(:from_value) { wal_locations[:main] }
- end
-
- it_behaves_like 'deleting keys from redis', 'existing wal location keys for ci database' do
- let(:key) { existing_wal_location_key(idempotency_key, :ci) }
- let(:from_value) { wal_locations[:ci] }
- end
-
- it_behaves_like 'deleting keys from redis', 'latest wal location keys for main database' do
- let(:key) { wal_location_key(idempotency_key, :main) }
- let(:from_value) { wal_locations[:main] }
- end
-
- it_behaves_like 'deleting keys from redis', 'latest wal location keys for ci database' do
- let(:key) { wal_location_key(idempotency_key, :ci) }
- let(:from_value) { wal_locations[:ci] }
+ it_behaves_like 'deleting keys from redis', 'cookie key' do
+ let(:key) { cookie_key }
+ let(:from_value) { "garbage" }
end
end
@@ -387,15 +308,14 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
describe '#set_deduplicated_flag!' do
context 'when the job is reschedulable' do
before do
+ duplicate_job.check! # ensure cookie exists
allow(duplicate_job).to receive(:reschedulable?) { true }
end
it 'sets the key in Redis' do
duplicate_job.set_deduplicated_flag!
- flag = with_redis { |redis| redis.get(deduplicated_flag_key) }
-
- expect(flag).to eq(described_class::DEDUPLICATED_FLAG_VALUE.to_s)
+ expect(cookie['deduplicated']).to eq('1')
end
it 'sets, gets and cleans up the deduplicated flag' do
@@ -415,11 +335,10 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
end
it 'does not set the key in Redis' do
+ duplicate_job.check!
duplicate_job.set_deduplicated_flag!
- flag = with_redis { |redis| redis.get(deduplicated_flag_key) }
-
- expect(flag).to be_nil
+ expect(cookie['deduplicated']).to eq(nil)
end
it 'does not set the deduplicated flag' do
@@ -445,43 +364,24 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
expect(duplicate_job.duplicate?).to be(false)
end
- it 'returns false if the existing jid is different from the job jid' do
- set_idempotency_key(idempotency_key, 'a different jid')
+ it 'returns true if the existing jid is different from the job jid' do
+ set_idempotency_key(cookie_key, { 'jid' => 'a different jid' }.to_msgpack)
duplicate_job.check!
expect(duplicate_job.duplicate?).to be(true)
end
end
- def existing_wal_location_key(idempotency_key, connection_name)
- "#{idempotency_key}:#{connection_name}:existing_wal_location"
- end
-
- def wal_location_key(idempotency_key, connection_name)
- "#{idempotency_key}:#{connection_name}:wal_location"
- end
-
- def set_idempotency_key(key, value = '1')
+ def set_idempotency_key(key, value)
with_redis { |r| r.set(key, value) }
end
- def rpush_to_redis_key(key, wal, offset)
- with_redis { |r| r.rpush(key, [wal, offset]) }
- end
-
- def read_idempotency_key_with_ttl(key)
- with_redis do |redis|
- redis.pipelined do |p|
- p.get(key)
- p.ttl(key)
- end
- end
+ def get_redis_msgpack(key)
+ MessagePack.unpack(with_redis { |redis| redis.get(key) })
end
- def read_range_from_redis(key)
- with_redis do |redis|
- redis.lrange(key, 0, -1)
- end
+ def redis_ttl(key)
+ with_redis { |redis| redis.ttl(key) }
end
end
@@ -497,7 +397,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
duplicate_job.check!
end
- it_behaves_like 'tracking duplicates in redis'
+ it_behaves_like 'with Redis cookies'
end
context 'when both multi-store feature flags are off' do
@@ -517,7 +417,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
duplicate_job.check!
end
- it_behaves_like 'tracking duplicates in redis'
+ it_behaves_like 'with Redis cookies'
end
describe '#scheduled?' do
@@ -562,6 +462,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'with deduplicated flag' do
before do
+ duplicate_job.check! # ensure cookie exists
duplicate_job.set_deduplicated_flag!
end
@@ -578,6 +479,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi
context 'with deduplicated flag' do
before do
+ duplicate_job.check! # ensure cookie exists
duplicate_job.set_deduplicated_flag!
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
index 54a1723afbc..1a53a9b8701 100644
--- a/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
describe '.initialize_process_metrics' do
it 'sets concurrency metrics' do
- expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i)
+ expect(concurrency_metric).to receive(:set).with({}, Sidekiq[:concurrency].to_i)
described_class.initialize_process_metrics
end
@@ -65,7 +65,7 @@ RSpec.describe Gitlab::SidekiqMiddleware::ServerMetrics do
end
it 'sets the concurrency metric' do
- expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i)
+ expect(concurrency_metric).to receive(:set).with({}, Sidekiq[:concurrency].to_i)
described_class.initialize_process_metrics
end
diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb
index 14dbeac37e8..af9075f5aa0 100644
--- a/spec/lib/gitlab/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb
@@ -6,7 +6,7 @@ require 'sidekiq/testing'
RSpec.describe Gitlab::SidekiqMiddleware do
let(:job_args) { [0.01] }
let(:disabled_sidekiq_middlewares) { [] }
- let(:chain) { Sidekiq::Middleware::Chain.new }
+ let(:chain) { Sidekiq::Middleware::Chain.new(Sidekiq) }
let(:queue) { 'test' }
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
let(:worker_class) do
diff --git a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb
index d4391d3023a..9ed2a0642fc 100644
--- a/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb
+++ b/spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb
@@ -16,34 +16,42 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
clear_queues
end
- describe '#execute', :aggregate_failures do
+ describe '#migrate_set', :aggregate_failures do
shared_examples 'processing a set' do
- let(:migrator) { described_class.new(set_name) }
+ let(:migrator) { described_class.new(mappings) }
let(:set_after) do
Sidekiq.redis { |c| c.zrange(set_name, 0, -1, with_scores: true) }
- .map { |item, score| [Sidekiq.load_json(item), score] }
+ .map { |item, score| [Gitlab::Json.load(item), score] }
end
context 'when the set is empty' do
+ let(:mappings) { { 'AuthorizedProjectsWorker' => 'new_queue' } }
+
it 'returns the number of scanned and migrated jobs' do
- expect(migrator.execute('AuthorizedProjectsWorker' => 'new_queue')).to eq(scanned: 0, migrated: 0)
+ expect(migrator.migrate_set(set_name)).to eq(
+ scanned: 0,
+ migrated: 0)
end
end
context 'when the set is not empty' do
+ let(:mappings) { {} }
+
it 'returns the number of scanned and migrated jobs' do
create_jobs
- expect(migrator.execute({})).to eq(scanned: 4, migrated: 0)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 0)
end
end
context 'when there are no matching jobs' do
+ let(:mappings) { { 'PostReceive' => 'new_queue' } }
+
it 'does not change any queue names' do
create_jobs(include_post_receive: false)
- expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 3, migrated: 0)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 3, migrated: 0)
expect(set_after.length).to eq(3)
expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects',
@@ -53,10 +61,13 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
context 'when there are matching jobs' do
it 'migrates only the workers matching the given worker from the set' do
+ migrator = described_class.new({ 'AuthorizedProjectsWorker' => 'new_queue' })
freeze_time do
create_jobs
- expect(migrator.execute('AuthorizedProjectsWorker' => 'new_queue')).to eq(scanned: 4, migrated: 3)
+ expect(migrator.migrate_set(set_name)).to eq(
+ scanned: 4,
+ migrated: 3)
set_after.each.with_index do |(item, score), i|
if item['class'] == 'AuthorizedProjectsWorker'
@@ -71,11 +82,14 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
end
it 'allows migrating multiple workers at once' do
+ migrator = described_class.new({
+ 'AuthorizedProjectsWorker' => 'new_queue',
+ 'PostReceive' => 'another_queue'
+ })
freeze_time do
create_jobs
- expect(migrator.execute('AuthorizedProjectsWorker' => 'new_queue', 'PostReceive' => 'another_queue'))
- .to eq(scanned: 4, migrated: 4)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 4)
set_after.each.with_index do |(item, score), i|
if item['class'] == 'AuthorizedProjectsWorker'
@@ -90,11 +104,14 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
end
it 'allows migrating multiple workers to the same queue' do
+ migrator = described_class.new({
+ 'AuthorizedProjectsWorker' => 'new_queue',
+ 'PostReceive' => 'new_queue'
+ })
freeze_time do
create_jobs
- expect(migrator.execute('AuthorizedProjectsWorker' => 'new_queue', 'PostReceive' => 'new_queue'))
- .to eq(scanned: 4, migrated: 4)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 4)
set_after.each.with_index do |(item, score), i|
expect(item).to include('queue' => 'new_queue', 'args' => [i])
@@ -104,16 +121,17 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
end
it 'does not try to migrate jobs that are removed from the set during the migration' do
+ migrator = described_class.new({ 'PostReceive' => 'new_queue' })
freeze_time do
create_jobs
- allow(migrator).to receive(:migrate_job).and_wrap_original do |meth, *args|
- Sidekiq.redis { |c| c.zrem(set_name, args.first) }
+ allow(migrator).to receive(:migrate_job_in_set).and_wrap_original do |meth, *args|
+ Sidekiq.redis { |c| c.zrem(set_name, args.second) }
meth.call(*args)
end
- expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 4, migrated: 0)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 0)
expect(set_after.length).to eq(3)
expect(set_after.map(&:first)).to all(include('queue' => 'authorized_projects'))
@@ -121,11 +139,12 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
end
it 'does not try to migrate unmatched jobs that are added to the set during the migration' do
+ migrator = described_class.new({ 'PostReceive' => 'new_queue' })
create_jobs
calls = 0
- allow(migrator).to receive(:migrate_job).and_wrap_original do |meth, *args|
+ allow(migrator).to receive(:migrate_job_in_set).and_wrap_original do |meth, *args|
if calls == 0
travel_to(5.hours.from_now) { create_jobs(include_post_receive: false) }
end
@@ -135,18 +154,19 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
meth.call(*args)
end
- expect(migrator.execute('PostReceive' => 'new_queue')).to eq(scanned: 4, migrated: 1)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 1)
expect(set_after.group_by { |job| job.first['queue'] }.transform_values(&:count))
.to eq('authorized_projects' => 6, 'new_queue' => 1)
end
it 'iterates through the entire set of jobs' do
+ migrator = described_class.new({ 'NonExistentWorker' => 'new_queue' })
50.times do |i|
travel_to(i.hours.from_now) { create_jobs }
end
- expect(migrator.execute('NonExistentWorker' => 'new_queue')).to eq(scanned: 200, migrated: 0)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 200, migrated: 0)
expect(set_after.length).to eq(200)
end
@@ -158,14 +178,16 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
stub_const("#{described_class}::LOG_FREQUENCY", 2)
logger = Logger.new(StringIO.new)
- migrator = described_class.new(set_name, logger: logger)
+ migrator = described_class.new({
+ 'AuthorizedProjectsWorker' => 'new_queue',
+ 'PostReceive' => 'another_queue'
+ }, logger: logger)
expect(logger).to receive(:info).with(a_string_matching('Processing')).once.ordered
expect(logger).to receive(:info).with(a_string_matching('In progress')).once.ordered
expect(logger).to receive(:info).with(a_string_matching('Done')).once.ordered
- expect(migrator.execute('AuthorizedProjectsWorker' => 'new_queue', 'PostReceive' => 'new_queue'))
- .to eq(scanned: 4, migrated: 4)
+ expect(migrator.migrate_set(set_name)).to eq(scanned: 4, migrated: 4)
end
end
end
@@ -186,25 +208,6 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
end
context 'retried jobs' do
- let(:set_name) { 'retry' }
- # Account for Sidekiq retry jitter
- # https://github.com/mperham/sidekiq/blob/3575ccb44c688dd08bfbfd937696260b12c622fb/lib/sidekiq/job_retry.rb#L217
- let(:schedule_jitter) { 10 }
-
- # Try to mimic as closely as possible what Sidekiq will actually
- # do to retry a job.
- def retry_in(klass, time, args)
- message = { 'class' => klass.name, 'args' => [args], 'retry' => true }.to_json
-
- allow(klass).to receive(:sidekiq_retry_in_block).and_return(proc { time })
-
- begin
- Sidekiq::JobRetry.new.local(klass, message, klass.queue) { raise 'boom' }
- rescue Sidekiq::JobRetry::Skip
- # Sidekiq scheduled the retry
- end
- end
-
def create_jobs(include_post_receive: true)
retry_in(AuthorizedProjectsWorker, 1.hour, 0)
retry_in(AuthorizedProjectsWorker, 2.hours, 1)
@@ -212,7 +215,248 @@ RSpec.describe Gitlab::SidekiqMigrateJobs, :clean_gitlab_redis_queues do
retry_in(AuthorizedProjectsWorker, 4.hours, 3)
end
+ include_context 'when handling retried jobs'
it_behaves_like 'processing a set'
end
end
+
+ describe '#migrate_queues', :aggregate_failures do
+ let(:migrator) { described_class.new(mappings, logger: logger) }
+ let(:logger) { nil }
+
+ def list_queues
+ queues = Sidekiq.redis do |conn|
+ conn.scan_each(match: "queue:*").to_a
+ end
+ queues.uniq.map { |queue| queue.split(':', 2).last }
+ end
+
+ def list_jobs(queue_name)
+ Sidekiq.redis { |conn| conn.lrange("queue:#{queue_name}", 0, -1) }
+ .map { |item| Gitlab::Json.load(item) }
+ end
+
+ def pre_migrate_checks; end
+
+ before do
+ queue_name_from_worker_name = Gitlab::SidekiqConfig::WorkerRouter.method(:queue_name_from_worker_name)
+ EmailReceiverWorker.sidekiq_options(queue: queue_name_from_worker_name.call(EmailReceiverWorker))
+ EmailReceiverWorker.perform_async('foo')
+ EmailReceiverWorker.perform_async('bar')
+
+ # test worker that has ':' inside the queue name
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.sidekiq_options(
+ queue: queue_name_from_worker_name.call(AuthorizedProjectUpdate::ProjectRecalculateWorker)
+ )
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async
+ end
+
+ after do
+ # resets the queue name to its original
+ EmailReceiverWorker.set_queue
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.set_queue
+ end
+
+ shared_examples 'migrating queues' do
+ it 'migrates the jobs to the correct destination queue' do
+ queues = list_queues
+ expect(queues).to include(*queues_included_pre_migrate)
+ expect(queues).not_to include(*queues_excluded_pre_migrate)
+ pre_migrate_checks
+
+ migrator.migrate_queues
+
+ queues = list_queues
+ expect(queues).not_to include(*queues_excluded_post_migrate)
+ expect(queues).to include(*queues_included_post_migrate)
+ post_migrate_checks
+ end
+ end
+
+ context 'with all workers mapped to default queue' do
+ let(:mappings) do
+ { 'EmailReceiverWorker' => 'default', 'AuthorizedProjectUpdate::ProjectRecalculateWorker' => 'default' }
+ end
+
+ let(:queues_included_pre_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_excluded_pre_migrate) { ['default'] }
+ let(:queues_excluded_post_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_included_post_migrate) { ['default'] }
+
+ def post_migrate_checks
+ jobs = list_jobs('default')
+ expect(jobs.length).to eq(3)
+ sorted = jobs.sort_by { |job| [job["class"], job["args"]] }
+ expect(sorted[0]).to include('class' => 'AuthorizedProjectUpdate::ProjectRecalculateWorker',
+ 'queue' => 'default')
+ expect(sorted[1]).to include('class' => 'EmailReceiverWorker', 'args' => ['bar'], 'queue' => 'default')
+ expect(sorted[2]).to include('class' => 'EmailReceiverWorker', 'args' => ['foo'], 'queue' => 'default')
+ end
+
+ it_behaves_like 'migrating queues'
+ end
+
+ context 'with custom mapping to different queues' do
+ let(:mappings) do
+ { 'EmailReceiverWorker' => 'new_email',
+ 'AuthorizedProjectUpdate::ProjectRecalculateWorker' => 'new_authorized' }
+ end
+
+ let(:queues_included_pre_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_excluded_pre_migrate) { %w[new_email new_authorized] }
+ let(:queues_excluded_post_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_included_post_migrate) { %w[new_email new_authorized] }
+
+ def post_migrate_checks
+ email_jobs = list_jobs('new_email')
+ expect(email_jobs.length).to eq(2)
+ expect(email_jobs[0]).to include('class' => 'EmailReceiverWorker', 'args' => ['bar'], 'queue' => 'new_email')
+ expect(email_jobs[1]).to include('class' => 'EmailReceiverWorker', 'args' => ['foo'], 'queue' => 'new_email')
+
+ export_jobs = list_jobs('new_authorized')
+ expect(export_jobs.length).to eq(1)
+ expect(export_jobs[0]).to include('class' => 'AuthorizedProjectUpdate::ProjectRecalculateWorker',
+ 'queue' => 'new_authorized')
+ end
+
+ it_behaves_like 'migrating queues'
+ end
+
+ context 'with illegal JSON payload' do
+ let(:job) { '{foo: 1}' }
+ let(:mappings) do
+ { 'EmailReceiverWorker' => 'default', 'AuthorizedProjectUpdate::ProjectRecalculateWorker' => 'default' }
+ end
+
+ let(:queues_included_pre_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_excluded_pre_migrate) { ['default'] }
+ let(:queues_excluded_post_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_included_post_migrate) { ['default'] }
+ let(:logger) { Logger.new(StringIO.new) }
+
+ before do
+ Sidekiq.redis do |conn|
+ conn.lpush("queue:email_receiver", job)
+ end
+ end
+
+ def pre_migrate_checks
+ expect(logger).to receive(:error)
+ .with(a_string_matching('Unmarshal JSON payload from SidekiqMigrateJobs failed'))
+ .once
+ end
+
+ def post_migrate_checks
+ jobs = list_jobs('default')
+ expect(jobs.length).to eq(3)
+ sorted = jobs.sort_by { |job| [job["class"], job["args"]] }
+ expect(sorted[0]).to include('class' => 'AuthorizedProjectUpdate::ProjectRecalculateWorker',
+ 'queue' => 'default')
+ expect(sorted[1]).to include('class' => 'EmailReceiverWorker', 'args' => ['bar'], 'queue' => 'default')
+ expect(sorted[2]).to include('class' => 'EmailReceiverWorker', 'args' => ['foo'], 'queue' => 'default')
+ end
+
+ it_behaves_like 'migrating queues'
+ end
+
+ context 'when multiple workers are in the same queue' do
+ before do
+ ExportCsvWorker.sidekiq_options(queue: 'email_receiver') # follows EmailReceiverWorker's queue
+ ExportCsvWorker.perform_async('fizz')
+ end
+
+ after do
+ ExportCsvWorker.set_queue
+ end
+
+ context 'when the queue exists in mappings' do
+ let(:mappings) do
+ { 'EmailReceiverWorker' => 'email_receiver', 'AuthorizedProjectUpdate::ProjectRecalculateWorker' => 'default',
+ 'ExportCsvWorker' => 'default' }
+ end
+
+ let(:queues_included_pre_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_excluded_pre_migrate) { ['default'] }
+ let(:queues_excluded_post_migrate) do
+ ['authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_included_post_migrate) { %w[default email_receiver] }
+
+ it_behaves_like 'migrating queues'
+ def post_migrate_checks
+ # jobs from email_receiver are not migrated at all
+ jobs = list_jobs('email_receiver')
+ expect(jobs.length).to eq(3)
+ sorted = jobs.sort_by { |job| [job["class"], job["args"]] }
+ expect(sorted[0]).to include('class' => 'EmailReceiverWorker', 'args' => ['bar'], 'queue' => 'email_receiver')
+ expect(sorted[1]).to include('class' => 'EmailReceiverWorker', 'args' => ['foo'], 'queue' => 'email_receiver')
+ expect(sorted[2]).to include('class' => 'ExportCsvWorker', 'args' => ['fizz'], 'queue' => 'email_receiver')
+ end
+ end
+
+ context 'when the queue doesnt exist in mappings' do
+ let(:mappings) do
+ { 'EmailReceiverWorker' => 'default', 'AuthorizedProjectUpdate::ProjectRecalculateWorker' => 'default',
+ 'ExportCsvWorker' => 'default' }
+ end
+
+ let(:queues_included_pre_migrate) do
+ ['email_receiver',
+ 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_excluded_pre_migrate) { ['default'] }
+ let(:queues_excluded_post_migrate) do
+ ['email_receiver', 'authorized_project_update:authorized_project_update_project_recalculate']
+ end
+
+ let(:queues_included_post_migrate) { ['default'] }
+
+ it_behaves_like 'migrating queues'
+ def post_migrate_checks
+ # jobs from email_receiver are all migrated
+ jobs = list_jobs('email_receiver')
+ expect(jobs.length).to eq(0)
+
+ jobs = list_jobs('default')
+ expect(jobs.length).to eq(4)
+ sorted = jobs.sort_by { |job| [job["class"], job["args"]] }
+ expect(sorted[0]).to include('class' => 'AuthorizedProjectUpdate::ProjectRecalculateWorker',
+ 'queue' => 'default')
+ expect(sorted[1]).to include('class' => 'EmailReceiverWorker', 'args' => ['bar'], 'queue' => 'default')
+ expect(sorted[2]).to include('class' => 'EmailReceiverWorker', 'args' => ['foo'], 'queue' => 'default')
+ expect(sorted[3]).to include('class' => 'ExportCsvWorker', 'args' => ['fizz'], 'queue' => 'default')
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/slash_commands/application_help_spec.rb b/spec/lib/gitlab/slash_commands/application_help_spec.rb
index b82121bf3a8..b182c0e5cc6 100644
--- a/spec/lib/gitlab/slash_commands/application_help_spec.rb
+++ b/spec/lib/gitlab/slash_commands/application_help_spec.rb
@@ -4,11 +4,13 @@ require 'spec_helper'
RSpec.describe Gitlab::SlashCommands::ApplicationHelp do
let(:params) { { command: '/gitlab', text: 'help' } }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:chat_user) { create(:chat_name, user: user) }
let(:project) { build(:project) }
describe '#execute' do
subject do
- described_class.new(project, params).execute
+ described_class.new(project, chat_user, params).execute
end
it 'displays the help section' do
@@ -16,5 +18,23 @@ RSpec.describe Gitlab::SlashCommands::ApplicationHelp do
expect(subject[:text]).to include('Available commands')
expect(subject[:text]).to include('/gitlab [project name or alias] issue show')
end
+
+ context 'with incident declare command' do
+ context 'when feature flag is enabled' do
+ it 'displays the declare command' do
+ expect(subject[:text]).to include('/gitlab incident declare')
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(incident_declare_slash_command: false)
+ end
+
+ it 'does not displays the declare command' do
+ expect(subject[:text]).not_to include('/gitlab incident declare')
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb
index 069577b3846..f4664bcfef9 100644
--- a/spec/lib/gitlab/slash_commands/command_spec.rb
+++ b/spec/lib/gitlab/slash_commands/command_spec.rb
@@ -122,5 +122,25 @@ RSpec.describe Gitlab::SlashCommands::Command do
it { is_expected.to eq(Gitlab::SlashCommands::IssueComment) }
end
+
+ context 'when incident declare is triggered' do
+ context 'IncidentNew is triggered' do
+ let(:params) { { text: 'incident declare' } }
+
+ it { is_expected.to eq(Gitlab::SlashCommands::IncidentManagement::IncidentNew) }
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(incident_declare_slash_command: false)
+ end
+
+ context 'IncidentNew is triggered' do
+ let(:params) { { text: 'incident declare' } }
+
+ it { is_expected.not_to eq(Gitlab::SlashCommands::IncidentManagement::IncidentNew) }
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/slash_commands/presenters/incident_management/incident_new_spec.rb b/spec/lib/gitlab/slash_commands/presenters/incident_management/incident_new_spec.rb
new file mode 100644
index 00000000000..cbc584b931f
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/presenters/incident_management/incident_new_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SlashCommands::Presenters::IncidentManagement::IncidentNew do
+ subject { described_class.new }
+
+ it 'returns the ephemeral message' do
+ message = subject.present('It works!')
+
+ expect(message).to be_a(Hash)
+ expect(message[:text]).to eq('It works!')
+ expect(message[:response_type]).to be(:ephemeral)
+ end
+end
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
index 9bf6f0b82bc..60bb006673f 100644
--- a/spec/lib/gitlab/sql/pattern_spec.rb
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -29,6 +29,9 @@ RSpec.describe Gitlab::SQL::Pattern do
'AH' | %i[title description] | %i[issue3]
'oh' | %i[title] | %i[issue3]
'ah' | %i[description] | %i[issue3]
+
+ '' | %i[title] | %i[issue1 issue2 issue3]
+ %w[a b] | %i[title] | %i[issue1 issue2 issue3]
end
with_them do
@@ -104,14 +107,14 @@ RSpec.describe Gitlab::SQL::Pattern do
end
end
- describe '.select_fuzzy_words' do
- subject(:select_fuzzy_words) { Issue.select_fuzzy_words(query) }
+ describe '.select_fuzzy_terms' do
+ subject(:select_fuzzy_terms) { Issue.select_fuzzy_terms(query) }
context 'with a word equal to 3 chars' do
let(:query) { 'foo' }
- it 'returns array cotaining a word' do
- expect(select_fuzzy_words).to match_array(['foo'])
+ it 'returns array containing a word' do
+ expect(select_fuzzy_terms).to match_array(['foo'])
end
end
@@ -119,7 +122,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'fo' }
it 'returns empty array' do
- expect(select_fuzzy_words).to match_array([])
+ expect(select_fuzzy_terms).to match_array([])
end
end
@@ -127,7 +130,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo baz' }
it 'returns array containing two words' do
- expect(select_fuzzy_words).to match_array(%w[foo baz])
+ expect(select_fuzzy_terms).to match_array(%w[foo baz])
end
end
@@ -135,7 +138,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo baz' }
it 'returns array containing two words' do
- expect(select_fuzzy_words).to match_array(%w[foo baz])
+ expect(select_fuzzy_terms).to match_array(%w[foo baz])
end
end
@@ -143,7 +146,19 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo ba' }
it 'returns array containing a word' do
- expect(select_fuzzy_words).to match_array(['foo'])
+ expect(select_fuzzy_terms).to match_array(['foo'])
+ end
+ end
+ end
+
+ describe '.split_query_to_search_terms' do
+ subject(:split_query_to_search_terms) { described_class.split_query_to_search_terms(query) }
+
+ context 'with words separated by spaces' do
+ let(:query) { 'really bar baz' }
+
+ it 'returns array containing individual words' do
+ expect(split_query_to_search_terms).to match_array(%w[really bar baz])
end
end
@@ -151,15 +166,15 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { '"really bar"' }
it 'returns array containing a multi-word' do
- expect(select_fuzzy_words).to match_array(['really bar'])
+ expect(split_query_to_search_terms).to match_array(['really bar'])
end
end
context 'with a multi-word surrounded by double quote and two words' do
let(:query) { 'foo "really bar" baz' }
- it 'returns array containing a multi-word and tow words' do
- expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz'])
+ it 'returns array containing a multi-word and two words' do
+ expect(split_query_to_search_terms).to match_array(['foo', 'really bar', 'baz'])
end
end
@@ -167,7 +182,7 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { 'foo"really bar"' }
it 'returns array containing two words with double quote' do
- expect(select_fuzzy_words).to match_array(['foo"really', 'bar"'])
+ expect(split_query_to_search_terms).to match_array(['foo"really', 'bar"'])
end
end
@@ -175,15 +190,15 @@ RSpec.describe Gitlab::SQL::Pattern do
let(:query) { '"really bar"baz' }
it 'returns array containing two words with double quote' do
- expect(select_fuzzy_words).to match_array(['"really', 'bar"baz'])
+ expect(split_query_to_search_terms).to match_array(['"really', 'bar"baz'])
end
end
context 'with two multi-word surrounded by double quote and two words' do
let(:query) { 'foo "really bar" baz "awesome feature"' }
- it 'returns array containing two multi-words and tow words' do
- expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz', 'awesome feature'])
+ it 'returns array containing two multi-words and two words' do
+ expect(split_query_to_search_terms).to match_array(['foo', 'really bar', 'baz', 'awesome feature'])
end
end
end
diff --git a/spec/lib/gitlab/tracking/helpers/weak_password_error_event_spec.rb b/spec/lib/gitlab/tracking/helpers/weak_password_error_event_spec.rb
new file mode 100644
index 00000000000..3df10f79e98
--- /dev/null
+++ b/spec/lib/gitlab/tracking/helpers/weak_password_error_event_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Tracking::Helpers::WeakPasswordErrorEvent do
+ let(:user) { build(:user) }
+
+ subject(:helper) { Class.new.include(described_class).new }
+
+ context "when user has a weak password error" do
+ before do
+ user.password = "password"
+ user.valid?
+ end
+
+ it "tracks the event" do
+ helper.track_weak_password_error(user, 'A', 'B')
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'A',
+ method: 'B'
+ )
+ end
+ end
+
+ context "when user does not have a weak password error" do
+ before do
+ user.password = "short"
+ user.valid?
+ end
+
+ it "does not track the event" do
+ helper.track_weak_password_error(user, 'A', 'B')
+ expect_no_snowplow_event
+ end
+ end
+
+ context "when user does not have any errors" do
+ it "does not track the event" do
+ helper.track_weak_password_error(user, 'A', 'B')
+ expect_no_snowplow_event
+ end
+ end
+end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index d4f96f1a37f..2e9a444bd24 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -22,8 +22,8 @@ RSpec.describe Gitlab::UrlBuilder do
:group_board | ->(board) { "/groups/#{board.group.full_path}/-/boards/#{board.id}" }
:commit | ->(commit) { "/#{commit.project.full_path}/-/commit/#{commit.id}" }
:issue | ->(issue) { "/#{issue.project.full_path}/-/issues/#{issue.iid}" }
- [:issue, :task] | ->(issue) { "/#{issue.project.full_path}/-/work_items/#{issue.id}" }
- :work_item | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.id}" }
+ [:issue, :task] | ->(issue) { "/#{issue.project.full_path}/-/work_items/#{issue.iid}?iid_path=true" }
+ :work_item | ->(work_item) { "/#{work_item.project.full_path}/-/work_items/#{work_item.iid}?iid_path=true" }
:merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" }
:project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" }
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/-/snippets/#{snippet.id}" }
@@ -56,6 +56,7 @@ RSpec.describe Gitlab::UrlBuilder do
:discussion_note_on_project_snippet | ->(note) { "/#{note.project.full_path}/-/snippets/#{note.noteable_id}#note_#{note.id}" }
:discussion_note_on_personal_snippet | ->(note) { "/-/snippets/#{note.noteable_id}#note_#{note.id}" }
:note_on_personal_snippet | ->(note) { "/-/snippets/#{note.noteable_id}#note_#{note.id}" }
+ :package | ->(package) { "/#{package.project.full_path}/-/packages/#{package.id}" }
end
with_them do
@@ -71,18 +72,6 @@ RSpec.describe Gitlab::UrlBuilder do
end
end
- context 'when work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'returns an issue path for an issue of type task' do
- task = create(:issue, :task)
-
- expect(subject.build(task, only_path: true)).to eq("/#{task.project.full_path}/-/issues/#{task.iid}")
- end
- end
-
context 'when passing a compare' do
# NOTE: The Compare requires an actual repository, which isn't available
# with the `build_stubbed` strategy used by the table tests above
@@ -196,6 +185,18 @@ RSpec.describe Gitlab::UrlBuilder do
end
end
+ context 'when passing Packages::Package' do
+ let(:package) { build_stubbed(:terraform_module_package) }
+
+ context 'with infrastructure package' do
+ it 'returns the url for infrastucture registry' do
+ url = subject.build(package)
+
+ expect(url).to eq "#{Gitlab.config.gitlab.url}/#{package.project.full_path}/-/infrastructure_registry/#{package.id}"
+ end
+ end
+ end
+
context 'when passing a DesignManagement::Design' do
let(:design) { build_stubbed(:design) }
@@ -226,5 +227,27 @@ RSpec.describe Gitlab::UrlBuilder do
expect(subject.build(object, only_path: true)).to eq("/#{project.full_path}")
end
end
+
+ context 'when use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
+ context 'when a task issue is passed' do
+ it 'returns a path using the work item\'s ID and no query params' do
+ task = create(:issue, :task)
+
+ expect(subject.build(task, only_path: true)).to eq("/#{task.project.full_path}/-/work_items/#{task.id}")
+ end
+ end
+
+ context 'when a work item is passed' do
+ it 'returns a path using the work item\'s ID and no query params' do
+ work_item = create(:work_item)
+
+ expect(subject.build(work_item, only_path: true)).to eq("/#{work_item.project.full_path}/-/work_items/#{work_item.id}")
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index a1bddcb3a47..931340947a2 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -74,13 +74,12 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
end
describe '#with_instrumentation_class' do
- let(:metric_status) { 'active' }
let(:all_definitions) do
metrics_definitions = [
- { key_path: 'metric1', instrumentation_class: 'RedisHLLMetric', status: 'data_available' },
- { key_path: 'metric2', instrumentation_class: 'RedisHLLMetric', status: 'implemented' },
- { key_path: 'metric3', instrumentation_class: 'RedisHLLMetric', status: 'deprecated' },
- { key_path: 'metric4', instrumentation_class: 'RedisHLLMetric', status: metric_status },
+ { key_path: 'metric1', instrumentation_class: 'RedisHLLMetric', status: 'active' },
+ { key_path: 'metric2', instrumentation_class: 'RedisHLLMetric', status: 'broken' },
+ { key_path: 'metric3', instrumentation_class: 'RedisHLLMetric', status: 'active' },
+ { key_path: 'metric4', instrumentation_class: 'RedisHLLMetric', status: 'removed' },
{ key_path: 'metric5', status: 'active' },
{ key_path: 'metric_missing_status' }
]
@@ -92,7 +91,7 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
end
it 'includes definitions with instrumentation_class' do
- expect(described_class.with_instrumentation_class.count).to eq(4)
+ expect(described_class.with_instrumentation_class.count).to eq(3)
end
context 'with removed metric' do
@@ -135,8 +134,9 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
:repair_issue_url | nil
:removed_by_url | 1
- :instrumentation_class | 'Metric_Class'
- :instrumentation_class | 'metricClass'
+ :performance_indicator_type | nil
+ :instrumentation_class | 'Metric_Class'
+ :instrumentation_class | 'metricClass'
end
with_them do
@@ -201,9 +201,9 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
using RSpec::Parameterized::TableSyntax
where(:status, :skip_validation?) do
- 'deprecated' | true
- 'removed' | true
'active' | false
+ 'broken' | false
+ 'removed' | true
end
with_them do
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
index 76eec2755df..1f00f7bbec3 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -3,403 +3,133 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redis_shared_state do
- let(:entity1) { 'dfb9d2d2-f56c-4c77-8aeb-6cddc4a1f857' }
- let(:entity2) { '1dd9afb2-a3ee-4de1-8ae3-a405579c8584' }
- let(:entity3) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' }
- let(:entity4) { '8b9a2671-2abf-4bec-a682-22f6a8f7bf31' }
let(:end_date) { Date.current }
- let(:sources) { Gitlab::Usage::Metrics::Aggregates::Sources }
let(:namespace) { described_class.to_s.deconstantize.constantize }
+ let(:sources) { Gitlab::Usage::Metrics::Aggregates::Sources }
let_it_be(:recorded_at) { Time.current.to_i }
- def aggregated_metric(name:, time_frame:, source: "redis", events: %w[event1 event2 event3], operator: "OR", feature_flag: nil)
- {
- name: name,
- source: source,
- events: events,
- operator: operator,
- time_frame: time_frame,
- feature_flag: feature_flag
- }.compact.with_indifferent_access
- end
-
- context 'aggregated_metrics_data' do
- shared_examples 'aggregated_metrics_data' do
- context 'no aggregated metric is defined' do
- it 'returns empty hash' do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics).and_return([])
- end
-
- expect(aggregated_metrics_data).to eq({})
- end
+ describe '.calculate_count_for_aggregation' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid configuration' do
+ where(:number_of_days, :operator, :datasource, :expected_method) do
+ 28 | 'AND' | 'redis_hll' | :calculate_metrics_intersections
+ 7 | 'AND' | 'redis_hll' | :calculate_metrics_intersections
+ 28 | 'AND' | 'database' | :calculate_metrics_intersections
+ 7 | 'AND' | 'database' | :calculate_metrics_intersections
+ 28 | 'OR' | 'redis_hll' | :calculate_metrics_union
+ 7 | 'OR' | 'redis_hll' | :calculate_metrics_union
+ 28 | 'OR' | 'database' | :calculate_metrics_union
+ 7 | 'OR' | 'database' | :calculate_metrics_union
end
- context 'there are aggregated metrics defined' do
- let(:aggregated_metrics) do
- [
- aggregated_metric(name: "gmau_1", source: datasource, time_frame: time_frame, operator: operator)
- ]
- end
-
- let(:results) { { 'gmau_1' => 5 } }
+ with_them do
+ let(:time_frame) { "#{number_of_days}d" }
+ let(:start_date) { number_of_days.days.ago.to_date }
let(:params) { { start_date: start_date, end_date: end_date, recorded_at: recorded_at } }
-
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
- end
- end
-
- context 'with OR operator' do
- let(:operator) { Gitlab::Usage::Metrics::Aggregates::UNION_OF_AGGREGATED_METRICS }
-
- it 'returns the number of unique events occurred for any metric in aggregate', :aggregate_failures do
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(5)
- expect(aggregated_metrics_data).to eq(results)
- end
+ let(:aggregate) do
+ {
+ source: datasource,
+ operator: operator,
+ events: %w[event1 event2]
+ }
end
- context 'with AND operator' do
- let(:operator) { Gitlab::Usage::Metrics::Aggregates::INTERSECTION_OF_AGGREGATED_METRICS }
-
- it 'returns the number of unique events that occurred for all of metrics in the aggregate', :aggregate_failures do
- expect(namespace::SOURCES[datasource]).to receive(:calculate_metrics_intersections).with(params.merge(metric_names: %w[event1 event2 event3])).and_return(5)
- expect(aggregated_metrics_data).to eq(results)
- end
- end
-
- context 'hidden behind feature flag' do
- let(:enabled_feature_flag) { 'test_ff_enabled' }
- let(:disabled_feature_flag) { 'test_ff_disabled' }
- let(:aggregated_metrics) do
- params = { source: datasource, time_frame: time_frame }
- [
- # represents stable aggregated metrics that has been fully released
- aggregated_metric(**params.merge(name: "gmau_without_ff")),
- # represents new aggregated metric that is under performance testing on gitlab.com
- aggregated_metric(**params.merge(name: "gmau_enabled", feature_flag: enabled_feature_flag)),
- # represents aggregated metric that is under development and shouldn't be yet collected even on gitlab.com
- aggregated_metric(**params.merge(name: "gmau_disabled", feature_flag: disabled_feature_flag))
- ]
- end
-
- it 'does not calculate data for aggregates with ff turned off' do
- skip_feature_flags_yaml_validation
- skip_default_enabled_yaml_check
- stub_feature_flags(enabled_feature_flag => true, disabled_feature_flag => false)
- allow(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).and_return(6)
-
- expect(aggregated_metrics_data).to eq('gmau_without_ff' => 6, 'gmau_enabled' => 6)
- end
- end
- end
-
- context 'error handling' do
- context 'development and test environment' do
- it 'raises error when unknown aggregation operator is used' do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics)
- .and_return([aggregated_metric(name: 'gmau_1', source: datasource, operator: "SUM", time_frame: time_frame)])
- end
-
- expect { aggregated_metrics_data }.to raise_error namespace::UnknownAggregationOperator
- end
-
- it 'raises error when unknown aggregation source is used' do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics)
- .and_return([aggregated_metric(name: 'gmau_1', source: 'whoami', time_frame: time_frame)])
- end
-
- expect { aggregated_metrics_data }.to raise_error namespace::UnknownAggregationSource
- end
-
- it 'raises error when union is missing' do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics)
- .and_return([aggregated_metric(name: 'gmau_1', source: datasource, time_frame: time_frame)])
- end
- allow(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).and_raise(sources::UnionNotAvailable)
-
- expect { aggregated_metrics_data }.to raise_error sources::UnionNotAvailable
- end
+ subject(:calculate_count_for_aggregation) do
+ described_class
+ .new(recorded_at)
+ .calculate_count_for_aggregation(aggregation: aggregate, time_frame: time_frame)
end
- context 'production' do
- before do
- stub_rails_env('production')
- end
-
- it 'rescues unknown aggregation operator error' do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics)
- .and_return([aggregated_metric(name: 'gmau_1', source: datasource, operator: "SUM", time_frame: time_frame)])
- end
-
- expect(aggregated_metrics_data).to eq('gmau_1' => -1)
- end
-
- it 'rescues unknown aggregation source error' do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics)
- .and_return([aggregated_metric(name: 'gmau_1', source: 'whoami', time_frame: time_frame)])
- end
-
- expect(aggregated_metrics_data).to eq('gmau_1' => -1)
- end
-
- it 'rescues error when union is missing' do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics)
- .and_return([aggregated_metric(name: 'gmau_1', source: datasource, time_frame: time_frame)])
- end
- allow(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).and_raise(sources::UnionNotAvailable)
-
- expect(aggregated_metrics_data).to eq('gmau_1' => -1)
- end
+ it 'returns the number of unique events for aggregation', :aggregate_failures do
+ expect(namespace::SOURCES[datasource])
+ .to receive(expected_method)
+ .with(params.merge(metric_names: %w[event1 event2]))
+ .and_return(5)
+ expect(calculate_count_for_aggregation).to eq(5)
end
end
end
- shared_examples 'database_sourced_aggregated_metrics' do
- let(:datasource) { namespace::DATABASE_SOURCE }
-
- it_behaves_like 'aggregated_metrics_data'
- end
-
- shared_examples 'redis_sourced_aggregated_metrics' do
- let(:datasource) { namespace::REDIS_SOURCE }
-
- it_behaves_like 'aggregated_metrics_data' do
- context 'error handling' do
- let(:aggregated_metrics) { [aggregated_metric(name: 'gmau_1', source: datasource, time_frame: time_frame)] }
- let(:error) { Gitlab::UsageDataCounters::HLLRedisCounter::EventError }
-
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
- end
- allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:calculate_events_union).and_raise(error)
- end
-
- context 'development and test environment' do
- it 're raises Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do
- expect { aggregated_metrics_data }.to raise_error error
- end
- end
-
- context 'production' do
- it 'rescues Gitlab::UsageDataCounters::HLLRedisCounter::EventError' do
- stub_rails_env('production')
-
- expect(aggregated_metrics_data).to eq('gmau_1' => -1)
- end
- end
- end
+ context 'with invalid configuration' do
+ where(:time_frame, :operator, :datasource, :expected_error) do
+ '28d' | 'SUM' | 'redis_hll' | namespace::UnknownAggregationOperator
+ '7d' | 'AND' | 'mongodb' | namespace::UnknownAggregationSource
+ 'all' | 'AND' | 'redis_hll' | namespace::DisallowedAggregationTimeFrame
end
- end
-
- describe '.aggregated_metrics_all_time_data' do
- subject(:aggregated_metrics_data) { described_class.new(recorded_at).all_time_data }
- let(:start_date) { nil }
- let(:end_date) { nil }
- let(:time_frame) { ['all'] }
-
- it_behaves_like 'database_sourced_aggregated_metrics'
-
- context 'redis sourced aggregated metrics' do
- let(:aggregated_metrics) { [aggregated_metric(name: 'gmau_1', time_frame: time_frame)] }
+ with_them do
+ let(:aggregate) do
+ {
+ source: datasource,
+ operator: operator,
+ events: %w[event1 event2]
+ }
+ end
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:aggregated_metrics).and_return(aggregated_metrics)
- end
+ subject(:calculate_count_for_aggregation) do
+ described_class
+ .new(recorded_at)
+ .calculate_count_for_aggregation(aggregation: aggregate, time_frame: time_frame)
end
- context 'development and test environment' do
- it 'raises Gitlab::Usage::Metrics::Aggregates::DisallowedAggregationTimeFrame' do
- expect { aggregated_metrics_data }.to raise_error namespace::DisallowedAggregationTimeFrame
+ context 'with non prod environment' do
+ it 'raises error' do
+ expect { calculate_count_for_aggregation }.to raise_error expected_error
end
end
- context 'production env' do
- it 'returns fallback value for unsupported time frame' do
+ context 'with prod environment' do
+ before do
stub_rails_env('production')
+ end
- expect(aggregated_metrics_data).to eq('gmau_1' => -1)
+ it 'returns fallback value' do
+ expect(calculate_count_for_aggregation).to be(-1)
end
end
end
end
- context 'legacy aggregated metrics configuration' do
- let(:temp_dir) { Dir.mktmpdir }
- let(:temp_file) { Tempfile.new(%w[common .yml], temp_dir) }
-
- before do
- stub_const("#{namespace}::AGGREGATED_METRICS_PATH", File.expand_path('*.yml', temp_dir))
- File.open(temp_file.path, "w+b") do |file|
- file.write [aggregated_metric(name: "gmau_1", time_frame: '7d')].to_yaml
- end
- end
-
- after do
- temp_file.unlink
- FileUtils.remove_entry(temp_dir) if Dir.exist?(temp_dir)
+ context 'when union data is not available' do
+ subject(:calculate_count_for_aggregation) do
+ described_class
+ .new(recorded_at)
+ .calculate_count_for_aggregation(aggregation: aggregate, time_frame: time_frame)
end
- it 'allows for YAML aliases in aggregated metrics configs' do
- expect(YAML).to receive(:safe_load).with(kind_of(String), aliases: true).at_least(:once)
-
- described_class.new(recorded_at)
+ where(:time_frame, :operator, :datasource) do
+ '28d' | 'OR' | 'redis_hll'
+ '7d' | 'OR' | 'database'
end
- end
-
- describe '.aggregated_metrics_weekly_data' do
- subject(:aggregated_metrics_data) { described_class.new(recorded_at).weekly_data }
- let(:start_date) { 7.days.ago.to_date }
- let(:time_frame) { ['7d'] }
-
- it_behaves_like 'database_sourced_aggregated_metrics'
- it_behaves_like 'redis_sourced_aggregated_metrics'
- end
-
- describe '.aggregated_metrics_monthly_data' do
- subject(:aggregated_metrics_data) { described_class.new(recorded_at).monthly_data }
-
- let(:start_date) { 4.weeks.ago.to_date }
- let(:time_frame) { ['28d'] }
-
- it_behaves_like 'database_sourced_aggregated_metrics'
- it_behaves_like 'redis_sourced_aggregated_metrics'
- end
-
- describe '.calculate_count_for_aggregation' do
- using RSpec::Parameterized::TableSyntax
-
- context 'with valid configuration' do
- where(:number_of_days, :operator, :datasource, :expected_method) do
- 28 | 'AND' | 'redis' | :calculate_metrics_intersections
- 7 | 'AND' | 'redis' | :calculate_metrics_intersections
- 28 | 'AND' | 'database' | :calculate_metrics_intersections
- 7 | 'AND' | 'database' | :calculate_metrics_intersections
- 28 | 'OR' | 'redis' | :calculate_metrics_union
- 7 | 'OR' | 'redis' | :calculate_metrics_union
- 28 | 'OR' | 'database' | :calculate_metrics_union
- 7 | 'OR' | 'database' | :calculate_metrics_union
- end
-
- with_them do
- let(:time_frame) { "#{number_of_days}d" }
- let(:start_date) { number_of_days.days.ago.to_date }
- let(:params) { { start_date: start_date, end_date: end_date, recorded_at: recorded_at } }
- let(:aggregate) do
- {
- source: datasource,
- operator: operator,
- events: %w[event1 event2]
- }
- end
-
- subject(:calculate_count_for_aggregation) do
- described_class
- .new(recorded_at)
- .calculate_count_for_aggregation(aggregation: aggregate, time_frame: time_frame)
- end
-
- it 'returns the number of unique events for aggregation', :aggregate_failures do
- expect(namespace::SOURCES[datasource])
- .to receive(expected_method)
- .with(params.merge(metric_names: %w[event1 event2]))
- .and_return(5)
- expect(calculate_count_for_aggregation).to eq(5)
- end
+ with_them do
+ before do
+ allow(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).and_raise(sources::UnionNotAvailable)
end
- end
- context 'with invalid configuration' do
- where(:time_frame, :operator, :datasource, :expected_error) do
- '28d' | 'SUM' | 'redis' | namespace::UnknownAggregationOperator
- '7d' | 'AND' | 'mongodb' | namespace::UnknownAggregationSource
- 'all' | 'AND' | 'redis' | namespace::DisallowedAggregationTimeFrame
+ let(:aggregate) do
+ {
+ source: datasource,
+ operator: operator,
+ events: %w[event1 event2]
+ }
end
- with_them do
- let(:aggregate) do
- {
- source: datasource,
- operator: operator,
- events: %w[event1 event2]
- }
- end
-
- subject(:calculate_count_for_aggregation) do
- described_class
- .new(recorded_at)
- .calculate_count_for_aggregation(aggregation: aggregate, time_frame: time_frame)
- end
-
- context 'with non prod environment' do
- it 'raises error' do
- expect { calculate_count_for_aggregation }.to raise_error expected_error
- end
- end
-
- context 'with prod environment' do
- before do
- stub_rails_env('production')
- end
-
- it 'returns fallback value' do
- expect(calculate_count_for_aggregation).to be(-1)
- end
+ context 'with non prod environment' do
+ it 'raises error' do
+ expect { calculate_count_for_aggregation }.to raise_error sources::UnionNotAvailable
end
end
- end
-
- context 'when union data is not available' do
- subject(:calculate_count_for_aggregation) do
- described_class
- .new(recorded_at)
- .calculate_count_for_aggregation(aggregation: aggregate, time_frame: time_frame)
- end
- where(:time_frame, :operator, :datasource) do
- '28d' | 'OR' | 'redis'
- '7d' | 'OR' | 'database'
- end
-
- with_them do
+ context 'with prod environment' do
before do
- allow(namespace::SOURCES[datasource]).to receive(:calculate_metrics_union).and_raise(sources::UnionNotAvailable)
- end
-
- let(:aggregate) do
- {
- source: datasource,
- operator: operator,
- events: %w[event1 event2]
- }
- end
-
- context 'with non prod environment' do
- it 'raises error' do
- expect { calculate_count_for_aggregation }.to raise_error sources::UnionNotAvailable
- end
+ stub_rails_env('production')
end
- context 'with prod environment' do
- before do
- stub_rails_env('production')
- end
-
- it 'returns fallback value' do
- expect(calculate_count_for_aggregation).to be(-1)
- end
+ it 'returns fallback value' do
+ expect(calculate_count_for_aggregation).to be(-1)
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb
new file mode 100644
index 00000000000..92459e92eac
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_merge_request_authors_metric_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountMergeRequestAuthorsMetric do
+ let(:expected_value) { 1 }
+ let(:start) { 30.days.ago.to_s(:db) }
+ let(:finish) { 2.days.ago.to_s(:db) }
+
+ let(:expected_query) do
+ "SELECT COUNT(DISTINCT \"merge_requests\".\"author_id\") FROM \"merge_requests\"" \
+ " WHERE \"merge_requests\".\"created_at\" BETWEEN '#{start}' AND '#{finish}'"
+ end
+
+ before do
+ user = create(:user)
+ user2 = create(:user)
+
+ create(:merge_request, created_at: 1.year.ago, author: user)
+ create(:merge_request, created_at: 1.week.ago, author: user2)
+ create(:merge_request, created_at: 1.week.ago, author: user2)
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', { time_frame: '28d' }
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
index f73155642d6..f1ecc8c8ab5 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb
@@ -3,12 +3,14 @@
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
+ let(:database_metric_class) { Class.new(described_class) }
+
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :count
- metric_class.start { metric_class.relation.minimum(:id) }
- metric_class.finish { metric_class.relation.maximum(:id) }
+ metric_class.start { Issue.minimum(:id) }
+ metric_class.finish { Issue.maximum(:id) }
end.new(time_frame: 'all')
end
@@ -38,11 +40,11 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with metric options specified with custom batch_size' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :count
- metric_class.start { metric_class.relation.minimum(:id) }
- metric_class.finish { metric_class.relation.maximum(:id) }
+ metric_class.start { Issue.minimum(:id) }
+ metric_class.finish { Issue.maximum(:id) }
metric_class.metric_options { { batch_size: 12345 } }
end.new(time_frame: 'all')
end
@@ -60,7 +62,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with start and finish not called' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :count
end.new(time_frame: 'all')
@@ -73,7 +75,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with availability defined' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :count
metric_class.available? { false }
@@ -87,7 +89,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with availability not defined' do
subject do
- Class.new(described_class) do
+ database_metric_class do
relation { Issue }
operation :count
end.new(time_frame: 'all')
@@ -100,11 +102,11 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with cache_start_and_finish_as called' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :count
- metric_class.start { metric_class.relation.minimum(:id) }
- metric_class.finish { metric_class.relation.maximum(:id) }
+ metric_class.start { Issue.minimum(:id) }
+ metric_class.finish { Issue.maximum(:id) }
metric_class.cache_start_and_finish_as :special_issue_count
end.new(time_frame: 'all')
end
@@ -123,11 +125,11 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with estimate_batch_distinct_count' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation(:estimate_batch_distinct_count)
- metric_class.start { metric_class.relation.minimum(:id) }
- metric_class.finish { metric_class.relation.maximum(:id) }
+ metric_class.start { Issue.minimum(:id) }
+ metric_class.finish { Issue.maximum(:id) }
end.new(time_frame: 'all')
end
@@ -139,13 +141,13 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
let(:buckets) { double('Buckets').as_null_object }
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation(:estimate_batch_distinct_count) do |result|
result.foo
end
- metric_class.start { metric_class.relation.minimum(:id) }
- metric_class.finish { metric_class.relation.maximum(:id) }
+ metric_class.start { Issue.minimum(:id) }
+ metric_class.finish { Issue.maximum(:id) }
end.new(time_frame: 'all')
end
@@ -163,7 +165,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with custom timestamp column' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :count
metric_class.timestamp_column :last_edited_at
@@ -171,6 +173,7 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
end
it 'calculates a correct result' do
+ create(:issue, last_edited_at: 40.days.ago)
create(:issue, last_edited_at: 5.days.ago)
expect(subject.value).to eq(1)
@@ -179,24 +182,40 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric do
context 'with default timestamp column' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :count
end.new(time_frame: '28d')
end
it 'calculates a correct result' do
- create(:issue, last_edited_at: 5.days.ago)
+ create(:issue, created_at: 40.days.ago)
create(:issue, created_at: 5.days.ago)
expect(subject.value).to eq(1)
end
end
+
+ context 'with additional parameters passed via options' do
+ subject do
+ database_metric_class.tap do |metric_class|
+ metric_class.relation ->(options) { Issue.where(confidential: options[:confidential]) }
+ metric_class.operation :count
+ end.new(time_frame: '28d', options: { confidential: true })
+ end
+
+ it 'calculates a correct result' do
+ create(:issue, created_at: 5.days.ago, confidential: true)
+ create(:issue, created_at: 5.days.ago, confidential: false)
+
+ expect(subject.value).to eq(1)
+ end
+ end
end
context 'with unimplemented operation method used' do
subject do
- described_class.tap do |metric_class|
+ database_metric_class.tap do |metric_class|
metric_class.relation { Issue }
metric_class.operation :invalid_operation
end.new(time_frame: 'all')
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_disabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_disabled_metric_spec.rb
deleted file mode 100644
index 757adee6117..00000000000
--- a/spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_disabled_metric_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DistinctCountProjectsWithExpirationPolicyDisabledMetric do
- before_all do
- create(:container_expiration_policy, enabled: false)
- create(:container_expiration_policy, enabled: false, created_at: 29.days.ago)
- create(:container_expiration_policy, enabled: true)
- end
-
- it_behaves_like 'a correct instrumented metric value', { time_frame: '28d' } do
- let(:expected_value) { 1 }
- end
-
- it_behaves_like 'a correct instrumented metric value', { time_frame: 'all' } do
- let(:expected_value) { 2 }
- end
-end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_metric_spec.rb
new file mode 100644
index 00000000000..a1ca658a0d7
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/distinct_count_projects_with_expiration_policy_metric_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DistinctCountProjectsWithExpirationPolicyMetric do
+ before_all do
+ create(:container_expiration_policy, enabled: false)
+ create(:container_expiration_policy, enabled: false, created_at: 29.days.ago)
+ create(:container_expiration_policy, keep_n: nil)
+ create(:container_expiration_policy, keep_n: 5, enabled: true)
+ create(:container_expiration_policy, keep_n: 5, enabled: true)
+ create(:container_expiration_policy, keep_n: 5, enabled: true)
+ create(:container_expiration_policy, older_than: '7d')
+ create(:container_expiration_policy, cadence: '14d')
+ create(:container_expiration_policy, enabled: true)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', options: { enabled: false } } do
+ let(:expected_value) { 1 }
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', options: { enabled: false } } do
+ let(:expected_value) { 2 }
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', options: { keep_n: 5, enabled: true } } do
+ let(:expected_value) { 3 }
+ end
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', options: { cadence: '14d' } } do
+ let(:expected_value) { 1 }
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_period_setting_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_period_setting_metric_spec.rb
new file mode 100644
index 00000000000..a63616aeb48
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_period_setting_metric_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DormantUserPeriodSettingMetric do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:deactivate_dormant_users_period_value, :expected_value) do
+ 90 | 90 # default
+ 365 | 365
+ end
+
+ with_them do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ stub_application_setting(deactivate_dormant_users_period: deactivate_dormant_users_period_value)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', {}
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_setting_enabled_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_setting_enabled_metric_spec.rb
new file mode 100644
index 00000000000..5c8ca502f82
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/dormant_user_setting_enabled_metric_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::DormantUserSettingEnabledMetric do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:deactivate_dormant_users_enabled, :expected_value) do
+ 1 | 1
+ 0 | 0
+ end
+
+ with_them do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ stub_application_setting(deactivate_dormant_users: deactivate_dormant_users_enabled)
+ end
+
+ it_behaves_like 'a correct instrumented metric value', {}
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb
new file mode 100644
index 00000000000..cb94da11d58
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_cta_clicked_metric_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InProductMarketingEmailCtaClickedMetric do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:email_attributes) { { cta_clicked_at: Date.yesterday, track: 'verify', series: 0 } }
+ let(:options) { { track: 'verify', series: 0 } }
+ let(:expected_value) { 2 }
+ let(:expected_query) do
+ 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails"' \
+ ' WHERE "in_product_marketing_emails"."cta_clicked_at" IS NOT NULL' \
+ ' AND "in_product_marketing_emails"."series" = 0'\
+ ' AND "in_product_marketing_emails"."track" = 1'
+ end
+
+ before do
+ create_list :in_product_marketing_email, 2, email_attributes
+
+ create :in_product_marketing_email, email_attributes.merge(cta_clicked_at: nil)
+ create :in_product_marketing_email, email_attributes.merge(track: 'team')
+ create :in_product_marketing_email, email_attributes.merge(series: 1)
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', {
+ options: { track: 'verify', series: 0 },
+ time_frame: 'all'
+ }
+
+ where(:options_key, :valid_value, :invalid_value) do
+ :track | 'admin_verify' | 'invite_team'
+ :series | 1 | 5
+ end
+
+ with_them do
+ it "raises an exception if option is not present" do
+ expect do
+ described_class.new(options: options.except(options_key), time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of})
+ end
+
+ it "raises an exception if option has invalid value" do
+ expect do
+ options[options_key] = invalid_value
+ described_class.new(options: options, time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of})
+ end
+
+ it "doesn't raise exceptions if option has valid value" do
+ options[options_key] = valid_value
+ described_class.new(options: options, time_frame: 'all')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb
new file mode 100644
index 00000000000..0cc82773d56
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/in_product_marketing_email_sent_metric_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::InProductMarketingEmailSentMetric do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:email_attributes) { { track: 'verify', series: 0 } }
+ let(:expected_value) { 2 }
+ let(:expected_query) do
+ 'SELECT COUNT("in_product_marketing_emails"."id") FROM "in_product_marketing_emails"' \
+ ' WHERE "in_product_marketing_emails"."series" = 0'\
+ ' AND "in_product_marketing_emails"."track" = 1'
+ end
+
+ before do
+ create_list :in_product_marketing_email, 2, email_attributes
+
+ create :in_product_marketing_email, email_attributes.merge(track: 'team')
+ create :in_product_marketing_email, email_attributes.merge(series: 1)
+ end
+
+ it_behaves_like 'a correct instrumented metric value and query', {
+ options: { track: 'verify', series: 0 },
+ time_frame: 'all'
+ }
+
+ where(:options_key, :valid_value, :invalid_value) do
+ :track | 'admin_verify' | 'invite_team'
+ :series | 1 | 5
+ end
+
+ with_them do
+ it "raises an exception if option is not present" do
+ expect do
+ described_class.new(options: email_attributes.except(options_key), time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of})
+ end
+
+ it "raises an exception if option has invalid value" do
+ expect do
+ email_attributes[options_key] = invalid_value
+ described_class.new(options: email_attributes, time_frame: 'all')
+ end.to raise_error(ArgumentError, %r{#{options_key} .* must be one of})
+ end
+
+ it "doesn't raise exceptions if option has valid value" do
+ email_attributes[options_key] = valid_value
+ described_class.new(options: email_attributes, time_frame: 'all')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb
index 3e315692d0a..35e5d7f2796 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::WorkItemsActivityAggreg
users_creating_work_items
users_updating_work_item_title
users_updating_work_item_dates
+ users_updating_work_item_labels
+ users_updating_work_item_milestone
users_updating_work_item_iteration
]
}
@@ -44,16 +46,26 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::WorkItemsActivityAggreg
describe '#value', :clean_gitlab_redis_shared_state do
let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
+ let(:author1_id) { 1 }
+ let(:author2_id) { 2 }
+ let(:event_time) { 1.week.ago }
before do
- counter.track_event(:users_creating_work_items, values: 1, time: 1.week.ago)
- counter.track_event(:users_updating_work_item_title, values: 1, time: 1.week.ago)
- counter.track_event(:users_updating_work_item_dates, values: 2, time: 1.week.ago)
- counter.track_event(:users_updating_work_item_iteration, values: 2, time: 1.week.ago)
+ counter.track_event(:users_creating_work_items, values: author1_id, time: event_time)
end
- it 'has correct value' do
- expect(described_class.new(metric_definition).value).to eq 2
+ it 'has correct value after events are tracked', :aggregate_failures do
+ expect do
+ counter.track_event(:users_updating_work_item_title, values: author1_id, time: event_time)
+ counter.track_event(:users_updating_work_item_dates, values: author1_id, time: event_time)
+ counter.track_event(:users_updating_work_item_labels, values: author1_id, time: event_time)
+ counter.track_event(:users_updating_work_item_milestone, values: author1_id, time: event_time)
+ end.to not_change { described_class.new(metric_definition).value }
+
+ expect do
+ counter.track_event(:users_updating_work_item_iteration, values: author2_id, time: event_time)
+ counter.track_event(:users_updating_weight_estimate, values: author1_id, time: event_time)
+ end.to change { described_class.new(metric_definition).value }.from(1).to(2)
end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
index f9cd6e88e0a..24107727a8e 100644
--- a/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb
@@ -63,7 +63,6 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do
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 }
@@ -74,7 +73,6 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do
context 'for average metrics' do
it_behaves_like 'name suggestion' do
# corresponding metric is collected with average(Ci::Pipeline, :duration)
- let(:key_path) { 'counts.ci_pipeline_duration' }
let(:operation) { :average }
let(:relation) { Ci::Pipeline }
let(:column) { :duration }
@@ -100,5 +98,16 @@ RSpec.describe Gitlab::Usage::Metrics::NameSuggestion do
let(:name_suggestion) { /<please fill metric name>/ }
end
end
+
+ context 'for metrics with `having` keyword' do
+ it_behaves_like 'name suggestion' do
+ let(:operation) { :count }
+ let(:relation) { Issue.with_alert_management_alerts.having('COUNT(alert_management_alerts) > 1').group(:id) }
+
+ let(:column) { nil }
+ let(:constraints) { /<adjective describing: '\(\(COUNT\(alert_management_alerts\) > 1\)\)'>/ }
+ let(:name_suggestion) { /count_#{constraints}_issues_<with>_alert_management_alerts/ }
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/having_constraints_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/having_constraints_spec.rb
new file mode 100644
index 00000000000..492acf2a902
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/having_constraints_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::HavingConstraints do
+ describe '#accept' do
+ let(:connection) { ApplicationRecord.connection }
+ let(:collector) { Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new) }
+
+ it 'builds correct constraints description' do
+ table = Arel::Table.new('records')
+ havings = table[:attribute].sum.eq(6).and(table[:attribute].count.gt(5))
+ arel = table.from.project(table['id'].count).having(havings).group(table[:attribute2])
+ described_class.new(connection).accept(arel, collector)
+
+ expect(collector.value).to eql '(SUM(records.attribute) = 6 AND COUNT(records.attribute) > 5)'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/constraints_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/where_constraints_spec.rb
index 68016e760e4..42a776478a4 100644
--- a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/constraints_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/where_constraints_spec.rb
@@ -2,14 +2,15 @@
require 'spec_helper'
-RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Constraints do
+RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::WhereConstraints do
describe '#accept' do
- let(:collector) { Arel::Collectors::SubstituteBinds.new(ActiveRecord::Base.connection, Arel::Collectors::SQLString.new) }
+ let(:connection) { ApplicationRecord.connection }
+ let(:collector) { Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new) }
it 'builds correct constraints description' do
table = Arel::Table.new('records')
arel = table.from.project(table['id'].count).where(table[:attribute].eq(true).and(table[:some_value].gt(5)))
- described_class.new(ApplicationRecord.connection).accept(arel, collector)
+ described_class.new(connection).accept(arel, collector)
expect(collector.value).to eql '(records.attribute = true AND records.some_value > 5)'
end
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index 1ca0bb0e9ea..f1115a8813d 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -12,6 +12,10 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
shared_examples 'tracks template' do
let(:subject) { described_class.track_unique_project_event(project: project, template: template_path, config_source: config_source, user: user) }
+ let(:template_name) do
+ expanded_template_name = described_class.expand_template_name(template_path)
+ described_class.ci_template_event_name(expanded_template_name, config_source)
+ end
it "has an event defined for template" do
expect do
@@ -20,33 +24,18 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
end
it "tracks template" do
- expanded_template_name = described_class.expand_template_name(template_path)
- expected_template_event_name = described_class.ci_template_event_name(expanded_template_name, config_source)
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(receive(:track_event)).with(expected_template_event_name, values: project.id)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(receive(:track_event)).with(template_name, values: project.id)
subject
end
- context 'Snowplow' do
- it 'event is not tracked if FF is disabled' do
- stub_feature_flags(route_hll_to_snowplow: false)
-
- subject
-
- expect_no_snowplow_event
- end
-
- it 'tracks event' do
- subject
-
- expect_snowplow_event(
- category: described_class.to_s,
- action: 'ci_templates_unique',
- namespace: project.namespace,
- user: user,
- project: project
- )
- end
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow }
+ let(:category) { described_class.to_s }
+ let(:action) { 'ci_templates_unique' }
+ let(:namespace) { project.namespace }
+ let(:label) { 'redis_hll_counters.ci_templates.ci_templates_total_unique_counts_monthly' }
+ let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: template_name).to_context] }
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index d0b935d59dd..08c712889a8 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
# Without freezing the time, the test may behave inconsistently
# depending on which day of the week test is run.
# Monday 6th of June
+ described_class.clear_memoization(:known_events)
reference_time = Time.utc(2020, 6, 1)
travel_to(reference_time) { example.run }
described_class.clear_memoization(:known_events)
diff --git a/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb
index e7edb8b9cf1..ced9ec7f221 100644
--- a/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/kubernetes_agent_counter_spec.rb
@@ -26,6 +26,12 @@ RSpec.describe Gitlab::UsageDataCounters::KubernetesAgentCounter do
expect(described_class.totals).to eq(kubernetes_agent_gitops_sync: 3, kubernetes_agent_k8s_api_proxy_request: 6)
end
+ context 'with empty events' do
+ let(:events) { nil }
+
+ it { expect { subject }.not_to change(described_class, :totals) }
+ end
+
context 'event is unknown' do
let(:events) do
{
diff --git a/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb
index 2d251017c87..aaf509b6f81 100644
--- a/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/work_item_activity_unique_counter_spec.rb
@@ -36,4 +36,12 @@ RSpec.describe Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter, :clean_
it_behaves_like 'work item unique counter'
end
+
+ describe '.track_work_item_milestone_changed_action' do
+ subject(:track_event) { described_class.track_work_item_milestone_changed_action(author: user) }
+
+ let(:event_name) { described_class::WORK_ITEM_MILESTONE_CHANGED }
+
+ it_behaves_like 'work item unique counter'
+ end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index cb645ae3e53..d8f50fa27bb 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -33,8 +33,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
.not_to include(:merge_requests_users)
expect(subject[:usage_activity_by_stage_monthly][:create])
.to include(:merge_requests_users)
- expect(subject[:counts_weekly]).to include(:aggregated_metrics)
- expect(subject[:counts_monthly]).to include(:aggregated_metrics)
end
it 'clears memoized values' do
@@ -608,13 +606,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
- %i[keep_n cadence older_than].each do |attribute|
- ContainerExpirationPolicy.send("#{attribute}_options").keys.each do |value|
- let_it_be("container_expiration_policy_with_#{attribute}_set_to_#{value}") { create(:container_expiration_policy, attribute => value) }
- end
+ ::ContainerExpirationPolicy.older_than_options.keys.each do |value|
+ let_it_be("container_expiration_policy_with_older_than_set_to_#{value}") { create(:container_expiration_policy, older_than: value) }
end
- let_it_be('container_expiration_policy_with_keep_n_set_to_null') { create(:container_expiration_policy, keep_n: nil) }
let_it_be('container_expiration_policy_with_older_than_set_to_null') { create(:container_expiration_policy, older_than: nil) }
let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
@@ -623,27 +618,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.data[:counts] }
it 'gathers usage data' do
- expect(subject[:projects_with_expiration_policy_enabled]).to eq 19
-
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 13
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
-
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_60d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 14
-
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 15
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_3month]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 2
end
end
@@ -757,33 +737,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- describe '.usage_data_counters' do
- subject { described_class.usage_data_counters }
-
- it { is_expected.to all(respond_to :totals) }
- it { is_expected.to all(respond_to :fallback_totals) }
-
- describe 'the results of calling #totals on all objects in the array' do
- subject { described_class.usage_data_counters.map(&:totals) }
-
- it { is_expected.to all(be_a Hash) }
- it { is_expected.to all(have_attributes(keys: all(be_a Symbol), values: all(be_a Integer))) }
- end
-
- describe 'the results of calling #fallback_totals on all objects in the array' do
- subject { described_class.usage_data_counters.map(&:fallback_totals) }
-
- it { is_expected.to all(be_a Hash) }
- it { is_expected.to all(have_attributes(keys: all(be_a Symbol), values: all(eq(-1)))) }
- end
-
- it 'does not have any conflicts' do
- all_keys = subject.flat_map { |counter| counter.totals.keys }
-
- expect(all_keys.size).to eq all_keys.to_set.size
- end
- end
-
describe '.license_usage_data' do
subject { described_class.license_usage_data }
@@ -1107,10 +1060,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
context 'snowplow stats' do
- before do
- stub_feature_flags(usage_data_instrumentation: false)
- end
-
it 'gathers snowplow stats' do
expect(subject[:settings][:snowplow_enabled]).to eq(Gitlab::CurrentSettings.snowplow_enabled?)
expect(subject[:settings][:snowplow_configured_to_gitlab_collector]).to eq(snowplow_gitlab_host?)
@@ -1159,20 +1108,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
let(:project) { build(:project) }
before do
- counter = Gitlab::UsageDataCounters::TrackUniqueEvents
- project_type = Event::TARGET_TYPES[:project]
- wiki = Event::TARGET_TYPES[:wiki]
- design = Event::TARGET_TYPES[:design]
-
- counter.track_event(event_action: :pushed, event_target: project_type, author_id: 1)
- counter.track_event(event_action: :pushed, event_target: project_type, author_id: 1)
- counter.track_event(event_action: :pushed, event_target: project_type, author_id: 2)
- counter.track_event(event_action: :pushed, event_target: project_type, author_id: 3)
- counter.track_event(event_action: :pushed, event_target: project_type, author_id: 4, time: time - 3.days)
- counter.track_event(event_action: :created, event_target: wiki, author_id: 3)
- counter.track_event(event_action: :created, event_target: design, author_id: 3)
- counter.track_event(event_action: :created, event_target: design, author_id: 4)
-
counter = Gitlab::UsageDataCounters::EditorUniqueCounter
counter.track_web_ide_edit_action(author: user1, project: project)
@@ -1191,10 +1126,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
it 'returns the distinct count of user actions within the specified time period' do
expect(described_class.action_monthly_active_users(time_period)).to eq(
{
- action_monthly_active_users_design_management: 2,
- action_monthly_active_users_project_repo: 3,
- action_monthly_active_users_wiki_repo: 1,
- action_monthly_active_users_git_write: 4,
action_monthly_active_users_web_ide_edit: 2,
action_monthly_active_users_sfe_edit: 2,
action_monthly_active_users_snippet_editor_edit: 2,
@@ -1234,23 +1165,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- describe '.aggregated_metrics_data' do
- it 'uses ::Gitlab::Usage::Metrics::Aggregates::Aggregate methods', :aggregate_failures do
- expected_payload = {
- counts_weekly: { aggregated_metrics: { global_search_gmau: 123 } },
- counts_monthly: { aggregated_metrics: { global_search_gmau: 456 } },
- counts: { aggregate_global_search_gmau: 789 }
- }
-
- expect_next_instance_of(::Gitlab::Usage::Metrics::Aggregates::Aggregate) do |instance|
- expect(instance).to receive(:weekly_data).and_return(global_search_gmau: 123)
- expect(instance).to receive(:monthly_data).and_return(global_search_gmau: 456)
- expect(instance).to receive(:all_time_data).and_return(global_search_gmau: 789)
- end
- expect(described_class.aggregated_metrics_data).to eq(expected_payload)
- end
- end
-
describe '.service_desk_counts' do
subject { described_class.send(:service_desk_counts) }
@@ -1264,110 +1178,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
- describe '.email_campaign_counts' do
- subject { described_class.send(:email_campaign_counts) }
-
- context 'when queries time out' do
- before do
- allow_any_instance_of(ActiveRecord::Relation).to receive(:count).and_raise(ActiveRecord::StatementInvalid)
- allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(should_raise_for_dev)
- end
-
- context 'with should_raise_for_dev? true' do
- let(:should_raise_for_dev) { true }
-
- it 'raises an error' do
- expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
- end
- end
-
- context 'with should_raise_for_dev? false' do
- let(:should_raise_for_dev) { false }
-
- it 'returns -1 for email campaign data' do
- expected_data = {
- "in_product_marketing_email_create_0_sent" => -1,
- "in_product_marketing_email_create_0_cta_clicked" => -1,
- "in_product_marketing_email_create_1_sent" => -1,
- "in_product_marketing_email_create_1_cta_clicked" => -1,
- "in_product_marketing_email_create_2_sent" => -1,
- "in_product_marketing_email_create_2_cta_clicked" => -1,
- "in_product_marketing_email_team_short_0_sent" => -1,
- "in_product_marketing_email_team_short_0_cta_clicked" => -1,
- "in_product_marketing_email_trial_short_0_sent" => -1,
- "in_product_marketing_email_trial_short_0_cta_clicked" => -1,
- "in_product_marketing_email_admin_verify_0_sent" => -1,
- "in_product_marketing_email_admin_verify_0_cta_clicked" => -1,
- "in_product_marketing_email_verify_0_sent" => -1,
- "in_product_marketing_email_verify_0_cta_clicked" => -1,
- "in_product_marketing_email_verify_1_sent" => -1,
- "in_product_marketing_email_verify_1_cta_clicked" => -1,
- "in_product_marketing_email_verify_2_sent" => -1,
- "in_product_marketing_email_verify_2_cta_clicked" => -1,
- "in_product_marketing_email_trial_0_sent" => -1,
- "in_product_marketing_email_trial_0_cta_clicked" => -1,
- "in_product_marketing_email_trial_1_sent" => -1,
- "in_product_marketing_email_trial_1_cta_clicked" => -1,
- "in_product_marketing_email_trial_2_sent" => -1,
- "in_product_marketing_email_trial_2_cta_clicked" => -1,
- "in_product_marketing_email_team_0_sent" => -1,
- "in_product_marketing_email_team_0_cta_clicked" => -1,
- "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
- }
-
- expect(subject).to eq(expected_data)
- end
- end
- end
-
- context 'when there are entries' do
- before do
- create(:in_product_marketing_email, track: :create, series: 0, cta_clicked_at: Time.zone.now)
- create(:in_product_marketing_email, track: :verify, series: 0)
- end
-
- it 'gathers email campaign data' do
- expected_data = {
- "in_product_marketing_email_create_0_sent" => 1,
- "in_product_marketing_email_create_0_cta_clicked" => 1,
- "in_product_marketing_email_create_1_sent" => 0,
- "in_product_marketing_email_create_1_cta_clicked" => 0,
- "in_product_marketing_email_create_2_sent" => 0,
- "in_product_marketing_email_create_2_cta_clicked" => 0,
- "in_product_marketing_email_team_short_0_sent" => 0,
- "in_product_marketing_email_team_short_0_cta_clicked" => 0,
- "in_product_marketing_email_trial_short_0_sent" => 0,
- "in_product_marketing_email_trial_short_0_cta_clicked" => 0,
- "in_product_marketing_email_admin_verify_0_sent" => 0,
- "in_product_marketing_email_admin_verify_0_cta_clicked" => 0,
- "in_product_marketing_email_verify_0_sent" => 1,
- "in_product_marketing_email_verify_0_cta_clicked" => 0,
- "in_product_marketing_email_verify_1_sent" => 0,
- "in_product_marketing_email_verify_1_cta_clicked" => 0,
- "in_product_marketing_email_verify_2_sent" => 0,
- "in_product_marketing_email_verify_2_cta_clicked" => 0,
- "in_product_marketing_email_trial_0_sent" => 0,
- "in_product_marketing_email_trial_0_cta_clicked" => 0,
- "in_product_marketing_email_trial_1_sent" => 0,
- "in_product_marketing_email_trial_1_cta_clicked" => 0,
- "in_product_marketing_email_trial_2_sent" => 0,
- "in_product_marketing_email_trial_2_cta_clicked" => 0,
- "in_product_marketing_email_team_0_sent" => 0,
- "in_product_marketing_email_team_0_cta_clicked" => 0,
- "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
- }
-
- expect(subject).to eq(expected_data)
- end
- end
- end
-
describe ".with_duration" do
it 'records duration' do
expect(::Gitlab::Usage::ServicePing::LegacyMetricTimingDecorator)
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
index cb03797b3d9..236b6d29ba7 100644
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -35,16 +35,23 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
end
strong_memoize_attr :method_name_attr
- strong_memoize_attr :different_method_name_attr, :different_member_name_attr
def different_method_name_attr
trace << value
value
end
+ strong_memoize_attr :different_method_name_attr, :different_member_name_attr
- strong_memoize_attr :enabled?
def enabled?
true
end
+ strong_memoize_attr :enabled?
+
+ def method_name_with_args(*args)
+ strong_memoize_with(:method_name_with_args, args) do
+ trace << [value, args]
+ value
+ end
+ end
def trace
@trace ||= []
@@ -141,6 +148,36 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
end
end
+ describe '#strong_memoize_with' do
+ [nil, false, true, 'value', 0, [0]].each do |value|
+ context "with value #{value}" do
+ let(:value) { value }
+
+ it 'only calls the block once' do
+ value0 = object.method_name_with_args(1)
+ value1 = object.method_name_with_args(1)
+ value2 = object.method_name_with_args([2, 3])
+ value3 = object.method_name_with_args([2, 3])
+
+ expect(value0).to eq(value)
+ expect(value1).to eq(value)
+ expect(value2).to eq(value)
+ expect(value3).to eq(value)
+
+ expect(object.trace).to contain_exactly([value, [1]], [value, [[2, 3]]])
+ end
+
+ it 'returns and defines the instance variable for the exact value' do
+ returned_value = object.method_name_with_args(1, 2, 3)
+ memoized_value = object.instance_variable_get(:@method_name_with_args)
+
+ expect(returned_value).to eql(value)
+ expect(memoized_value).to eql({ [[1, 2, 3]] => value })
+ end
+ end
+ end
+ end
+
describe '#strong_memoized?' do
let(:value) { :anything }
@@ -227,5 +264,21 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
expect(klass.public_instance_methods).to include(:public_method)
end
end
+
+ context "when method doesn't exist" do
+ let(:klass) do
+ strong_memoize_class = described_class
+
+ Struct.new(:value) do
+ include strong_memoize_class
+ end
+ end
+
+ subject { klass.strong_memoize_attr(:nonexistent_method) }
+
+ it 'fails when strong-memoizing a nonexistent method' do
+ expect { subject }.to raise_error(NameError, %r{undefined method `nonexistent_method' for class})
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index d1fdaf7a9db..80b2ec63af9 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -63,9 +63,21 @@ RSpec.describe Gitlab::Utils do
expect(check_path_traversal!('dir/.foo.rb')).to eq('dir/.foo.rb')
end
- it 'does nothing for a non-string' do
+ it 'does nothing for nil' do
expect(check_path_traversal!(nil)).to be_nil
end
+
+ it 'does nothing for safe HashedPath' do
+ expect(check_path_traversal!(Gitlab::HashedPath.new('tmp', root_hash: 1))).to eq '6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/tmp'
+ end
+
+ it 'raises for unsafe HashedPath' do
+ expect { check_path_traversal!(Gitlab::HashedPath.new('tmp', '..', 'etc', 'passwd', root_hash: 1)) }.to raise_error(/Invalid path/)
+ end
+
+ it 'raises for other non-strings' do
+ expect { check_path_traversal!(%w[/tmp /tmp/../etc/passwd]) }.to raise_error(/Invalid path/)
+ end
end
describe '.check_allowed_absolute_path_and_path_traversal!' do
diff --git a/spec/lib/gitlab/webpack/file_loader_spec.rb b/spec/lib/gitlab/webpack/file_loader_spec.rb
index 6475ef58611..c2e9cd8124d 100644
--- a/spec/lib/gitlab/webpack/file_loader_spec.rb
+++ b/spec/lib/gitlab/webpack/file_loader_spec.rb
@@ -31,8 +31,8 @@ RSpec.describe Gitlab::Webpack::FileLoader do
stub_request(:get, "http://hostname:2000/public_path/#{error_file_path}").to_raise(StandardError)
end
- it "returns content when respondes succesfully" do
- expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
+ it "returns content when responds successfully" do
+ expect(Gitlab::Webpack::FileLoader.load(file_path)).to eq(file_contents)
end
it "raises error when 404" do
diff --git a/spec/lib/grafana/client_spec.rb b/spec/lib/grafana/client_spec.rb
index c233d0b8445..13fe9acc6e9 100644
--- a/spec/lib/grafana/client_spec.rb
+++ b/spec/lib/grafana/client_spec.rb
@@ -54,7 +54,7 @@ RSpec.describe Grafana::Client do
}
exceptions.each do |exception, message|
- context "#{exception}" do
+ context exception.to_s do
before do
stub_request(:get, grafana_api_url).to_raise(exception)
end
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 1629aec89f5..c2201fb60ac 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -192,11 +192,28 @@ RSpec.describe ObjectStorage::DirectUpload do
end
end
- shared_examples 'a valid Google upload' do
+ shared_examples 'a valid Google upload' do |use_workhorse_client: true|
+ let(:gocloud_url) { "gs://#{bucket_name}" }
+
it_behaves_like 'a valid upload'
- it 'does not set Workhorse client data' do
- expect(subject.keys).not_to include(:UseWorkhorseClient, :RemoteTempObjectID, :ObjectStorage)
+ if use_workhorse_client
+ it 'enables the Workhorse client' do
+ expect(subject[:UseWorkhorseClient]).to be true
+ expect(subject[:RemoteTempObjectID]).to eq(object_name)
+ expect(subject[:ObjectStorage][:Provider]).to eq('Google')
+ expect(subject[:ObjectStorage][:GoCloudConfig]).to eq({ URL: gocloud_url })
+ end
+ end
+
+ context 'with workhorse_google_client disabled' do
+ before do
+ stub_feature_flags(workhorse_google_client: false)
+ end
+
+ it 'does not set Workhorse client data' do
+ expect(subject.keys).not_to include(:UseWorkhorseClient, :RemoteTempObjectID, :ObjectStorage)
+ end
end
end
@@ -411,28 +428,88 @@ RSpec.describe ObjectStorage::DirectUpload do
end
context 'when Google is used' do
- let(:credentials) do
- {
- provider: 'Google',
- google_storage_access_key_id: 'GOOGLE_ACCESS_KEY_ID',
- google_storage_secret_access_key: 'GOOGLE_SECRET_ACCESS_KEY'
- }
+ let(:consolidated_settings) { true }
+
+ # We need to use fog mocks as using google_application_default
+ # will trigger network requests which we don't want in this spec.
+ # In turn, using fog mocks will don't use a specific storage endpoint,
+ # hence the storage_url with the empty host.
+ let(:storage_url) { 'https:///uploads/' }
+
+ before do
+ Fog.mock!
end
- let(:storage_url) { 'https://storage.googleapis.com/uploads/' }
+ context 'with google_application_default' do
+ let(:credentials) do
+ {
+ provider: 'Google',
+ google_project: 'GOOGLE_PROJECT',
+ google_application_default: true
+ }
+ end
- context 'when length is known' do
- let(:has_length) { true }
+ context 'when length is known' do
+ let(:has_length) { true }
- it_behaves_like 'a valid Google upload'
- it_behaves_like 'a valid upload without multipart data'
+ it_behaves_like 'a valid Google upload'
+ it_behaves_like 'a valid upload without multipart data'
+ end
+
+ context 'when length is unknown' do
+ let(:has_length) { false }
+
+ it_behaves_like 'a valid Google upload'
+ it_behaves_like 'a valid upload without multipart data'
+ end
end
- context 'when length is unknown' do
- let(:has_length) { false }
+ context 'with google_json_key_location' do
+ let(:credentials) do
+ {
+ provider: 'Google',
+ google_project: 'GOOGLE_PROJECT',
+ google_json_key_location: 'LOCATION'
+ }
+ end
+
+ context 'when length is known' do
+ let(:has_length) { true }
+
+ it_behaves_like 'a valid Google upload', use_workhorse_client: true
+ it_behaves_like 'a valid upload without multipart data'
+ end
+
+ context 'when length is unknown' do
+ let(:has_length) { false }
+
+ it_behaves_like 'a valid Google upload', use_workhorse_client: true
+ it_behaves_like 'a valid upload without multipart data'
+ end
+ end
+
+ context 'with google_json_key_string' do
+ let(:credentials) do
+ {
+ provider: 'Google',
+ google_project: 'GOOGLE_PROJECT',
+ google_json_key_string: 'STRING'
+ }
+ end
+
+ context 'when length is known' do
+ let(:has_length) { true }
+
+ it_behaves_like 'a valid Google upload', use_workhorse_client: true
+ it_behaves_like 'a valid upload without multipart data'
+ end
+
+ context 'when length is unknown' do
+ let(:has_length) { false }
- it_behaves_like 'a valid Google upload'
- it_behaves_like 'a valid upload without multipart data'
+ it_behaves_like 'a valid Google upload', use_workhorse_client: true
+ it_behaves_like 'a valid upload without multipart data'
+ end
end
end
@@ -466,4 +543,38 @@ RSpec.describe ObjectStorage::DirectUpload do
end
end
end
+
+ describe '#use_workhorse_google_client?' do
+ let(:direct_upload) { described_class.new(config, object_name, has_length: true) }
+
+ subject { direct_upload.use_workhorse_google_client? }
+
+ context 'with consolidated_settings' do
+ let(:consolidated_settings) { true }
+
+ [
+ { google_application_default: true },
+ { google_json_key_string: 'TEST' },
+ { google_json_key_location: 'PATH' }
+ ].each do |google_config|
+ context "with #{google_config.each_key.first}" do
+ let(:credentials) { google_config }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'without any google setting' do
+ let(:credentials) { {} }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'without consolidated_settings' do
+ let(:consolidated_settings) { true }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index b29e48b0de5..97547912663 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -51,6 +51,7 @@ RSpec.describe OmniAuth::Strategies::Jwt do
context "when the #{algorithm} algorithm is used" do
let(:algorithm) { algorithm }
let(:secret) do
+ # rubocop:disable Style/CaseLikeIf
if private_key_class == OpenSSL::PKey::RSA
private_key_class.generate(2048)
.to_pem
@@ -61,6 +62,7 @@ RSpec.describe OmniAuth::Strategies::Jwt do
else
private_key_class.new(jwt_config.strategy.secret)
end
+ # rubocop:enable Style/CaseLikeIf
end
let(:private_key) { private_key_class ? private_key_class.new(secret) : secret }
diff --git a/spec/lib/rouge/formatters/html_gitlab_spec.rb b/spec/lib/rouge/formatters/html_gitlab_spec.rb
index 7c92c62e30b..79bfdb262c0 100644
--- a/spec/lib/rouge/formatters/html_gitlab_spec.rb
+++ b/spec/lib/rouge/formatters/html_gitlab_spec.rb
@@ -11,6 +11,16 @@ RSpec.describe Rouge::Formatters::HTMLGitlab do
let(:tokens) { lexer.lex("def hello", continue: false) }
let(:options) { { tag: lang } }
+ context 'when svg and indexes are present to trim' do
+ let(:options) { { tag: lang, ellipsis_indexes: [0], ellipsis_svg: "svg_icon" } }
+
+ it 'returns highlighted ruby code with svg' do
+ code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span><span class="gl-px-2 gl-rounded-base gl-mx-2 gl-bg-gray-100 gl-cursor-help has-tooltip" title="Content has been trimmed">svg_icon</span></span>}
+
+ is_expected.to eq(code)
+ end
+ end
+
it 'returns highlighted ruby code' do
code = %q{<span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">hello</span></span>}
diff --git a/spec/lib/sbom/package_url/argument_validator_spec.rb b/spec/lib/sbom/package_url/argument_validator_spec.rb
new file mode 100644
index 00000000000..246da1c0bda
--- /dev/null
+++ b/spec/lib/sbom/package_url/argument_validator_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
+
+RSpec.describe Sbom::PackageUrl::ArgumentValidator do
+ let(:mock_package_url) { Struct.new(:type, :namespace, :name, :version, :qualifiers, keyword_init: true) }
+ let(:package) do
+ mock_package_url.new(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers
+ )
+ end
+
+ subject(:validate) { described_class.new(package).validate! }
+
+ context 'with valid arguments' do
+ include_context 'with valid purl examples'
+
+ with_them do
+ it 'does not raise error' do
+ expect { validate }.not_to raise_error
+ end
+ end
+ end
+
+ context 'with invalid arguments' do
+ include_context 'with invalid purl examples'
+
+ with_them do
+ it 'raises an ArgumentError' do
+ expect { validate }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ context 'with multiple errors' do
+ let(:type) { nil }
+ let(:name) { nil }
+ let(:package) { mock_package_url.new(type: type, name: name) }
+
+ it 'reports all errors' do
+ expect { validate }.to raise_error(ArgumentError, 'Type is required, Name is required')
+ end
+ end
+end
diff --git a/spec/lib/sbom/package_url/decoder_spec.rb b/spec/lib/sbom/package_url/decoder_spec.rb
new file mode 100644
index 00000000000..5b480475b7c
--- /dev/null
+++ b/spec/lib/sbom/package_url/decoder_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
+
+RSpec.describe Sbom::PackageUrl::Decoder do
+ describe '#decode' do
+ subject(:decode) { described_class.new(purl).decode! }
+
+ include_context 'with valid purl examples'
+
+ with_them do
+ it do
+ is_expected.to have_attributes(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ )
+ end
+ end
+
+ context 'when no argument is passed' do
+ let(:purl) { nil }
+
+ it 'raises an error' do
+ expect { decode }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when an invalid package URL string is passed' do
+ include_context 'with invalid purl examples'
+
+ with_them do
+ it 'raises an error' do
+ expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageUrl)
+ end
+ end
+ end
+
+ context 'when namespace or subpath contains an encoded slash' do
+ where(:purl) do
+ [
+ 'pkg:golang/google.org/golang/genproto#googleapis%2fapi%2fannotations',
+ 'pkg:golang/google.org%2fgolang/genproto#googleapis/api/annotations'
+ ]
+ end
+
+ with_them do
+ it { expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageUrl) }
+ end
+ end
+
+ context 'when name contains an encoded slash' do
+ let(:purl) { 'pkg:golang/google.org/golang%2fgenproto#googleapis/api/annotations' }
+
+ it do
+ is_expected.to have_attributes(
+ type: 'golang',
+ namespace: 'google.org',
+ name: 'golang/genproto',
+ version: nil,
+ qualifiers: nil,
+ subpath: 'googleapis/api/annotations'
+ )
+ end
+ end
+
+ context 'with URL encoded segments' do
+ let(:purl) do
+ 'pkg:golang/namespace%21/google.golang.org%20genproto@version%21?k=v%21#googleapis%20api%20annotations'
+ end
+
+ it 'decodes them' do
+ is_expected.to have_attributes(
+ type: 'golang',
+ namespace: 'namespace!',
+ name: 'google.golang.org genproto',
+ version: 'version!',
+ qualifiers: { 'k' => 'v!' },
+ subpath: 'googleapis api annotations'
+ )
+ end
+ end
+
+ context 'when segments contain empty values' do
+ let(:purl) { 'pkg:golang/google.golang.org//.././genproto#googleapis/..//./api/annotations' }
+
+ it 'removes them from the segments' do
+ is_expected.to have_attributes(
+ type: 'golang',
+ namespace: 'google.golang.org/../.', # . and .. are allowed in the namespace, but not the subpath
+ name: 'genproto',
+ version: nil,
+ qualifiers: nil,
+ subpath: 'googleapis/api/annotations'
+ )
+ end
+ end
+
+ context 'when qualifiers have no value' do
+ let(:purl) { 'pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25&foo=&bar=' }
+
+ it 'they are ignored' do
+ is_expected.to have_attributes(
+ type: 'rpm',
+ namespace: 'fedora',
+ name: 'curl',
+ version: '7.50.3-1.fc25',
+ qualifiers: { 'arch' => 'i386',
+ 'distro' => 'fedora-25' },
+ subpath: nil
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/sbom/package_url/encoder_spec.rb b/spec/lib/sbom/package_url/encoder_spec.rb
new file mode 100644
index 00000000000..bdbd61636b5
--- /dev/null
+++ b/spec/lib/sbom/package_url/encoder_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
+
+RSpec.describe Sbom::PackageUrl::Encoder do
+ describe '#encode' do
+ let(:package) do
+ ::Sbom::PackageUrl.new(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ )
+ end
+
+ subject(:encode) { described_class.new(package).encode }
+
+ include_context 'with valid purl examples'
+
+ with_them do
+ it { is_expected.to eq(canonical_purl) }
+ end
+ end
+end
diff --git a/spec/lib/sbom/package_url/normalizer_spec.rb b/spec/lib/sbom/package_url/normalizer_spec.rb
new file mode 100644
index 00000000000..bbc2bd3ca13
--- /dev/null
+++ b/spec/lib/sbom/package_url/normalizer_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
+
+RSpec.describe Sbom::PackageUrl::Normalizer do
+ shared_examples 'name normalization' do
+ context 'with bitbucket url' do
+ let(:type) { 'bitbucket' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'downcases text' do
+ is_expected.to eq('purl_spec')
+ end
+ end
+
+ context 'with github url' do
+ let(:type) { 'github' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'downcases text' do
+ is_expected.to eq('purl_spec')
+ end
+ end
+
+ context 'with pypi url' do
+ let(:type) { 'pypi' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'downcases text and replaces underscores' do
+ is_expected.to eq('purl-spec')
+ end
+ end
+
+ context 'with other urls' do
+ let(:type) { 'npm' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'does not change the text' do
+ is_expected.to eq(text)
+ end
+ end
+ end
+
+ describe '#normalize_name' do
+ subject(:normalize_name) { described_class.new(type: type, text: text).normalize_name }
+
+ it_behaves_like 'name normalization'
+
+ context 'when text is nil' do
+ let(:type) { 'npm' }
+ let(:text) { nil }
+
+ it 'raises an error' do
+ expect { normalize_name }.to raise_error(ArgumentError, 'Name is required')
+ end
+ end
+ end
+
+ describe '#normalize_namespace' do
+ subject(:normalize_namespace) { described_class.new(type: type, text: text).normalize_namespace }
+
+ it_behaves_like 'name normalization'
+
+ context 'when text is nil' do
+ let(:type) { 'npm' }
+ let(:text) { nil }
+
+ it 'allows nil values' do
+ expect(normalize_namespace).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/sbom/package_url_spec.rb b/spec/lib/sbom/package_url_spec.rb
new file mode 100644
index 00000000000..6760b0a68e5
--- /dev/null
+++ b/spec/lib/sbom/package_url_spec.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+# MIT License
+#
+# Copyright (c) 2021 package-url
+# Portions Copyright 2022 Gitlab B.V.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../support/helpers/next_instance_of'
+require_relative '../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
+
+RSpec.describe Sbom::PackageUrl do
+ include NextInstanceOf
+
+ describe '#initialize' do
+ subject do
+ described_class.new(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ )
+ end
+
+ context 'with well-formed arguments' do
+ include_context 'with valid purl examples'
+
+ with_them do
+ it do
+ is_expected.to have_attributes(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ )
+ end
+ end
+ end
+
+ context 'when no arguments are given' do
+ it { expect { described_class.new }.to raise_error(ArgumentError) }
+ end
+
+ context 'when parameters are invalid' do
+ include_context 'with invalid purl examples'
+
+ with_them do
+ it 'raises an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe 'normalization' do
+ it 'downcases provided type component' do
+ purl = described_class.new(type: 'EXAMPLE', name: 'test')
+
+ expect(purl.type).to eq('example')
+ expect(purl.name).to eq('test')
+ end
+
+ it 'does not down provided name component' do
+ purl = described_class.new(type: 'example', name: 'TEST')
+
+ expect(purl.type).to eq('example')
+ expect(purl.name).to eq('TEST')
+ end
+ end
+ end
+
+ describe '#parse' do
+ let(:url) { 'pkg:gem/rails@6.1.6.1' }
+
+ subject(:parse) { described_class.parse(url) }
+
+ it 'delegates parsing to the decoder' do
+ expect_next_instance_of(described_class::Decoder, url) do |decoder|
+ expect(decoder).to receive(:decode!)
+ end
+
+ parse
+ end
+ end
+
+ describe '#to_h' do
+ let(:package) do
+ described_class.new(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ )
+ end
+
+ subject(:to_h) { package.to_h }
+
+ include_context 'with valid purl examples'
+
+ with_them do
+ it do
+ is_expected.to eq(
+ {
+ scheme: 'pkg',
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ }
+ )
+ end
+ end
+ end
+
+ describe '#to_s' do
+ let(:package) do
+ described_class.new(
+ type: 'npm',
+ namespace: nil,
+ name: 'lodash',
+ version: nil,
+ qualifiers: nil,
+ subpath: nil
+ )
+ end
+
+ it 'delegates to_s to the encoder' do
+ expect_next_instance_of(described_class::Encoder, package) do |encoder|
+ expect(encoder).to receive(:encode)
+ end
+
+ package.to_s
+ end
+ end
+end
diff --git a/spec/lib/serializers/symbolized_json_spec.rb b/spec/lib/serializers/symbolized_json_spec.rb
deleted file mode 100644
index b9217854d9a..00000000000
--- a/spec/lib/serializers/symbolized_json_spec.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'oj'
-
-RSpec.describe Serializers::SymbolizedJson do
- describe '.dump' do
- let(:obj) { { key: "value" } }
-
- subject { described_class.dump(obj) }
-
- it 'returns a hash' do
- is_expected.to eq(obj)
- end
- end
-
- describe '.load' do
- let(:data_string) { '{"key":"value","variables":[{"key":"VAR1","value":"VALUE1"}]}' }
- let(:data_hash) { Gitlab::Json.parse(data_string) }
-
- context 'when loading a hash' do
- subject { described_class.load(data_hash) }
-
- it 'decodes a string' do
- is_expected.to be_a(Hash)
- end
-
- it 'allows to access with symbols' do
- expect(subject[:key]).to eq('value')
- expect(subject[:variables].first[:key]).to eq('VAR1')
- end
- end
-
- context 'when loading a nil' do
- subject { described_class.load(nil) }
-
- it 'returns nil' do
- is_expected.to be_nil
- end
- end
- end
-end
diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
index 2da7d324708..64408ac3b88 100644
--- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
@@ -23,6 +23,52 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
expect(subject.render?).to be true
end
end
+
+ describe 'behavior based on access level setting' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create(:project) }
+ let(:enabled) { Featurable::PRIVATE }
+ let(:disabled) { Featurable::DISABLED }
+
+ where(:operations_access_level, :infrastructure_access_level, :render) do
+ ref(:disabled) | ref(:enabled) | true
+ ref(:disabled) | ref(:disabled) | false
+ ref(:enabled) | ref(:enabled) | true
+ ref(:enabled) | ref(:disabled) | false
+ end
+
+ with_them do
+ it 'renders based on the infrastructure access level' do
+ project.project_feature.update!(operations_access_level: operations_access_level)
+ project.project_feature.update!(infrastructure_access_level: infrastructure_access_level)
+
+ expect(subject.render?).to be render
+ end
+ end
+
+ context 'when `split_operations_visibility_permissions` feature flag is disabled' do
+ before do
+ stub_feature_flags(split_operations_visibility_permissions: false)
+ end
+
+ where(:operations_access_level, :infrastructure_access_level, :render) do
+ ref(:disabled) | ref(:enabled) | false
+ ref(:disabled) | ref(:disabled) | false
+ ref(:enabled) | ref(:enabled) | true
+ ref(:enabled) | ref(:disabled) | true
+ end
+
+ with_them do
+ it 'renders based on the operations access level' do
+ project.project_feature.update!(operations_access_level: operations_access_level)
+ project.project_feature.update!(infrastructure_access_level: infrastructure_access_level)
+
+ expect(subject.render?).to be render
+ end
+ end
+ end
+ end
end
describe '#link' do
diff --git a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
index bd0904b9db2..f6a8dd7367d 100644
--- a/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/monitor_menu_spec.rb
@@ -102,19 +102,5 @@ RSpec.describe Sidebars::Projects::Menus::MonitorMenu do
it_behaves_like 'access rights checks'
end
-
- describe 'Product Analytics' do
- let(:item_id) { :product_analytics }
-
- specify { is_expected.not_to be_nil }
-
- describe 'when feature flag :product_analytics is disabled' do
- specify do
- stub_feature_flags(product_analytics: false)
-
- is_expected.to be_nil
- end
- end
- end
end
end
diff --git a/spec/lib/unnested_in_filters/rewriter_spec.rb b/spec/lib/unnested_in_filters/rewriter_spec.rb
index 21bab42c95c..fe34fba579b 100644
--- a/spec/lib/unnested_in_filters/rewriter_spec.rb
+++ b/spec/lib/unnested_in_filters/rewriter_spec.rb
@@ -12,6 +12,12 @@ RSpec.describe UnnestedInFilters::Rewriter do
describe '#rewrite?' do
subject(:rewrite?) { rewriter.rewrite? }
+ context 'when a join table is receiving an IN list query' do
+ let(:relation) { User.joins(:status).where(status: { message: %w[foo bar] }).order(id: :desc).limit(2) }
+
+ it { is_expected.to be_falsey }
+ end
+
context 'when the given relation does not have an `IN` predicate' do
let(:relation) { User.where(username: 'user') }
@@ -170,6 +176,91 @@ RSpec.describe UnnestedInFilters::Rewriter do
end
end
+ context 'when the combined attributes include the primary key' do
+ let(:relation) { User.where(user_type: %i(support_bot alert_bot)).order(id: :desc).limit(2) }
+
+ let(:expected_query) do
+ <<~SQL
+ SELECT
+ "users".*
+ FROM
+ "users"
+ WHERE
+ "users"."id" IN (
+ SELECT
+ "users"."id"
+ FROM
+ unnest('{1,2}' :: smallint []) AS "user_types"("user_type"),
+ LATERAL (
+ SELECT
+ "users"."user_type",
+ "users"."id"
+ FROM
+ "users"
+ WHERE
+ (users."user_type" = "user_types"."user_type")
+ ORDER BY
+ "users"."id" DESC
+ LIMIT
+ 2
+ ) AS users
+ ORDER BY
+ "users"."id" DESC
+ LIMIT
+ 2
+ )
+ ORDER BY
+ "users"."id" DESC
+ LIMIT
+ 2
+ SQL
+ end
+
+ it 'changes the query' do
+ expect(issued_query.gsub(/\s/, '')).to start_with(expected_query.gsub(/\s/, ''))
+ end
+ end
+
+ context 'when a join table is receiving an IN list query' do
+ let(:relation) { User.joins(:status).where(status: { message: %w[foo bar] }).order(id: :desc).limit(2) }
+
+ let(:expected_query) do
+ <<~SQL
+ SELECT
+ "users".*
+ FROM
+ "users"
+ WHERE
+ "users"."id" IN (
+ SELECT
+ "users"."id"
+ FROM
+ LATERAL (
+ SELECT
+ message,
+ "users"."id"
+ FROM
+ "users"
+ INNER JOIN "user_statuses" "status" ON "status"."user_id" = "users"."id"
+ WHERE
+ "status"."message" IN ('foo', 'bar')
+ ORDER BY
+ "users"."id" DESC
+ LIMIT 2) AS users
+ ORDER BY
+ "users"."id" DESC
+ LIMIT 2)
+ ORDER BY
+ "users"."id" DESC
+ LIMIT 2
+ SQL
+ end
+
+ it 'does not rewrite the in statement for the joined table' do
+ expect(issued_query.gsub(/\s/, '')).to start_with(expected_query.gsub(/\s/, ''))
+ end
+ end
+
describe 'logging' do
subject(:load_reload) { rewriter.rewrite }
diff --git a/spec/mailers/emails/identity_verification_spec.rb b/spec/mailers/emails/identity_verification_spec.rb
new file mode 100644
index 00000000000..57ae95cc1ee
--- /dev/null
+++ b/spec/mailers/emails/identity_verification_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Emails::IdentityVerification do
+ include EmailSpec::Matchers
+ include_context 'gitlab email notification'
+
+ describe 'verification_instructions_email' do
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:token) { '123456' }
+
+ subject do
+ Notify.verification_instructions_email(user.email, token: token)
+ end
+
+ it_behaves_like 'an email sent from GitLab'
+
+ it 'is sent to the user' do
+ is_expected.to deliver_to user.email
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject s_('IdentityVerification|Verify your identity')
+ end
+
+ it 'has the mailgun suppression bypass header' do
+ is_expected.to have_header 'X-Mailgun-Suppressions-Bypass', 'true'
+ end
+
+ it 'includes the token' do
+ is_expected.to have_body_text token
+ end
+
+ it 'includes the expiration time' do
+ expires_in_minutes = Users::EmailVerification::ValidateTokenService::TOKEN_VALID_FOR_MINUTES
+
+ is_expected.to have_body_text format(s_('IdentityVerification|Your verification code expires after '\
+ '%{expires_in_minutes} minutes.'), expires_in_minutes: expires_in_minutes)
+ end
+ end
+end
diff --git a/spec/mailers/emails/releases_spec.rb b/spec/mailers/emails/releases_spec.rb
index d1d7f5e6d6a..e8ca9533256 100644
--- a/spec/mailers/emails/releases_spec.rb
+++ b/spec/mailers/emails/releases_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe Emails::Releases do
subject { Notify.new_release_email(user.id, release) }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'an email with X-GitLab headers containing project details'
context 'when the release has a name' do
it 'shows the correct subject' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 1f53c472c5c..5733e892d2a 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -750,7 +750,7 @@ RSpec.describe Notify do
before_all do
private_project.add_guest(recipient)
- note.update!(note: "#{private_issue.to_reference(full: true)}")
+ note.update!(note: private_issue.to_reference(full: true).to_s)
end
let(:html_part) { subject.body.parts.last.to_s }
diff --git a/spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb b/spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb
deleted file mode 100644
index 13a6aa5413e..00000000000
--- a/spec/migrations/20210818185845_backfill_projects_with_coverage_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_migration!
-
-RSpec.describe BackfillProjectsWithCoverage, :suppress_gitlab_schemas_validate_connection do
- let(:projects) { table(:projects) }
- let(:ci_pipelines) { table(:ci_pipelines) }
- let(:ci_daily_build_group_report_results) { table(:ci_daily_build_group_report_results) }
- let(:group) { table(:namespaces).create!(name: 'user', path: 'user') }
- let(:project_1) { projects.create!(namespace_id: group.id) }
- let(:project_2) { projects.create!(namespace_id: group.id) }
- let(:pipeline_1) { ci_pipelines.create!(project_id: project_1.id) }
- let(:pipeline_2) { ci_pipelines.create!(project_id: project_2.id) }
- let(:pipeline_3) { ci_pipelines.create!(project_id: project_2.id) }
-
- describe '#up' do
- before do
- stub_const("#{described_class}::BATCH_SIZE", 2)
- stub_const("#{described_class}::SUB_BATCH_SIZE", 1)
-
- ci_daily_build_group_report_results.create!(
- id: 1,
- project_id: project_1.id,
- date: 3.days.ago,
- last_pipeline_id: pipeline_1.id,
- ref_path: 'main',
- group_name: 'rspec',
- data: { coverage: 95.0 },
- default_branch: true,
- group_id: group.id
- )
-
- ci_daily_build_group_report_results.create!(
- id: 2,
- project_id: project_2.id,
- date: 2.days.ago,
- last_pipeline_id: pipeline_2.id,
- ref_path: 'main',
- group_name: 'rspec',
- data: { coverage: 95.0 },
- default_branch: true,
- group_id: group.id
- )
-
- ci_daily_build_group_report_results.create!(
- id: 3,
- project_id: project_2.id,
- date: 1.day.ago,
- last_pipeline_id: pipeline_3.id,
- ref_path: 'test_branch',
- group_name: 'rspec',
- data: { coverage: 95.0 },
- default_branch: false,
- group_id: group.id
- )
- end
-
- it 'schedules BackfillProjectsWithCoverage background jobs', :aggregate_failures do
- Sidekiq::Testing.fake! do
- freeze_time do
- migrate!
-
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2, 1)
- expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 3, 3, 1)
- expect(BackgroundMigrationWorker.jobs.size).to eq(2)
- end
- end
- end
- end
-end
diff --git a/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
index ece971a50c9..063a51227dd 100644
--- a/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
+++ b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
@@ -29,7 +29,7 @@ RSpec.describe PopulateVulnerabilityReads, :migration do
project_id: project.id,
external_type: 'uuid-v5',
external_id: 'uuid-v5',
- fingerprint: Digest::SHA1.hexdigest("#{vulnerability.id}"),
+ fingerprint: Digest::SHA1.hexdigest(vulnerability.id.to_s),
name: 'Identifier for UUIDv5')
create_finding!(
diff --git a/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb b/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
index 614044657ec..174cfda1a46 100644
--- a/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
+++ b/spec/migrations/20220921144258_remove_orphan_group_token_users_spec.rb
@@ -34,11 +34,7 @@ RSpec.describe RemoveOrphanGroupTokenUsers, :migration, :sidekiq_inline do
let(:members) { table(:members) }
let(:namespaces) { table(:namespaces) }
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'removes orphan project bot and its tokens', :aggregate_failures do
+ it 'initiates orphan project bot removal', :aggregate_failures do
expect(DeleteUserWorker)
.to receive(:perform_async)
.with(orphan_bot.id, orphan_bot.id, skip_authorization: true)
@@ -46,7 +42,8 @@ RSpec.describe RemoveOrphanGroupTokenUsers, :migration, :sidekiq_inline do
migrate!
- expect(users.count).to eq 2
+ expect(Users::GhostUserMigration.where(user: orphan_bot)).to be_exists
+ expect(users.count).to eq 3
expect(personal_access_tokens.count).to eq 2
expect(personal_access_tokens.find_by(user_id: orphan_bot.id)).to eq nil
end
diff --git a/spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb b/spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb
new file mode 100644
index 00000000000..4de897802b9
--- /dev/null
+++ b/spec/migrations/20221018050323_add_objective_and_keyresult_to_work_item_types_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddObjectiveAndKeyresultToWorkItemTypes, :migration do
+ include MigrationHelpers::WorkItemTypesHelper
+
+ let_it_be(:work_item_types) { table(:work_item_types) }
+
+ let(:base_types) do
+ {
+ issue: 0,
+ incident: 1,
+ test_case: 2,
+ requirement: 3,
+ task: 4,
+ objective: 5,
+ key_result: 6
+ }
+ end
+
+ after(:all) do
+ # Make sure base types are recreated after running the migration
+ # because migration specs are not run in a transaction
+ reset_work_item_types
+ end
+
+ it 'skips creating both objective & keyresult type record if it already exists' do
+ reset_db_state_prior_to_migration
+ work_item_types.find_or_create_by!(name: 'Key Result', namespace_id: nil, base_type: base_types[:key_result],
+ icon_name: 'issue-type-keyresult')
+ work_item_types.find_or_create_by!(name: 'Objective', namespace_id: nil, base_type: base_types[:objective],
+ icon_name: 'issue-type-objective')
+
+ expect do
+ migrate!
+ end.to not_change(work_item_types, :count)
+ end
+
+ it 'adds both objective & keyresult to base work item types' do
+ reset_db_state_prior_to_migration
+
+ expect do
+ migrate!
+ end.to change(work_item_types, :count).from(5).to(7)
+
+ expect(work_item_types.all.pluck(:base_type)).to include(base_types[:objective])
+ expect(work_item_types.all.pluck(:base_type)).to include(base_types[:key_result])
+ end
+
+ def reset_db_state_prior_to_migration
+ # Database needs to be in a similar state as when this migration was created
+ work_item_types.delete_all
+ work_item_types.find_or_create_by!(name: 'Issue', namespace_id: nil, base_type: base_types[:issue],
+ icon_name: 'issue-type-issue')
+ work_item_types.find_or_create_by!(name: 'Incident', namespace_id: nil, base_type: base_types[:incident],
+ icon_name: 'issue-type-incident')
+ work_item_types.find_or_create_by!(name: 'Test Case', namespace_id: nil, base_type: base_types[:test_case],
+ icon_name: 'issue-type-test-case')
+ work_item_types.find_or_create_by!(name: 'Requirement', namespace_id: nil, base_type: base_types[:requirement],
+ icon_name: 'issue-type-requirements')
+ work_item_types.find_or_create_by!(name: 'Task', namespace_id: nil, base_type: base_types[:task],
+ icon_name: 'issue-type-task')
+ end
+end
diff --git a/spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb b/spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb
new file mode 100644
index 00000000000..4dd6d5757ce
--- /dev/null
+++ b/spec/migrations/20221018062308_schedule_backfill_project_namespace_details_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleBackfillProjectNamespaceDetails, schema: 20221018062308 do
+ context 'when on gitlab.com' do
+ let_it_be(:background_migration) { described_class::MIGRATION }
+ let_it_be(:migration) { described_class.new }
+
+ before do
+ migration.up
+ end
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of projects' do
+ expect(background_migration).to(
+ have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :id,
+ interval: described_class::INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migration.down
+
+ expect(described_class::MIGRATION).not_to have_scheduled_batched_migration
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb b/spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb
new file mode 100644
index 00000000000..ea95c34674e
--- /dev/null
+++ b/spec/migrations/20221018193635_ensure_task_note_renaming_background_migration_finished_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe EnsureTaskNoteRenamingBackgroundMigrationFinished, :migration do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+ let(:batch_failed_status) { 2 }
+ let(:batch_finalized_status) { 3 }
+
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ shared_examples 'finalizes the migration' do
+ it 'finalizes the migration' do
+ expect do
+ migrate!
+
+ task_renaming_migration.reload
+ failed_job.reload
+ end.to change(task_renaming_migration, :status).from(task_renaming_migration.status).to(3).and(
+ change(failed_job, :status).from(batch_failed_status).to(batch_finalized_status)
+ )
+ end
+ end
+
+ context 'when migration is missing' do
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let!(:task_renaming_migration) do
+ batched_migrations.create!(
+ job_class_name: 'RenameTaskSystemNoteToChecklistItem',
+ table_name: :system_note_metadata,
+ column_name: :id,
+ job_arguments: [],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 200,
+ gitlab_schema: :gitlab_main,
+ status: 3 # finished
+ )
+ end
+
+ context 'when migration finished successfully' do
+ it 'does not raise exception' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different migration statuses', :redis do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ let!(:failed_job) do
+ table(:batched_background_migration_jobs).create!(
+ batched_background_migration_id: task_renaming_migration.id,
+ status: batch_failed_status,
+ min_value: 1,
+ max_value: 10,
+ attempts: 2,
+ batch_size: 100,
+ sub_batch_size: 10
+ )
+ end
+
+ before do
+ task_renaming_migration.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb b/spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb
new file mode 100644
index 00000000000..48a00df430d
--- /dev/null
+++ b/spec/migrations/20221021145820_create_routing_table_for_builds_metadata_v2_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CreateRoutingTableForBuildsMetadataV2, :migration do
+ let_it_be(:migration) { described_class.new }
+
+ describe '#up' do
+ context 'when the table is already partitioned' do
+ before do
+ # `convert_table_to_first_list_partition` checks if it's being executed
+ # inside a transaction, but we're using transactional fixtures here so we
+ # need to tell it that it's not inside a transaction.
+ # We toggle the behavior depending on how many transactions we have open
+ # instead of just returning `false` because the migration could have the
+ # DDL transaction enabled.
+ #
+ open_transactions = ActiveRecord::Base.connection.open_transactions
+ allow(migration).to receive(:transaction_open?) do
+ ActiveRecord::Base.connection.open_transactions > open_transactions
+ end
+
+ migration.convert_table_to_first_list_partition(
+ table_name: :ci_builds_metadata,
+ partitioning_column: :partition_id,
+ parent_table_name: :p_ci_builds_metadata,
+ initial_partitioning_value: 100)
+ end
+
+ it 'skips the migration' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb b/spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb
new file mode 100644
index 00000000000..4d6f06eb146
--- /dev/null
+++ b/spec/migrations/20221025043930_change_default_value_on_password_last_changed_at_to_user_details_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ChangeDefaultValueOnPasswordLastChangedAtToUserDetails, :migration do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:users) { table(:users) }
+ let(:user_details) { table(:user_details) }
+
+ it 'correctly migrates up and down' do
+ user = create_user!(email: '1234@abc')
+ user_details.create!(user_id: user.id, provisioned_by_group_id: namespace.id)
+
+ expect(UserDetail.find_by(user_id: user.id).password_last_changed_at).to be_nil
+
+ migrate!
+
+ user = create_user!(email: 'abc@1234')
+ user_details.create!(user_id: user.id, provisioned_by_group_id: namespace.id)
+
+ expect(UserDetail.find_by(user_id: user.id).password_last_changed_at).not_to be_nil
+ end
+
+ private
+
+ def create_user!(name: "Example User", email: "user@example.com", user_type: nil)
+ users.create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ user_type: user_type,
+ confirmed_at: Time.current
+ )
+ end
+end
diff --git a/spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb b/spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb
new file mode 100644
index 00000000000..5f8467f9307
--- /dev/null
+++ b/spec/migrations/20221028022627_add_index_on_password_last_changed_at_to_user_details_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddIndexOnPasswordLastChangedAtToUserDetails, :migration do
+ let(:index_name) { 'index_user_details_on_password_last_changed_at' }
+
+ it 'correctly migrates up and down' do
+ expect(subject).not_to be_index_exists_by_name(:user_details, index_name)
+
+ migrate!
+
+ expect(subject).to be_index_exists_by_name(:user_details, index_name)
+ end
+end
diff --git a/spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb b/spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb
new file mode 100644
index 00000000000..3ae4287f3c4
--- /dev/null
+++ b/spec/migrations/20221101032521_add_default_preferred_language_to_application_settings_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddDefaultPreferredLanguageToApplicationSettings do
+ let(:application_setting) { table(:application_settings).create! }
+
+ describe "#up" do
+ it 'allows to read default_preferred_language field' do
+ migrate!
+
+ expect(application_setting.attributes.keys).to include('default_preferred_language')
+ expect(application_setting.default_preferred_language).to eq 'en'
+ end
+ end
+
+ describe "#down" do
+ it 'deletes default_preferred_language field' do
+ migrate!
+ schema_migrate_down!
+
+ expect(application_setting.attributes.keys).not_to include('default_preferred_language')
+ end
+ end
+end
diff --git a/spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb b/spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb
new file mode 100644
index 00000000000..e0370e48db6
--- /dev/null
+++ b/spec/migrations/20221101032600_add_text_limit_to_default_preferred_language_on_application_settings_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddTextLimitToDefaultPreferredLanguageOnApplicationSettings do
+ let(:application_setting) { table(:application_settings).create! }
+ let(:too_long_text) { SecureRandom.alphanumeric(described_class::MAXIMUM_LIMIT + 1) }
+
+ subject { application_setting.update_column(:default_preferred_language, too_long_text) }
+
+ describe "#up" do
+ it 'adds text limit to default_preferred_language' do
+ migrate!
+
+ expect { subject }.to raise_error ActiveRecord::StatementInvalid
+ end
+ end
+
+ describe "#down" do
+ it 'deletes text limit to default_preferred_language' do
+ migrate!
+ schema_migrate_down!
+
+ expect { subject }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb b/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb
new file mode 100644
index 00000000000..c55e4bcfba7
--- /dev/null
+++ b/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CreateNextCiPartitionsRecord, migration: :gitlab_ci do
+ let(:migration) { described_class.new }
+ let(:partitions) { table(:ci_partitions) }
+
+ describe '#up' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'creates next partitions record and resets the sequence' do
+ expect { migrate! }
+ .to change { partitions.where(id: 101).any? }
+ .from(false).to(true)
+
+ expect { partitions.create! }.not_to raise_error
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not create records' do
+ expect { migrate! }.not_to change(partitions, :count)
+ end
+ end
+ end
+
+ describe '#down' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'removes the record' do
+ migrate!
+
+ expect { migration.down }
+ .to change { partitions.where(id: 101).any? }
+ .from(true).to(false)
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true, false)
+ end
+
+ it 'does not remove the record' do
+ expect { migrate! }.to change(partitions, :count).by(1)
+
+ expect { migration.down }.not_to change(partitions, :count)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb b/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb
new file mode 100644
index 00000000000..99754d609ed
--- /dev/null
+++ b/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CreateSecondPartitionForBuildsMetadata, :migration do
+ let(:migration) { described_class.new }
+ let(:partitions) { table(:ci_partitions) }
+
+ describe '#up' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'creates a new partition' do
+ expect { migrate! }.to change { partitions_count }.by(1)
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not create the partition' do
+ expect { migrate! }.not_to change { partitions_count }
+ end
+ end
+ end
+
+ describe '#down' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'removes the partition' do
+ migrate!
+
+ expect { migration.down }.to change { partitions_count }.by(-1)
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not change the partitions count' do
+ migrate!
+
+ expect { migration.down }.not_to change { partitions_count }
+ end
+ end
+ end
+
+ def partitions_count
+ Gitlab::Database::PostgresPartition.for_parent_table(:p_ci_builds_metadata).size
+ end
+end
diff --git a/spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb b/spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb
new file mode 100644
index 00000000000..92ece81ffc8
--- /dev/null
+++ b/spec/migrations/cleanup_vulnerability_state_transitions_with_same_from_state_to_state_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CleanupVulnerabilityStateTransitionsWithSameFromStateToState, :migration do
+ let_it_be(:namespace) { table(:namespaces).create!(name: 'namespace', type: 'Group', path: 'namespace') }
+ let_it_be(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let_it_be(:project) do
+ table(:projects).create!(
+ path: 'project',
+ namespace_id: namespace.id,
+ project_namespace_id: namespace.id
+ )
+ end
+
+ let_it_be(:vulnerability) do
+ table(:vulnerabilities).create!(
+ project_id: project.id,
+ author_id: user.id,
+ title: 'test',
+ severity: 7,
+ confidence: 7,
+ report_type: 0
+ )
+ end
+
+ let_it_be(:state_transitions) { table(:vulnerability_state_transitions) }
+
+ let!(:state_transition_with_no_state_change) do
+ state_transitions.create!(
+ vulnerability_id: vulnerability.id,
+ from_state: 2,
+ to_state: 2
+ )
+ end
+
+ let!(:state_transition_with_state_change) do
+ state_transitions.create!(
+ vulnerability_id: vulnerability.id,
+ from_state: 1,
+ to_state: 2
+ )
+ end
+
+ it 'deletes state transitions with no state change' do
+ expect { migrate! }.to change(state_transitions, :count).from(2).to(1)
+ end
+end
diff --git a/spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb b/spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb
new file mode 100644
index 00000000000..259b175cd19
--- /dev/null
+++ b/spec/migrations/delete_migrate_shared_vulnerability_scanners_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+require_migration!
+
+RSpec.describe DeleteMigrateSharedVulnerabilityScanners, :migration do
+ let(:batched_background_migrations) { table(:batched_background_migrations) }
+ let(:batched_background_migration_jobs) { table(:batched_background_migration_jobs) }
+
+ let(:migration) do
+ batched_background_migrations.create!(created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ min_value: 1,
+ max_value: 1,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: 100,
+ interval: 300,
+ status: 3,
+ job_class_name: described_class::MIGRATION,
+ batch_class_name: "PrimaryKeyBatchingStrategy",
+ table_name: described_class::TABLE_NAME,
+ column_name: described_class::BATCH_COLUMN,
+ job_arguments: [],
+ pause_ms: 100,
+ max_batch_size: 1000,
+ gitlab_schema: "gitlab_main")
+ end
+
+ let(:jobs) do
+ Array.new(10) do
+ batched_background_migration_jobs.create!(batched_background_migration_id: migration.id,
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ min_value: 1,
+ max_value: 1,
+ batch_size: 1,
+ sub_batch_size: 1,
+ status: 0,
+ attempts: 0,
+ metrics: {},
+ pause_ms: 100)
+ end
+ end
+
+ describe "#up" do
+ it "deletes jobs" do
+ expect { migrate! }.to change(batched_background_migration_jobs, :count).from(jobs.count).to(0)
+ end
+
+ it "deletes the migration" do
+ expect { migrate! }.to change { batched_background_migrations.find_by(id: migration.id) }.from(migration).to(nil)
+ end
+
+ context "when background migration does not exist" do
+ before do
+ migration.destroy!
+ end
+
+ it "does not delete jobs" do
+ expect { migrate! }.not_to change(batched_background_migration_jobs, :count)
+ end
+
+ it "does not delete the migration" do
+ expect { migrate! }.not_to change { batched_background_migrations.find_by(id: migration.id) }
+ end
+ end
+ end
+end
diff --git a/spec/migrations/finalize_invalid_member_cleanup_spec.rb b/spec/migrations/finalize_invalid_member_cleanup_spec.rb
new file mode 100644
index 00000000000..a29a89c2396
--- /dev/null
+++ b/spec/migrations/finalize_invalid_member_cleanup_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FinalizeInvalidMemberCleanup, :migration do
+ let(:batched_migrations) { table(:batched_background_migrations) }
+
+ let_it_be(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ shared_examples 'finalizes the migration' do
+ it 'finalizes the migration' do
+ allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner|
+ expect(runner).to receive(:finalize).with('DestroyInvalidMembers', :members, :id, [])
+ end
+ end
+ end
+
+ context 'when migration is missing' do
+ it 'warns migration not found' do
+ expect(Gitlab::AppLogger)
+ .to receive(:warn).with(/Could not find batched background migration for the given configuration:/)
+
+ migrate!
+ end
+ end
+
+ context 'with migration present' do
+ let!(:destroy_invalid_member_migration) do
+ batched_migrations.create!(
+ job_class_name: 'DestroyInvalidMembers',
+ table_name: :members,
+ column_name: :id,
+ job_arguments: [],
+ interval: 2.minutes,
+ min_value: 1,
+ max_value: 2,
+ batch_size: 1000,
+ sub_batch_size: 200,
+ gitlab_schema: :gitlab_main,
+ status: 3 # finished
+ )
+ end
+
+ context 'when migration finished successfully' do
+ it 'does not raise exception' do
+ expect { migrate! }.not_to raise_error
+ end
+ end
+
+ context 'with different migration statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :description) do
+ 0 | 'paused'
+ 1 | 'active'
+ 4 | 'failed'
+ 5 | 'finalizing'
+ end
+
+ with_them do
+ before do
+ destroy_invalid_member_migration.update!(status: status)
+ end
+
+ it_behaves_like 'finalizes the migration'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/queue_backfill_user_details_fields_spec.rb b/spec/migrations/queue_backfill_user_details_fields_spec.rb
new file mode 100644
index 00000000000..388ac6d1bce
--- /dev/null
+++ b/spec/migrations/queue_backfill_user_details_fields_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueueBackfillUserDetailsFields do
+ let_it_be(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :users,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/queue_populate_projects_star_count_spec.rb b/spec/migrations/queue_populate_projects_star_count_spec.rb
new file mode 100644
index 00000000000..848136d8005
--- /dev/null
+++ b/spec/migrations/queue_populate_projects_star_count_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe QueuePopulateProjectsStarCount do
+ let_it_be(:batched_migration) { described_class::MIGRATION }
+
+ it 'schedules a new batched migration' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL
+ )
+ }
+ end
+ end
+end
diff --git a/spec/migrations/recount_epic_cache_counts_spec.rb b/spec/migrations/recount_epic_cache_counts_spec.rb
new file mode 100644
index 00000000000..56aa96cb40c
--- /dev/null
+++ b/spec/migrations/recount_epic_cache_counts_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RecountEpicCacheCounts, :migration do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules a batched background migration' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :epics,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb b/spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb
new file mode 100644
index 00000000000..e8253f39c68
--- /dev/null
+++ b/spec/migrations/reschedule_migrate_shared_vulnerability_scanners_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+require_migration!
+
+RSpec.describe RescheduleMigrateSharedVulnerabilityScanners, :migration do
+ include Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers
+
+ def connection
+ ApplicationRecord.connection
+ end
+
+ describe "#up" do
+ before do
+ migrate!
+ end
+
+ it "schedules" do
+ expect(described_class::MIGRATION).to have_scheduled_batched_migration(
+ table_name: described_class::TABLE_NAME,
+ column_name: described_class::BATCH_COLUMN,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ gitlab_schema: :gitlab_main
+ )
+ end
+ end
+
+ describe '#down' do
+ before do
+ schema_migrate_down!
+ end
+
+ it "deletes" do
+ expect(described_class::MIGRATION).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/sanitize_confidential_note_todos_spec.rb b/spec/migrations/sanitize_confidential_note_todos_spec.rb
new file mode 100644
index 00000000000..00dece82cc1
--- /dev/null
+++ b/spec/migrations/sanitize_confidential_note_todos_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe SanitizeConfidentialNoteTodos do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules a batched background migration' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :notes,
+ column_name: :id,
+ interval: described_class::DELAY_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ max_batch_size: described_class::MAX_BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/schedule_migrate_shared_vulnerability_scanners_spec.rb b/spec/migrations/schedule_migrate_shared_vulnerability_scanners_spec.rb
deleted file mode 100644
index f00d6568b67..00000000000
--- a/spec/migrations/schedule_migrate_shared_vulnerability_scanners_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-
-require_migration!
-
-RSpec.describe ScheduleMigrateSharedVulnerabilityScanners, :migration do
- describe "#up" do
- before do
- migrate!
- end
-
- it "schedules" do
- expect(described_class::MIGRATION).to have_scheduled_batched_migration(
- table_name: described_class::TABLE_NAME,
- column_name: described_class::BATCH_COLUMN,
- interval: described_class::DELAY_INTERVAL,
- batch_size: described_class::BATCH_SIZE,
- max_batch_size: described_class::BATCH_SIZE,
- sub_batch_size: described_class::SUB_BATCH_SIZE,
- gitlab_schema: :gitlab_main
- )
- end
-
- describe "ID range" do
- let(:expected_range) do
- { min_value: described_class::BATCH_MIN_VALUE,
- max_value: described_class::BATCH_MAX_VALUE }
- end
-
- subject do
- Gitlab::Database::BackgroundMigration::BatchedMigration
- .for_configuration(:gitlab_main,
- described_class::MIGRATION,
- described_class::TABLE_NAME,
- described_class::BATCH_COLUMN,
- [])
- end
-
- it "is set" do
- # The `have_scheduled_batched_migration` matcher accepts the
- # `batch_min_value` and `batch_max_value` keywords. However the respective
- # column names are `min_value` and `max_value`. Hence the matcher cannot
- # be used in this case, as it asserts the wrong attributes.
- expect(subject).to all(have_attributes(expected_range))
- end
- end
- end
-
- describe '#down' do
- before do
- schema_migrate_down!
- end
-
- it "deletes" do
- expect(described_class::MIGRATION).not_to have_scheduled_batched_migration
- end
- end
-end
diff --git a/spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb b/spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb
new file mode 100644
index 00000000000..761c0ef2fdb
--- /dev/null
+++ b/spec/migrations/set_email_confirmation_setting_from_send_user_confirmation_email_setting_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SetEmailConfirmationSettingFromSendUserConfirmationEmailSetting do
+ let(:migration) { described_class.new }
+ let(:application_settings_table) { table(:application_settings) }
+
+ describe '#up' do
+ context "when 'send_user_confirmation_email' is set to 'true'" do
+ it "updates 'email_confirmation_setting' to '2' (hard)" do
+ application_settings_table.create!(send_user_confirmation_email: true, email_confirmation_setting: 0)
+
+ migration.up
+
+ expect(application_settings_table.last.email_confirmation_setting).to eq 2
+ end
+ end
+
+ context "when 'send_user_confirmation_email' is set to 'false'" do
+ it "updates 'email_confirmation_setting' to '0' (off)" do
+ application_settings_table.create!(send_user_confirmation_email: false, email_confirmation_setting: 0)
+
+ migration.up
+
+ expect(application_settings_table.last.email_confirmation_setting).to eq 0
+ end
+ end
+ end
+
+ describe '#down' do
+ it "updates 'email_confirmation_setting' to default value: '0' (off)" do
+ application_settings_table.create!(send_user_confirmation_email: true, email_confirmation_setting: 2)
+
+ migration.down
+
+ expect(application_settings_table.last.email_confirmation_setting).to eq 0
+ end
+ end
+end
diff --git a/spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb b/spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb
new file mode 100644
index 00000000000..9a17f375f82
--- /dev/null
+++ b/spec/migrations/sync_new_amount_used_for_ci_namespace_monthly_usages_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe SyncNewAmountUsedForCiNamespaceMonthlyUsages, migration: :gitlab_ci do
+ let(:namespace_usages) { table(:ci_namespace_monthly_usages) }
+
+ before do
+ # Disabling the trigger temporarily to allow records being created with out-of-sync
+ # `new_amount_used` and `amount_used`. This will simulate existing records before
+ # we add the trigger.
+ ActiveRecord::Base.connection
+ .execute("ALTER TABLE ci_namespace_monthly_usages DISABLE TRIGGER sync_namespaces_amount_used_columns")
+
+ this_month = Time.now.utc.beginning_of_month
+ last_month = 1.month.ago.utc.beginning_of_month
+ last_year = 1.year.ago.utc.beginning_of_month
+
+ namespace_usages.create!(namespace_id: 1, date: last_year)
+ namespace_usages.create!(namespace_id: 1, date: this_month, amount_used: 10, new_amount_used: 0)
+ namespace_usages.create!(namespace_id: 1, date: last_month, amount_used: 20, new_amount_used: 0)
+
+ namespace_usages.create!(namespace_id: 2, date: last_year)
+ namespace_usages.create!(namespace_id: 2, date: this_month, amount_used: 30, new_amount_used: 0)
+ namespace_usages.create!(namespace_id: 2, date: last_month, amount_used: 40, new_amount_used: 0)
+
+ ActiveRecord::Base.connection
+ .execute("ALTER TABLE ci_namespace_monthly_usages ENABLE TRIGGER sync_namespaces_amount_used_columns")
+ end
+
+ it 'updates `new_amount_used` with values from `amount_used`' do
+ expect(namespace_usages.where(new_amount_used: 0).count).to eq(6)
+
+ migrate!
+
+ expect(namespace_usages.where(new_amount_used: 0).count).to eq(2)
+ expect(namespace_usages.order(:id).pluck(:new_amount_used))
+ .to contain_exactly(0, 0, 10, 20, 30, 40)
+ end
+end
diff --git a/spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb b/spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb
new file mode 100644
index 00000000000..8d45f1107ea
--- /dev/null
+++ b/spec/migrations/sync_new_amount_used_for_ci_project_monthly_usages_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe SyncNewAmountUsedForCiProjectMonthlyUsages, migration: :gitlab_ci do
+ let(:project_usages) { table(:ci_project_monthly_usages) }
+
+ before do
+ # Disabling the trigger temporarily to allow records being created with out-of-sync
+ # `new_amount_used` and `amount_used`. This will simulate existing records before
+ # we add the trigger.
+ ActiveRecord::Base.connection
+ .execute("ALTER TABLE ci_project_monthly_usages DISABLE TRIGGER sync_projects_amount_used_columns")
+
+ this_month = Time.now.utc.beginning_of_month
+ last_month = 1.month.ago.utc.beginning_of_month
+ last_year = 1.year.ago.utc.beginning_of_month
+
+ project_usages.create!(project_id: 1, date: last_year)
+ project_usages.create!(project_id: 1, date: this_month, amount_used: 10, new_amount_used: 0)
+ project_usages.create!(project_id: 1, date: last_month, amount_used: 20, new_amount_used: 0)
+
+ project_usages.create!(project_id: 2, date: last_year)
+ project_usages.create!(project_id: 2, date: this_month, amount_used: 30, new_amount_used: 0)
+ project_usages.create!(project_id: 2, date: last_month, amount_used: 40, new_amount_used: 0)
+
+ ActiveRecord::Base.connection
+ .execute("ALTER TABLE ci_project_monthly_usages ENABLE TRIGGER sync_projects_amount_used_columns")
+ end
+
+ it 'updates `new_amount_used` with values from `amount_used`' do
+ expect(project_usages.where(new_amount_used: 0).count).to eq(6)
+
+ migrate!
+
+ expect(project_usages.where(new_amount_used: 0).count).to eq(2)
+ expect(project_usages.order(:id).pluck(:new_amount_used))
+ .to contain_exactly(0, 0, 10, 20, 30, 40)
+ end
+end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index 5d316f7cff2..3665f13015e 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -260,7 +260,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
redis.set("session:gitlab:#{rack_session.private_id}", '')
redis.set(session_key, serialized_session)
- redis.sadd(lookup_key, active_session_lookup_key)
+ redis.sadd?(lookup_key, active_session_lookup_key)
end
end
@@ -338,7 +338,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
session_private_id = Rack::Session::SessionId.new(session_public_id).private_id
active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(key_name(user.id, session_private_id), dump_session(active_session))
- redis.sadd(lookup_key, session_private_id)
+ redis.sadd?(lookup_key, session_private_id)
end
# setup for unrelated user
@@ -347,7 +347,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(key_name(unrelated_user_id, session_private_id), dump_session(active_session))
- redis.sadd(described_class.lookup_key_name(unrelated_user_id), session_private_id)
+ redis.sadd?(described_class.lookup_key_name(unrelated_user_id), session_private_id)
end
end
@@ -372,7 +372,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
Gitlab::Redis::Sessions.with do |redis|
redis.set(key_name(user.id, impersonated_session_id),
dump_session(ActiveSession.new(session_id: Rack::Session::SessionId.new(impersonated_session_id), is_impersonated: true)))
- redis.sadd(lookup_key, impersonated_session_id)
+ redis.sadd?(lookup_key, impersonated_session_id)
end
expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2)
@@ -418,8 +418,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
it 'removes obsolete lookup entries' do
Gitlab::Redis::Sessions.with do |redis|
redis.set(session_key, '')
- redis.sadd(lookup_key, current_session_id)
- redis.sadd(lookup_key, '59822c7d9fcdfa03725eff41782ad97d')
+ redis.sadd(lookup_key, [current_session_id, '59822c7d9fcdfa03725eff41782ad97d'])
end
ActiveSession.cleanup(user)
@@ -445,7 +444,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
key_name(user.id, number),
dump_session(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago))
)
- redis.sadd(lookup_key, number.to_s)
+ redis.sadd?(lookup_key, number.to_s)
end
end
end
@@ -477,7 +476,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
it 'removes obsolete lookup entries even without active session' do
Gitlab::Redis::Sessions.with do |redis|
- redis.sadd(lookup_key, "#{max_number_of_sessions_plus_two + 1}")
+ redis.sadd?(lookup_key, (max_number_of_sessions_plus_two + 1).to_s)
end
ActiveSession.cleanup(user)
@@ -534,7 +533,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
key_name(user.id, number),
dump_session(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago))
)
- redis.sadd(lookup_key, number.to_s)
+ redis.sadd?(lookup_key, number.to_s)
end
end
end
@@ -601,11 +600,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
dump_session(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago))
)
- redis.sadd(lookup_key, number.to_s)
+ redis.sadd?(lookup_key, number.to_s)
end
- redis.sadd(lookup_key, (active_count + 1).to_s)
- redis.sadd(lookup_key, (active_count + 2).to_s)
+ redis.sadd?(lookup_key, [(active_count + 1).to_s, (active_count + 2).to_s])
end
end
diff --git a/spec/models/alert_management/http_integration_spec.rb b/spec/models/alert_management/http_integration_spec.rb
index f88a66a7c27..b453b3a82e0 100644
--- a/spec/models/alert_management/http_integration_spec.rb
+++ b/spec/models/alert_management/http_integration_spec.rb
@@ -13,6 +13,11 @@ RSpec.describe AlertManagement::HttpIntegration do
it { is_expected.to belong_to(:project) }
end
+ describe 'default values' do
+ it { expect(described_class.new.endpoint_identifier).to be_present }
+ it { expect(described_class.new(endpoint_identifier: 'test').endpoint_identifier).to eq('test') }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
@@ -124,10 +129,6 @@ RSpec.describe AlertManagement::HttpIntegration do
end
context 'when unsaved' do
- context 'when unassigned' do
- it_behaves_like 'valid token'
- end
-
context 'when assigned' do
include_context 'assign token', 'random_token'
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 2817e177d28..9d84279a75e 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -10,6 +10,20 @@ RSpec.describe Appearance do
it { is_expected.to have_many(:uploads) }
+ describe 'default values' do
+ subject(:appearance) { described_class.new }
+
+ it { expect(appearance.title).to eq('') }
+ it { expect(appearance.description).to eq('') }
+ it { expect(appearance.new_project_guidelines).to eq('') }
+ it { expect(appearance.profile_image_guidelines).to eq('') }
+ it { expect(appearance.header_message).to eq('') }
+ it { expect(appearance.footer_message).to eq('') }
+ it { expect(appearance.message_background_color).to eq('#E75E40') }
+ it { expect(appearance.message_font_color).to eq('#FFFFFF') }
+ it { expect(appearance.email_header_and_footer_enabled).to eq(false) }
+ end
+
describe '#single_appearance_row' do
it 'adds an error when more than 1 row exists' do
create(:appearance)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 77bb6b502b5..fd86a784b2d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -17,6 +17,14 @@ RSpec.describe ApplicationSetting do
it { expect(setting.uuid).to be_present }
it { expect(setting).to have_db_column(:auto_devops_enabled) }
+ describe 'default values' do
+ subject(:setting) { described_class.new }
+
+ it { expect(setting.id).to eq(1) }
+ it { expect(setting.repository_storages_weighted).to eq({}) }
+ it { expect(setting.kroki_formats).to eq({}) }
+ end
+
describe 'validations' do
let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' }
@@ -203,6 +211,9 @@ RSpec.describe ApplicationSetting do
it { is_expected.to allow_value([]).for(:valid_runner_registrars) }
it { is_expected.to allow_value(%w(project group)).for(:valid_runner_registrars) }
+ it { is_expected.to allow_value(http).for(:jira_connect_proxy_url) }
+ it { is_expected.to allow_value(https).for(:jira_connect_proxy_url) }
+
context 'when deactivate_dormant_users is enabled' do
before do
stub_application_setting(deactivate_dormant_users: true)
@@ -261,6 +272,7 @@ RSpec.describe ApplicationSetting do
end
it { is_expected.not_to allow_value('http://localhost:9000').for(:grafana_url) }
+ it { is_expected.not_to allow_value('http://localhost:9000').for(:jira_connect_proxy_url) }
end
context 'with invalid grafana URL' do
@@ -1121,6 +1133,11 @@ RSpec.describe ApplicationSetting do
it { is_expected.to validate_presence_of(:error_tracking_api_url) }
end
end
+
+ context 'for default_preferred_language' do
+ it { is_expected.to allow_value(*Gitlab::I18n.available_locales).for(:default_preferred_language) }
+ it { is_expected.not_to allow_value(nil, '', 'invalid_locale').for(:default_preferred_language) }
+ end
end
context 'restrict creating duplicates' do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index b0bfdabe13c..8fdc9852f6e 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -27,6 +27,13 @@ RSpec.describe BroadcastMessage do
it { is_expected.to validate_inclusion_of(:target_access_levels).in_array(described_class::ALLOWED_TARGET_ACCESS_LEVELS) }
end
+ describe 'default values' do
+ subject(:message) { described_class.new }
+
+ it { expect(message.color).to eq('#E75E40') }
+ it { expect(message.font).to eq('#FFFFFF') }
+ end
+
shared_examples 'time constrainted' do |broadcast_type|
it 'returns message if time match' do
message = create(:broadcast_message, broadcast_type: broadcast_type)
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 44a6bec0130..df24c92149d 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -27,6 +27,8 @@ RSpec.describe Ci::Bridge do
it_behaves_like 'has ID tokens', :ci_bridge
+ it_behaves_like 'a retryable job'
+
it 'has one downstream pipeline' do
expect(bridge).to have_one(:sourced_pipeline)
expect(bridge).to have_one(:downstream_pipeline)
@@ -35,18 +37,8 @@ RSpec.describe Ci::Bridge do
describe '#retryable?' do
let(:bridge) { create(:ci_bridge, :success) }
- it 'returns true' do
- expect(bridge.retryable?).to eq(true)
- end
-
- context 'without ci_recreate_downstream_pipeline ff' do
- before do
- stub_feature_flags(ci_recreate_downstream_pipeline: false)
- end
-
- it 'returns false' do
- expect(bridge.retryable?).to eq(false)
- end
+ it 'returns false' do
+ expect(bridge.retryable?).to eq(false)
end
end
@@ -204,6 +196,8 @@ RSpec.describe Ci::Bridge do
end
describe '#downstream_variables' do
+ subject(:downstream_variables) { bridge.downstream_variables }
+
it 'returns variables that are going to be passed downstream' do
expect(bridge.downstream_variables)
.to include(key: 'BRIDGE', value: 'cross')
@@ -309,7 +303,7 @@ RSpec.describe Ci::Bridge do
end
context 'when the pipeline runs from a pipeline schedule' do
- let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) }
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
let(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
let(:options) do
@@ -328,6 +322,79 @@ RSpec.describe Ci::Bridge do
end
end
end
+
+ context 'when using raw variables' do
+ let(:options) do
+ {
+ trigger: {
+ project: 'my/project',
+ branch: 'master',
+ forward: { yaml_variables: true,
+ pipeline_variables: true }.compact
+ }
+ }
+ end
+
+ let(:yaml_variables) do
+ [
+ {
+ key: 'VAR6',
+ value: 'value6 $VAR1'
+ },
+ {
+ key: 'VAR7',
+ value: 'value7 $VAR1',
+ raw: true
+ }
+ ]
+ end
+
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
+ let(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) }
+
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'VAR1', value: 'value1')
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'VAR2', value: 'value2 $VAR1')
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'VAR3', value: 'value3 $VAR1', raw: true)
+
+ pipeline_schedule.variables.create!(key: 'VAR4', value: 'value4 $VAR1')
+ pipeline_schedule.variables.create!(key: 'VAR5', value: 'value5 $VAR1', raw: true)
+
+ bridge.yaml_variables.concat(yaml_variables)
+ end
+
+ it 'expands variables according to their raw attributes' do
+ expect(downstream_variables).to contain_exactly(
+ { key: 'BRIDGE', value: 'cross' },
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2 value1' },
+ { key: 'VAR3', value: 'value3 $VAR1', raw: true },
+ { key: 'VAR4', value: 'value4 value1' },
+ { key: 'VAR5', value: 'value5 $VAR1', raw: true },
+ { key: 'VAR6', value: 'value6 value1' },
+ { key: 'VAR7', value: 'value7 $VAR1', raw: true }
+ )
+ end
+
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
+ before do
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
+ end
+
+ it 'ignores the raw attribute' do
+ expect(downstream_variables).to contain_exactly(
+ { key: 'BRIDGE', value: 'cross' },
+ { key: 'VAR1', value: 'value1' },
+ { key: 'VAR2', value: 'value2 value1' },
+ { key: 'VAR3', value: 'value3 value1' },
+ { key: 'VAR4', value: 'value4 value1' },
+ { key: 'VAR5', value: 'value5 value1' },
+ { key: 'VAR6', value: 'value6 value1' },
+ { key: 'VAR7', value: 'value7 value1' }
+ )
+ end
+ end
+ end
end
describe 'metadata support' do
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 16cff72db64..e728ce0f474 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -182,4 +182,36 @@ RSpec.describe Ci::BuildMetadata do
end
end
end
+
+ describe 'routing table switch' do
+ context 'with ff disabled' do
+ before do
+ stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: false)
+ end
+
+ it 'uses the legacy table' do
+ expect(described_class.table_name).to eq('ci_builds_metadata')
+ end
+ end
+
+ context 'with ff enabled' do
+ before do
+ stub_feature_flags(ci_partitioning_use_ci_builds_metadata_routing_table: true)
+ end
+
+ it 'uses the routing table' do
+ expect(described_class.table_name).to eq('p_ci_builds_metadata')
+ end
+ end
+ end
+
+ context 'jsonb fields serialization' do
+ it 'changing other fields does not change config_options' do
+ expect { metadata.id = metadata.id }.not_to change(metadata, :changes)
+ end
+
+ it 'accessing config_options does not change it' do
+ expect { metadata.config_options }.not_to change(metadata, :changes)
+ end
+ end
end
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index ed5ed456d7b..9bb8a1bd626 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe Ci::BuildRunnerSession, model: true do
let(:specification) { subject.service_specification(service: service, port: port, path: path, subprotocols: subprotocols) }
it 'returns service proxy url' do
- expect(specification[:url]).to eq "https://localhost/proxy/#{service}/#{port}/#{path}"
+ expect(specification[:url]).to eq "https://gitlab.example.com/proxy/#{service}/#{port}/#{path}"
end
it 'returns default service proxy websocket subprotocol' do
@@ -89,7 +89,7 @@ RSpec.describe Ci::BuildRunnerSession, model: true do
let(:port) { nil }
it 'uses the default port name' do
- expect(specification[:url]).to eq "https://localhost/proxy/#{service}/default_port/#{path}"
+ expect(specification[:url]).to eq "https://gitlab.example.com/proxy/#{service}/default_port/#{path}"
end
end
@@ -97,7 +97,7 @@ RSpec.describe Ci::BuildRunnerSession, model: true do
let(:service) { '' }
it 'uses the service name "build" as default' do
- expect(specification[:url]).to eq "https://localhost/proxy/build/#{port}/#{path}"
+ expect(specification[:url]).to eq "https://gitlab.example.com/proxy/build/#{port}/#{path}"
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 9713734e97a..813b4b3faa6 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -25,6 +25,7 @@ RSpec.describe Ci::Build do
it { is_expected.to have_many(:needs) }
it { is_expected.to have_many(:sourced_pipelines) }
+ it { is_expected.to have_one(:sourced_pipeline) }
it { is_expected.to have_many(:job_variables) }
it { is_expected.to have_many(:report_results) }
it { is_expected.to have_many(:pages_deployments) }
@@ -86,6 +87,8 @@ RSpec.describe Ci::Build do
it_behaves_like 'has ID tokens', :ci_build
+ it_behaves_like 'a retryable job'
+
describe '.manual_actions' do
let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) }
let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) }
@@ -605,8 +608,8 @@ RSpec.describe Ci::Build do
end
end
- describe '#prevent_rollback_deployment?' do
- subject { build.prevent_rollback_deployment? }
+ describe '#outdated_deployment?' do
+ subject { build.outdated_deployment? }
let(:build) { create(:ci_build, :created, :with_deployment, project: project, environment: 'production') }
@@ -624,21 +627,33 @@ RSpec.describe Ci::Build do
it { expect(subject).to be_falsey }
end
- context 'when deployment cannot rollback' do
+ context 'when build is not an outdated deployment' do
before do
- expect(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
+ allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
end
it { expect(subject).to be_falsey }
end
- context 'when build can prevent rollback deployment' do
+ context 'when build is older than the latest deployment and still pending status' do
before do
- expect(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
+ allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
end
it { expect(subject).to be_truthy }
end
+
+ context 'when build is older than the latest deployment but succeeded once' do
+ let(:build) { create(:ci_build, :success, :with_deployment, project: project, environment: 'production') }
+
+ before do
+ allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
+ end
+
+ it 'returns false for allowing rollback' do
+ expect(subject).to be_falsey
+ end
+ end
end
describe '#schedulable?' do
@@ -1316,6 +1331,8 @@ RSpec.describe Ci::Build do
end
context 'hide build token' do
+ let_it_be(:build) { FactoryBot.build(:ci_build, pipeline: pipeline) }
+
let(:data) { "new #{build.token} data" }
it { is_expected.to match(/^new x+ data$/) }
@@ -1606,8 +1623,8 @@ RSpec.describe Ci::Build do
end
describe 'environment' do
- describe '#has_environment?' do
- subject { build.has_environment? }
+ describe '#has_environment_keyword?' do
+ subject { build.has_environment_keyword? }
context 'when environment is defined' do
before do
@@ -1751,7 +1768,7 @@ RSpec.describe Ci::Build do
context 'and start action is defined' do
before do
- build.update!(options: { environment: { action: 'start' } } )
+ build.update!(options: { environment: { action: 'start' } })
end
it { is_expected.to be_truthy }
@@ -1781,7 +1798,7 @@ RSpec.describe Ci::Build do
context 'and stop action is defined' do
before do
- build.update!(options: { environment: { action: 'stop' } } )
+ build.update!(options: { environment: { action: 'stop' } })
end
it { is_expected.to be_truthy }
@@ -2783,16 +2800,6 @@ RSpec.describe Ci::Build do
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)
- end
-
- it 'CI_JOB_JWT is not included' do
- expect(subject.pluck(:key)).not_to include('CI_JOB_JWT')
- end
- end
-
context 'when CI_JOB_JWT generation fails' do
[
OpenSSL::PKey::RSAError,
@@ -3806,6 +3813,26 @@ RSpec.describe Ci::Build do
build.enqueue
end
+
+ it 'assigns the token' do
+ expect { build.enqueue }.to change(build, :token).from(nil).to(an_instance_of(String))
+ end
+
+ context 'with ci_assign_job_token_on_scheduling disabled' do
+ before do
+ stub_feature_flags(ci_assign_job_token_on_scheduling: false)
+ end
+
+ it 'assigns the token on creation' do
+ expect(build.token).to be_present
+ end
+
+ it 'does not change the token when enqueuing' do
+ expect { build.enqueue }.not_to change(build, :token)
+
+ expect(build).to be_pending
+ end
+ end
end
describe 'state transition: pending: :running' do
@@ -5083,6 +5110,60 @@ RSpec.describe Ci::Build do
context 'when CI_DEBUG_TRACE is not in variables' do
it { is_expected.to eq false }
end
+
+ context 'when CI_DEBUG_SERVICES=true is in variables' do
+ context 'when in instance variables' do
+ before do
+ create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: 'true')
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when in group variables' do
+ before do
+ create(:ci_group_variable, key: 'CI_DEBUG_SERVICES', value: 'true', group: project.group)
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when in pipeline variables' do
+ before do
+ create(:ci_pipeline_variable, key: 'CI_DEBUG_SERVICES', value: 'true', pipeline: pipeline)
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when in project variables' do
+ before do
+ create(:ci_variable, key: 'CI_DEBUG_SERVICES', value: 'true', project: project)
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when in job variables' do
+ before do
+ create(:ci_job_variable, key: 'CI_DEBUG_SERVICES', value: 'true', job: build)
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'when in yaml variables' do
+ before do
+ build.update!(yaml_variables: [{ key: :CI_DEBUG_SERVICES, value: 'true' }])
+ end
+
+ it { is_expected.to eq true }
+ end
+ end
+
+ context 'when CI_DEBUG_SERVICES is not in variables' do
+ it { is_expected.to eq false }
+ end
end
describe '#drop_with_exit_code!' do
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index e08fe196d65..3328ed62f15 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -29,6 +29,11 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git
}[data_store]
end
+ describe 'default attributes' do
+ it { expect(described_class.new.data_store).to eq('redis_trace_chunks') }
+ it { expect(described_class.new(data_store: :fog).data_store).to eq('fog') }
+ end
+
describe 'chunk creation' do
let(:metrics) { spy('metrics') }
diff --git a/spec/models/ci/build_trace_spec.rb b/spec/models/ci/build_trace_spec.rb
index f2df4874b84..907b49dc180 100644
--- a/spec/models/ci/build_trace_spec.rb
+++ b/spec/models/ci/build_trace_spec.rb
@@ -38,9 +38,9 @@ RSpec.describe Ci::BuildTrace do
let(:data) { StringIO.new("UTF-8 dashes here: ───\n🐤🐤🐤🐤\xF0\x9F\x90\n") }
it 'returns valid UTF-8 data', :aggregate_failures do
- expect(subject.lines[0]).to eq({ offset: 0, content: [{ text: 'UTF-8 dashes here: ───' }] } )
+ expect(subject.lines[0]).to eq({ offset: 0, content: [{ text: 'UTF-8 dashes here: ───' }] })
# Each of the dashes is 3 bytes, so we get 19 + 9 + 1 = 29
- expect(subject.lines[1]).to eq({ offset: 29, content: [{ text: '🐤🐤🐤🐤�' }] } )
+ expect(subject.lines[1]).to eq({ offset: 29, content: [{ text: '🐤🐤🐤🐤�' }] })
end
end
end
diff --git a/spec/models/ci/pipeline_metadata_spec.rb b/spec/models/ci/pipeline_metadata_spec.rb
index 0704cbc8ec1..977c90bcc2a 100644
--- a/spec/models/ci/pipeline_metadata_spec.rb
+++ b/spec/models/ci/pipeline_metadata_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Ci::PipelineMetadata do
it { is_expected.to belong_to(:pipeline) }
describe 'validations' do
- it { is_expected.to validate_length_of(:title).is_at_least(1).is_at_most(255) }
+ it { is_expected.to validate_length_of(:name).is_at_least(1).is_at_most(255) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:pipeline) }
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 42b5210a080..2c945898e61 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -50,7 +50,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to respond_to :git_author_full_text }
it { is_expected.to respond_to :short_sha }
it { is_expected.to delegate_method(:full_path).to(:project).with_prefix }
- it { is_expected.to delegate_method(:title).to(:pipeline_metadata).allow_nil }
+ it { is_expected.to delegate_method(:name).to(:pipeline_metadata).allow_nil }
describe 'validations' do
it { is_expected.to validate_presence_of(:sha) }
@@ -166,7 +166,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it do
pipeline.status = from_status.to_s
- if from_status != to_status
+ if from_status != to_status || success_to_success?
expect(pipeline.set_status(to_status.to_s))
.to eq(true)
else
@@ -174,6 +174,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
.to eq(false), "loopback transitions are not allowed"
end
end
+
+ private
+
+ def success_to_success?
+ from_status == :success && to_status == :success
+ end
end
end
@@ -1601,7 +1607,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
describe 'track artifact report' do
- let(:pipeline) { create(:ci_pipeline, :running, :with_test_reports, status: :running, user: create(:user)) }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline, :running, :with_test_reports, :with_coverage_reports, status: :running, user: user) }
context 'when transitioning to completed status' do
%i[drop! skip! succeed! cancel!].each do |command|
@@ -1613,11 +1620,29 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
context 'when pipeline retried from failed to success', :clean_gitlab_redis_shared_state do
- let(:test_event_name) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_1) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_2) { 'i_testing_coverage_report_uploaded' }
let(:start_time) { 1.week.ago }
let(:end_time) { 1.week.from_now }
- it 'counts only one report' do
+ it 'counts only one test event report' do
+ expect(Ci::JobArtifacts::TrackArtifactReportWorker).to receive(:perform_async).with(pipeline.id).twice.and_call_original
+
+ Sidekiq::Testing.inline! do
+ pipeline.drop!
+ pipeline.run!
+ pipeline.succeed!
+ end
+
+ unique_pipeline_pass = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
+ event_names: test_event_name_1,
+ start_date: start_time,
+ end_date: end_time
+ )
+ expect(unique_pipeline_pass).to eq(1)
+ end
+
+ it 'counts only one coverage event report' do
expect(Ci::JobArtifacts::TrackArtifactReportWorker).to receive(:perform_async).with(pipeline.id).twice.and_call_original
Sidekiq::Testing.inline! do
@@ -1627,7 +1652,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end
unique_pipeline_pass = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
- event_names: test_event_name,
+ event_names: test_event_name_2,
start_date: start_time,
end_date: end_time
)
@@ -4173,7 +4198,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let(:pipeline) { create(:ci_pipeline) }
let!(:old_job) { create(:ci_build, name: 'rspec', retried: true, pipeline: pipeline) }
let!(:job_without_artifacts) { create(:ci_build, name: 'rspec', pipeline: pipeline) }
- let!(:expected_job) { create(:ci_build, :artifacts, name: 'rspec', pipeline: pipeline ) }
+ let!(:expected_job) { create(:ci_build, :artifacts, name: 'rspec', pipeline: pipeline) }
let!(:different_job) { create(:ci_build, name: 'deploy', pipeline: pipeline) }
subject { pipeline.find_job_with_archive_artifacts('rspec') }
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index a199111b1e3..e62e5f84a6d 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -52,7 +52,11 @@ RSpec.describe Ci::Processable do
let_it_be(:internal_job_variable) { create(:ci_job_variable, job: processable) }
- let(:clone_accessors) { ::Ci::Build.clone_accessors.without(::Ci::Build.extra_accessors) }
+ let(:clone_accessors) do
+ %i[pipeline project ref tag options name allow_failure stage stage_idx trigger_request yaml_variables
+ when environment coverage_regex description tag_list protected needs_attributes job_variables_attributes
+ resource_group scheduling_type ci_stage partition_id id_tokens]
+ end
let(:reject_accessors) do
%i[id status user token_encrypted coverage runner artifacts_expire_at
@@ -77,13 +81,14 @@ RSpec.describe Ci::Processable do
commit_id deployment erased_by_id project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
- sourced_pipelines artifacts_file_store artifacts_metadata_store
+ sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
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
queuing_entry runtime_metadata trace_metadata
- dast_site_profile dast_scanner_profile stage_id].freeze
+ dast_site_profile dast_scanner_profile stage_id dast_site_profiles_build
+ dast_scanner_profiles_build].freeze
end
before_all do
@@ -177,10 +182,7 @@ RSpec.describe Ci::Processable do
Ci::Build.attribute_names.map(&:to_sym) +
Ci::Build.attribute_aliases.keys.map(&:to_sym) +
Ci::Build.reflect_on_all_associations.map(&:name) +
- [:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens] -
- # ToDo: Move EE accessors to ee/
- ::Ci::Build.extra_accessors -
- [:dast_site_profiles_build, :dast_scanner_profiles_build]
+ [:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens]
current_accessors.uniq!
@@ -284,12 +286,6 @@ RSpec.describe Ci::Processable do
end
end
- context 'when the processable is a bridge' do
- subject(:processable) { create(:ci_bridge, pipeline: pipeline) }
-
- it_behaves_like 'retryable processable'
- end
-
context 'when the processable is a build' do
subject(:processable) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb
index 20f64d40865..4413bd8e98b 100644
--- a/spec/models/ci/secure_file_spec.rb
+++ b/spec/models/ci/secure_file_spec.rb
@@ -21,6 +21,15 @@ RSpec.describe Ci::SecureFile do
subject { build(:ci_secure_file, project: create(:project)) }
end
+ describe 'default attributes' do
+ before do
+ allow(Ci::SecureFileUploader).to receive(:default_store).and_return(5)
+ end
+
+ it { expect(described_class.new.file_store).to eq(5) }
+ it { expect(described_class.new(file_store: 3).file_store).to eq(3) }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:checksum) }
it { is_expected.to validate_presence_of(:file_store) }
@@ -131,7 +140,7 @@ RSpec.describe Ci::SecureFile do
describe '#update_metadata!' do
it 'assigns the expected metadata when a parsable file is supplied' do
file = create(:ci_secure_file, name: 'file1.cer',
- file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.cer') ))
+ file: CarrierWaveStringFile.new(fixture_file('ci_secure_files/sample.cer')))
file.update_metadata!
expect(file.expires_at).to eq(DateTime.parse('2022-04-26 19:20:40'))
diff --git a/spec/models/ci/sources/pipeline_spec.rb b/spec/models/ci/sources/pipeline_spec.rb
index 732dd5c3df3..fdc1c111c40 100644
--- a/spec/models/ci/sources/pipeline_spec.rb
+++ b/spec/models/ci/sources/pipeline_spec.rb
@@ -20,14 +20,14 @@ RSpec.describe Ci::Sources::Pipeline do
context 'loose foreign key on ci_sources_pipelines.source_project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
- let!(:parent) { create(:project) }
+ let!(:parent) { create(:project, namespace: create(:group)) }
let!(:model) { create(:ci_sources_pipeline, source_project: parent) }
end
end
context 'loose foreign key on ci_sources_pipelines.project_id' do
it_behaves_like 'cleanup by a loose foreign key' do
- let!(:parent) { create(:project) }
+ let!(:parent) { create(:project, namespace: create(:group)) }
let!(:model) { create(:ci_sources_pipeline, project: parent) }
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index dd9af33a562..b392ab4ed11 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -389,7 +389,7 @@ RSpec.describe Ci::Stage, :models do
end
context 'without pipeline' do
- subject(:stage) { build(:ci_stage, pipeline: nil) }
+ subject(:stage) { build(:ci_stage, pipeline: nil, project: build_stubbed(:project)) }
it { is_expected.to validate_presence_of(:partition_id) }
diff --git a/spec/models/ci/trigger_request_spec.rb b/spec/models/ci/trigger_request_spec.rb
index 0d462741089..a6e8e8496ac 100644
--- a/spec/models/ci/trigger_request_spec.rb
+++ b/spec/models/ci/trigger_request_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Ci::TriggerRequest do
describe 'validation' do
it 'be invalid if saving a variable' do
- trigger = build(:ci_trigger_request, variables: { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
+ trigger = build(:ci_trigger_request, variables: { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' })
expect(trigger).not_to be_valid
end
diff --git a/spec/models/ci/unit_test_spec.rb b/spec/models/ci/unit_test_spec.rb
index b3180492a36..e35a4ce40da 100644
--- a/spec/models/ci/unit_test_spec.rb
+++ b/spec/models/ci/unit_test_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Ci::UnitTest do
it_behaves_like 'cleanup by a loose foreign key' do
- let!(:parent) { create(:project) }
+ let!(:parent) { create(:project, namespace: create(:group)) }
let!(:model) { create(:ci_unit_test, project: parent) }
end
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
index 05ab8c4108e..427a99efadd 100644
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ b/spec/models/clusters/applications/cert_manager_spec.rb
@@ -10,6 +10,11 @@ RSpec.describe Clusters::Applications::CertManager do
include_examples 'cluster application version specs', :clusters_applications_cert_manager
include_examples 'cluster application initial status specs'
+ describe 'default values' do
+ it { expect(cert_manager.version).to eq(described_class::VERSION) }
+ it { expect(cert_manager.email).to eq("admin@example.com") }
+ end
+
describe '#can_uninstall?' do
subject { cert_manager.can_uninstall? }
diff --git a/spec/models/clusters/applications/crossplane_spec.rb b/spec/models/clusters/applications/crossplane_spec.rb
index 7082576028b..d1abaa52c7f 100644
--- a/spec/models/clusters/applications/crossplane_spec.rb
+++ b/spec/models/clusters/applications/crossplane_spec.rb
@@ -14,6 +14,11 @@ RSpec.describe Clusters::Applications::Crossplane do
it { is_expected.to validate_presence_of(:stack) }
end
+ describe 'default values' do
+ it { expect(subject.version).to eq(described_class::VERSION) }
+ it { expect(subject.stack).to be_empty }
+ end
+
describe '#can_uninstall?' do
subject { crossplane.can_uninstall? }
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index 5212e321a55..1b8be92475a 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Clusters::Applications::Helm do
include_examples 'cluster application core specs', :clusters_applications_helm
+ describe 'default values' do
+ it { expect(subject.version).to eq(Gitlab::Kubernetes::Helm::V2::BaseCommand::HELM_VERSION) }
+ end
+
describe '.available' do
subject { described_class.available }
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index e16d97c42d9..e5caa11452e 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -18,6 +18,11 @@ RSpec.describe Clusters::Applications::Ingress do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
+ describe 'default values' do
+ it { expect(subject.ingress_type).to eq("nginx") }
+ it { expect(subject.version).to eq(described_class::VERSION) }
+ end
+
describe '#can_uninstall?' do
subject { ingress.can_uninstall? }
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index e7de2d24334..9336d2352f8 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -10,6 +10,10 @@ RSpec.describe Clusters::Applications::Jupyter do
it { is_expected.to belong_to(:oauth_application) }
+ describe 'default values' do
+ it { expect(subject.version).to eq(described_class::VERSION) }
+ end
+
describe '#can_uninstall?' do
let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index d0e470bfa42..3914450339a 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -21,6 +21,10 @@ RSpec.describe Clusters::Applications::Knative do
it { is_expected.to have_one(:serverless_domain_cluster).class_name('::Serverless::DomainCluster').with_foreign_key('clusters_applications_knative_id').inverse_of(:knative) }
end
+ describe 'default values' do
+ it { expect(subject.version).to eq(described_class::VERSION) }
+ end
+
describe 'when cloud run is enabled' do
let(:cluster) { create(:cluster, :provided_by_gcp, :cloud_run_enabled) }
let(:knative_cloud_run) { create(:clusters_applications_knative, cluster: cluster) }
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 549a273e2d7..15c3162270e 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -12,6 +12,13 @@ RSpec.describe Clusters::Applications::Prometheus do
include_examples 'cluster application helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs'
+ describe 'default values' do
+ subject(:prometheus) { build(:clusters_applications_prometheus) }
+
+ it { expect(prometheus.alert_manager_token).to be_an_instance_of(String) }
+ it { expect(prometheus.version).to eq(described_class::VERSION) }
+ end
+
describe 'after_destroy' do
let(:cluster) { create(:cluster, :with_installed_helm) }
let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
@@ -130,7 +137,7 @@ RSpec.describe Clusters::Applications::Prometheus do
end
context 'with knative installed' do
- let(:knative) { create(:clusters_applications_knative, :updated ) }
+ let(:knative) { create(:clusters_applications_knative, :updated) }
let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
subject { prometheus.install_command }
@@ -161,7 +168,7 @@ RSpec.describe Clusters::Applications::Prometheus do
end
describe '#predelete' do
- let(:knative) { create(:clusters_applications_knative, :updated ) }
+ let(:knative) { create(:clusters_applications_knative, :updated) }
let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
subject { prometheus.uninstall_command.predelete }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 8f02161843b..04b5ae9641d 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -13,6 +13,10 @@ RSpec.describe Clusters::Applications::Runner do
it { is_expected.to belong_to(:runner) }
+ describe 'default values' do
+ it { expect(subject.version).to eq(described_class::VERSION) }
+ end
+
describe '#can_uninstall?' do
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 73cd7bb9075..be64d72e031 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -49,6 +49,10 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to respond_to :project }
it { is_expected.to be_namespace_per_environment }
+ describe 'default values' do
+ it { expect(subject.helm_major_version).to eq(3) }
+ end
+
it_behaves_like 'it has loose foreign keys' do
let(:factory_name) { :cluster }
end
diff --git a/spec/models/clusters/integrations/prometheus_spec.rb b/spec/models/clusters/integrations/prometheus_spec.rb
index 90e99aefdce..d6d1105cdb1 100644
--- a/spec/models/clusters/integrations/prometheus_spec.rb
+++ b/spec/models/clusters/integrations/prometheus_spec.rb
@@ -15,6 +15,16 @@ RSpec.describe Clusters::Integrations::Prometheus do
it { is_expected.not_to allow_value(nil).for(:enabled) }
end
+ describe 'default values' do
+ subject(:integration) { build(:clusters_integrations_prometheus) }
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return('randomtoken')
+ end
+
+ it { expect(integration.alert_manager_token).to eq('randomtoken') }
+ end
+
describe 'after_destroy' do
subject(:integration) { create(:clusters_integrations_prometheus, cluster: cluster, enabled: true) }
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 4ac2fd022ba..b280275c2e5 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -21,6 +21,12 @@ RSpec.describe Clusters::Platforms::Kubernetes do
it_behaves_like 'having unique enum values'
+ describe 'default values' do
+ let(:kubernetes) { create(:cluster_platform_kubernetes) }
+
+ it { expect(kubernetes.authorization_type).to eq("rbac") }
+ end
+
describe 'before_validation' do
let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
@@ -427,6 +433,55 @@ RSpec.describe Clusters::Platforms::Kubernetes do
let(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
include_examples 'successful deployment request'
+
+ context 'when reading ingress raises NoMethodError' do
+ before do
+ allow_next_instance_of(Gitlab::Kubernetes::KubeClient) do |kube_client|
+ allow(kube_client).to receive(:get_pods).with(namespace: namespace).and_return([])
+ allow(kube_client).to receive(:get_deployments).with(namespace: namespace).and_return([])
+ allow(kube_client).to receive(:get_ingresses).with(namespace: namespace).and_raise(NoMethodError)
+ end
+ end
+
+ context 'when version request succeeds' do
+ before do
+ stub_server_min_version(min_server_version)
+ end
+
+ context 'when server min version is < 23' do
+ let(:min_server_version) { "18" }
+
+ it 'does not raise error', :unlimited_max_formatted_output_length do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'returns empty array for the K8s component keys' do
+ expect(subject).to include({ pods: [], deployments: [], ingresses: [] })
+ end
+ end
+
+ context 'when server min version is >= 23' do
+ let(:min_server_version) { "23" }
+
+ it 'does raise error' do
+ expect { subject }.to raise_error(NoMethodError)
+ end
+ end
+ end
+
+ context 'when the version request fails' do
+ before do
+ stub_server_min_version_failed_request
+ end
+
+ it "tracks error and returns empty arrays" do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(kind_of(Clusters::Platforms::Kubernetes::FailedVersionCheckError))
+
+ expect(subject).to include({ pods: [], deployments: [], ingresses: [] })
+ end
+ end
+ end
end
context 'on a group level cluster' do
@@ -452,7 +507,7 @@ RSpec.describe Clusters::Platforms::Kubernetes do
context 'when there are ignored K8s connections errors' do
described_class::IGNORED_CONNECTION_EXCEPTIONS.each do |exception|
- context "#{exception}" do
+ context exception.to_s do
before do
exception_args = ['arg1']
exception_args.push('arg2', 'arg3') if exception.name == 'Kubeclient::HttpError'
diff --git a/spec/models/clusters/providers/aws_spec.rb b/spec/models/clusters/providers/aws_spec.rb
index 3b4a48cc5be..2afed663edf 100644
--- a/spec/models/clusters/providers/aws_spec.rb
+++ b/spec/models/clusters/providers/aws_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Clusters::Providers::Aws do
include_examples 'provider status', :cluster_provider_aws
- describe 'default_value_for' do
+ describe 'default values' do
let(:provider) { build(:cluster_provider_aws) }
it "sets default values" do
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
index ad9ada04875..a1f00069937 100644
--- a/spec/models/clusters/providers/gcp_spec.rb
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -8,13 +8,14 @@ RSpec.describe Clusters::Providers::Gcp do
include_examples 'provider status', :cluster_provider_gcp
- describe 'default_value_for' do
+ describe 'default values' do
let(:gcp) { build(:cluster_provider_gcp) }
it "has default value" do
expect(gcp.zone).to eq('us-central1-a')
expect(gcp.num_nodes).to eq(3)
expect(gcp.machine_type).to eq('n1-standard-2')
+ expect(gcp.cloud_run).to eq(false)
end
end
diff --git a/spec/models/commit_signatures/gpg_signature_spec.rb b/spec/models/commit_signatures/gpg_signature_spec.rb
index 605ad725dd7..1ffaaeba396 100644
--- a/spec/models/commit_signatures/gpg_signature_spec.rb
+++ b/spec/models/commit_signatures/gpg_signature_spec.rb
@@ -85,4 +85,10 @@ RSpec.describe CommitSignatures::GpgSignature do
end
end
end
+
+ describe '#user' do
+ it 'retrieves the gpg_key user' do
+ expect(signature.user).to eq(gpg_key.user)
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index adbd20b6730..704203ed29c 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -32,6 +32,7 @@ RSpec.describe CommitStatus do
it { is_expected.to respond_to :failed? }
it { is_expected.to respond_to :running? }
it { is_expected.to respond_to :pending? }
+ it { is_expected.not_to be_retried }
describe '#author' do
subject { commit_status.author }
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 569dc3a3a3e..577004c2cf6 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -73,9 +73,9 @@ RSpec.describe BulkInsertSafe do
key: Settings.attr_encrypted_db_key_base_32,
insecure_mode: false
- default_value_for :enum_value, 'case_1'
- default_value_for :sha_value, '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'
- default_value_for :jsonb_value, { "key" => "value" }
+ attribute :enum_value, default: 'case_1'
+ attribute :sha_value, default: '2fd4e1c67a2d28fced849ee1bb76e7391b93eb12'
+ attribute :jsonb_value, default: -> { { "key" => "value" } }
def self.name
'BulkInsertItem'
diff --git a/spec/models/concerns/ci/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb
index bf699119a37..861d8f3b974 100644
--- a/spec/models/concerns/ci/has_variable_spec.rb
+++ b/spec/models/concerns/ci/has_variable_spec.rb
@@ -84,6 +84,7 @@ RSpec.describe Ci::HasVariable do
key: subject.key,
value: subject.value,
public: false,
+ raw: false,
masked: false
}
end
diff --git a/spec/models/concerns/ci/partitionable/switch_spec.rb b/spec/models/concerns/ci/partitionable/switch_spec.rb
new file mode 100644
index 00000000000..d955ad223f8
--- /dev/null
+++ b/spec/models/concerns/ci/partitionable/switch_spec.rb
@@ -0,0 +1,316 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::Partitionable::Switch, :aggregate_failures do
+ let(:model) do
+ Class.new(Ci::ApplicationRecord) do
+ self.primary_key = :id
+ self.table_name = :_test_ci_jobs_metadata
+ self.sequence_name = :_test_ci_jobs_metadata_id_seq
+
+ def self.name
+ 'TestSwitchJobMetadata'
+ end
+ end
+ end
+
+ let(:table_rollout_flag) { :ci_partitioning_use_test_routing_table }
+
+ let(:partitioned_model) { model::Partitioned }
+
+ let(:jobs_model) do
+ Class.new(Ci::ApplicationRecord) do
+ self.primary_key = :id
+ self.table_name = :_test_ci_jobs
+
+ def self.name
+ 'TestSwitchJob'
+ end
+ end
+ end
+
+ before do
+ allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited)
+
+ create_tables(<<~SQL)
+ CREATE TABLE _test_ci_jobs_metadata(
+ id serial NOT NULL PRIMARY KEY,
+ job_id int,
+ partition_id int NOT NULL DEFAULT 1,
+ expanded_environment_name text);
+
+ CREATE TABLE _test_p_ci_jobs_metadata (
+ LIKE _test_ci_jobs_metadata INCLUDING DEFAULTS
+ ) PARTITION BY LIST(partition_id);
+
+ ALTER TABLE _test_p_ci_jobs_metadata
+ ADD CONSTRAINT _test_p_ci_jobs_metadata_id_partition_id
+ UNIQUE (id, partition_id);
+
+ ALTER TABLE _test_p_ci_jobs_metadata
+ ATTACH PARTITION _test_ci_jobs_metadata FOR VALUES IN (1);
+
+ CREATE TABLE _test_ci_jobs(id serial NOT NULL PRIMARY KEY);
+ SQL
+
+ stub_const('Ci::Partitionable::Testing::PARTITIONABLE_MODELS', [model.name])
+
+ model.include(Ci::Partitionable)
+
+ model.partitionable scope: ->(r) { 1 },
+ through: { table: :_test_p_ci_jobs_metadata, flag: table_rollout_flag }
+
+ model.belongs_to :job, anonymous_class: jobs_model
+
+ jobs_model.has_one :metadata, anonymous_class: model,
+ foreign_key: :job_id, inverse_of: :job,
+ dependent: :destroy
+
+ allow(Feature::Definition).to receive(:get).and_call_original
+ allow(Feature::Definition).to receive(:get).with(table_rollout_flag)
+ .and_return(
+ Feature::Definition.new("development/#{table_rollout_flag}.yml",
+ { type: 'development', name: table_rollout_flag }
+ )
+ )
+ end
+
+ it { expect(model).not_to be_routing_class }
+
+ it { expect(partitioned_model).to be_routing_class }
+
+ it { expect(partitioned_model.table_name).to eq('_test_p_ci_jobs_metadata') }
+
+ it { expect(partitioned_model.quoted_table_name).to eq('"_test_p_ci_jobs_metadata"') }
+
+ it { expect(partitioned_model.arel_table.name).to eq('_test_p_ci_jobs_metadata') }
+
+ it { expect(partitioned_model.sequence_name).to eq('_test_ci_jobs_metadata_id_seq') }
+
+ context 'when switching the tables' do
+ before do
+ stub_feature_flags(table_rollout_flag => false)
+ end
+
+ %i[table_name quoted_table_name arel_table predicate_builder].each do |name|
+ it "switches #{name} to routing table and rollbacks" do
+ old_value = model.public_send(name)
+ routing_value = partitioned_model.public_send(name)
+
+ expect(old_value).not_to eq(routing_value)
+
+ expect { stub_feature_flags(table_rollout_flag => true) }
+ .to change(model, name).from(old_value).to(routing_value)
+
+ expect { stub_feature_flags(table_rollout_flag => false) }
+ .to change(model, name).from(routing_value).to(old_value)
+ end
+ end
+
+ it 'can switch aggregate methods' do
+ rollout_and_rollback_flag(
+ -> { expect(sql { model.count }).to all match(/FROM "_test_ci_jobs_metadata"/) },
+ -> { expect(sql { model.count }).to all match(/FROM "_test_p_ci_jobs_metadata"/) }
+ )
+ end
+
+ it 'can switch reads' do
+ rollout_and_rollback_flag(
+ -> { expect(sql { model.last }).to all match(/FROM "_test_ci_jobs_metadata"/) },
+ -> { expect(sql { model.last }).to all match(/FROM "_test_p_ci_jobs_metadata"/) }
+ )
+ end
+
+ it 'can switch inserts' do
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql(filter: /INSERT/) { model.create! })
+ .to all match(/INSERT INTO "_test_ci_jobs_metadata"/)
+ },
+ -> {
+ expect(sql(filter: /INSERT/) { model.create! })
+ .to all match(/INSERT INTO "_test_p_ci_jobs_metadata"/)
+ }
+ )
+ end
+
+ it 'can switch deletes' do
+ 3.times { model.create! }
+
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql(filter: /DELETE/) { model.last.destroy! })
+ .to all match(/DELETE FROM "_test_ci_jobs_metadata"/)
+ },
+ -> {
+ expect(sql(filter: /DELETE/) { model.last.destroy! })
+ .to all match(/DELETE FROM "_test_p_ci_jobs_metadata"/)
+ }
+ )
+ end
+
+ context 'with associations' do
+ let(:job) { jobs_model.create! }
+
+ it 'reads' do
+ model.create!(job_id: job.id)
+
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql(filter: /jobs_metadata/) { jobs_model.find(job.id).metadata })
+ .to all match(/FROM "_test_ci_jobs_metadata"/)
+ },
+ -> {
+ expect(sql(filter: /jobs_metadata/) { jobs_model.find(job.id).metadata })
+ .to all match(/FROM "_test_p_ci_jobs_metadata"/)
+ }
+ )
+ end
+
+ it 'writes' do
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! })
+ .to all match(/INSERT INTO "_test_ci_jobs_metadata"/)
+ },
+ -> {
+ expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.find(job.id).create_metadata! })
+ .to all match(/INSERT INTO "_test_p_ci_jobs_metadata"/)
+ }
+ )
+ end
+
+ it 'deletes' do
+ 3.times do
+ job = jobs_model.create!
+ job.create_metadata!
+ end
+
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! })
+ .to all match(/DELETE FROM "_test_ci_jobs_metadata"/)
+ },
+ -> {
+ expect(sql(filter: /DELETE .* jobs_metadata/) { jobs_model.last.destroy! })
+ .to all match(/DELETE FROM "_test_p_ci_jobs_metadata"/)
+ }
+ )
+ end
+
+ it 'can switch joins from jobs' do
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql { jobs_model.joins(:metadata).last })
+ .to all match(/INNER JOIN "_test_ci_jobs_metadata"/)
+ },
+ -> {
+ expect(sql { jobs_model.joins(:metadata).last })
+ .to all match(/INNER JOIN "_test_p_ci_jobs_metadata"/)
+ }
+ )
+ end
+
+ it 'can switch joins from metadata' do
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql { model.joins(:job).last })
+ .to all match(/FROM "_test_ci_jobs_metadata" INNER JOIN "_test_ci_jobs"/)
+ },
+ -> {
+ expect(sql { model.joins(:job).last })
+ .to all match(/FROM "_test_p_ci_jobs_metadata" INNER JOIN "_test_ci_jobs"/)
+ }
+ )
+ end
+
+ it 'preloads' do
+ job = jobs_model.create!
+ job.create_metadata!
+
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql(filter: /jobs_metadata/) { jobs_model.preload(:metadata).last })
+ .to all match(/FROM "_test_ci_jobs_metadata"/)
+ },
+ -> {
+ expect(sql(filter: /jobs_metadata/) { jobs_model.preload(:metadata).last })
+ .to all match(/FROM "_test_p_ci_jobs_metadata"/)
+ }
+ )
+ end
+
+ context 'with nested attributes' do
+ before do
+ jobs_model.accepts_nested_attributes_for :metadata
+ end
+
+ it 'writes' do
+ attrs = { metadata_attributes: { expanded_environment_name: 'test_env_name' } }
+
+ rollout_and_rollback_flag(
+ -> {
+ expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) })
+ .to all match(/INSERT INTO "_test_ci_jobs_metadata" .* 'test_env_name'/)
+ },
+ -> {
+ expect(sql(filter: /INSERT .* jobs_metadata/) { jobs_model.create!(attrs) })
+ .to all match(/INSERT INTO "_test_p_ci_jobs_metadata" .* 'test_env_name'/)
+ }
+ )
+ end
+ end
+ end
+ end
+
+ context 'with safe request store', :request_store do
+ it 'changing the flag to true does not affect the current request' do
+ stub_feature_flags(table_rollout_flag => false)
+
+ expect(model.table_name).to eq('_test_ci_jobs_metadata')
+
+ stub_feature_flags(table_rollout_flag => true)
+
+ expect(model.table_name).to eq('_test_ci_jobs_metadata')
+ end
+
+ it 'changing the flag to false does not affect the current request' do
+ stub_feature_flags(table_rollout_flag => true)
+
+ expect(model.table_name).to eq('_test_p_ci_jobs_metadata')
+
+ stub_feature_flags(table_rollout_flag => false)
+
+ expect(model.table_name).to eq('_test_p_ci_jobs_metadata')
+ end
+ end
+
+ def rollout_and_rollback_flag(old, new)
+ # Load class and SQL statements cache
+ old.call
+
+ stub_feature_flags(table_rollout_flag => true)
+
+ # Test switch
+ new.call
+
+ stub_feature_flags(table_rollout_flag => false)
+
+ # Test that it can switch back in the same process
+ old.call
+ end
+
+ def create_tables(table_sql)
+ Ci::ApplicationRecord.connection.execute(table_sql)
+ end
+
+ def sql(filter: nil, &block)
+ result = ActiveRecord::QueryRecorder.new(&block)
+ result = result.log
+
+ return result unless filter
+
+ result.select { |statement| statement.match?(filter) }
+ end
+end
diff --git a/spec/models/concerns/ci/partitionable_spec.rb b/spec/models/concerns/ci/partitionable_spec.rb
index d53501ccc3d..f3d33c971c7 100644
--- a/spec/models/concerns/ci/partitionable_spec.rb
+++ b/spec/models/concerns/ci/partitionable_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Ci::Partitionable do
- describe 'partitionable models inclusion' do
- let(:ci_model) { Class.new(Ci::ApplicationRecord) }
+ let(:ci_model) { Class.new(Ci::ApplicationRecord) }
+ describe 'partitionable models inclusion' do
subject { ci_model.include(described_class) }
it 'raises an exception' do
@@ -23,4 +23,21 @@ RSpec.describe Ci::Partitionable do
end
end
end
+
+ context 'with through options' do
+ before do
+ allow(ActiveSupport::DescendantsTracker).to receive(:store_inherited)
+ stub_const("#{described_class}::Testing::PARTITIONABLE_MODELS", [ci_model.name])
+
+ ci_model.include(described_class)
+ ci_model.partitionable scope: ->(r) { 1 },
+ through: { table: :_test_table_name, flag: :some_flag }
+ end
+
+ it { expect(ci_model.routing_table_name).to eq(:_test_table_name) }
+
+ it { expect(ci_model.routing_table_name_flag).to eq(:some_flag) }
+
+ it { expect(ci_model.ancestors).to include(described_class::Switch) }
+ end
end
diff --git a/spec/models/concerns/encrypted_user_password_spec.rb b/spec/models/concerns/encrypted_user_password_spec.rb
new file mode 100644
index 00000000000..b6447313967
--- /dev/null
+++ b/spec/models/concerns/encrypted_user_password_spec.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe User do
+ describe '#authenticatable_salt' do
+ let(:user) { build(:user, encrypted_password: encrypted_password) }
+
+ subject(:authenticatable_salt) { user.authenticatable_salt }
+
+ context 'when password is stored in BCrypt format' do
+ let(:encrypted_password) { '$2a$10$AvwDCyF/8HnlAv./UkAZx.vAlKRS89yNElP38FzdgOmVaSaiDL7xm' }
+
+ it 'returns the first 30 characters of the encrypted_password' do
+ expect(authenticatable_salt).to eq(user.encrypted_password[0, 29])
+ end
+ end
+
+ context 'when password is stored in PBKDF2 format' do
+ let(:encrypted_password) { '$pbkdf2-sha512$20000$rKbYsScsDdk$iwWBewXmrkD2fFfaG1SDcMIvl9gvEo3fBWUAfiqyVceTlw/DYgKBByHzf45pF5Qn59R4R.NQHsFpvZB4qlsYmw' } # rubocop:disable Layout/LineLength
+
+ it 'uses the decoded password salt' do
+ expect(authenticatable_salt).to eq('aca6d8b1272c0dd9')
+ end
+
+ it 'does not use the first 30 characters of the encrypted_password' do
+ expect(authenticatable_salt).not_to eq(encrypted_password[0, 29])
+ end
+ end
+
+ context 'when the encrypted_password is an unknown type' do
+ let(:encrypted_password) { '$argon2i$v=19$m=512,t=4,p=2$eM+ZMyYkpDRGaI3xXmuNcQ$c5DeJg3eb5dskVt1mDdxfw' }
+
+ it 'returns the first 30 characters of the encrypted_password' do
+ expect(authenticatable_salt).to eq(encrypted_password[0, 29])
+ end
+ end
+ end
+
+ describe '#valid_password?' do
+ subject(:validate_password) { user.valid_password?(password) }
+
+ let(:user) { build(:user, encrypted_password: encrypted_password) }
+ let(:password) { described_class.random_password }
+
+ shared_examples 'password validation fails when the password is encrypted using an unsupported method' do
+ let(:encrypted_password) { '$argon2i$v=19$m=512,t=4,p=2$eM+ZMyYkpDRGaI3xXmuNcQ$c5DeJg3eb5dskVt1mDdxfw' }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when the default encryption method is BCrypt' do
+ it_behaves_like 'password validation fails when the password is encrypted using an unsupported method'
+
+ context 'when the user password PBKDF2+SHA512' do
+ let(:encrypted_password) do
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(
+ password, 20_000, Devise.friendly_token[0, 16])
+ end
+
+ it { is_expected.to eq(true) }
+
+ it 're-encrypts the password as BCrypt' do
+ expect(user.encrypted_password).to start_with('$pbkdf2-sha512$')
+
+ validate_password
+
+ expect(user.encrypted_password).to start_with('$2a$')
+ end
+ end
+ end
+
+ context 'when the default encryption method is PBKDF2+SHA512 and the user password is BCrypt', :fips_mode do
+ it_behaves_like 'password validation fails when the password is encrypted using an unsupported method'
+
+ context 'when the user password BCrypt' do
+ let(:encrypted_password) { Devise::Encryptor.digest(described_class, password) }
+
+ it { is_expected.to eq(true) }
+
+ it 're-encrypts the password as PBKDF2+SHA512' do
+ expect(user.encrypted_password).to start_with('$2a$')
+
+ validate_password
+
+ expect(user.reload.encrypted_password).to start_with('$pbkdf2-sha512$')
+ end
+ end
+ end
+ end
+
+ describe '#password=' do
+ let(:user) { build(:user) }
+ let(:password) { described_class.random_password }
+
+ def compare_bcrypt_password(user, password)
+ Devise::Encryptor.compare(described_class, user.encrypted_password, password)
+ end
+
+ def compare_pbkdf2_password(user, password)
+ Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.compare(user.encrypted_password, password)
+ end
+
+ context 'when FIPS mode is enabled', :fips_mode do
+ it 'calls PBKDF2 digest and not the default Devise encryptor' do
+ expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512)
+ .to receive(:digest).at_least(:once).and_call_original
+ expect(Devise::Encryptor).not_to receive(:digest)
+
+ user.password = password
+ end
+
+ it 'saves the password in PBKDF2 format' do
+ user.password = password
+ user.save!
+
+ expect(compare_pbkdf2_password(user, password)).to eq(true)
+ expect { compare_bcrypt_password(user, password) }.to raise_error(::BCrypt::Errors::InvalidHash)
+ end
+ end
+
+ it 'calls default Devise encryptor and not the PBKDF2 encryptor' do
+ expect(Devise::Encryptor).to receive(:digest).at_least(:once).and_call_original
+ expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512).not_to receive(:digest)
+
+ user.password = password
+ end
+
+ it 'saves the password in BCrypt format' do
+ user.password = password
+ user.save!
+
+ expect { compare_pbkdf2_password(user, password) }
+ .to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash
+ expect(compare_bcrypt_password(user, password)).to eq(true)
+ end
+ end
+end
diff --git a/spec/models/concerns/file_store_mounter_spec.rb b/spec/models/concerns/file_store_mounter_spec.rb
new file mode 100644
index 00000000000..459f3d35668
--- /dev/null
+++ b/spec/models/concerns/file_store_mounter_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe FileStoreMounter, :aggregate_failures do
+ let(:uploader_class) do
+ Class.new do
+ def object_store
+ :object_store
+ end
+ end
+ end
+
+ let(:test_class) { Class.new { include(FileStoreMounter) } }
+
+ let(:uploader_instance) { uploader_class.new }
+
+ describe '.mount_file_store_uploader' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:mount_file_store_uploader) do
+ test_class.mount_file_store_uploader uploader_class, skip_store_file: skip_store_file, file_field: file_field
+ end
+
+ where(:skip_store_file, :file_field) do
+ true | :file
+ false | :file
+ false | :signed_file
+ true | :signed_file
+ end
+
+ with_them do
+ it 'defines instance methods and registers a callback' do
+ expect(test_class).to receive(:mount_uploader).with(file_field, uploader_class)
+ expect(test_class).to receive(:define_method).with("update_#{file_field}_store")
+ expect(test_class).to receive(:define_method).with("store_#{file_field}_now!")
+
+ if skip_store_file
+ expect(test_class).to receive(:skip_callback).with(:save, :after, "store_#{file_field}!".to_sym)
+ expect(test_class).not_to receive(:after_save)
+ else
+ expect(test_class).not_to receive(:skip_callback)
+ expect(test_class)
+ .to receive(:after_save)
+ .with("update_#{file_field}_store".to_sym, if: "saved_change_to_#{file_field}?".to_sym)
+ end
+
+ mount_file_store_uploader
+ end
+ end
+
+ context 'with an unknown file_field' do
+ let(:skip_store_file) { false }
+ let(:file_field) { 'unknown' }
+
+ it do
+ expect { mount_file_store_uploader }.to raise_error(ArgumentError, 'file_field not allowed: unknown')
+ end
+ end
+ end
+
+ context 'with an instance' do
+ let(:instance) { test_class.new }
+
+ before do
+ allow(test_class).to receive(:mount_uploader)
+ allow(test_class).to receive(:after_save)
+ test_class.mount_file_store_uploader uploader_class
+ end
+
+ describe '#update_file_store' do
+ subject(:update_file_store) { instance.update_file_store }
+
+ it 'calls update column' do
+ expect(instance).to receive(:file).and_return(uploader_instance)
+ expect(instance).to receive(:update_column).with('file_store', :object_store)
+
+ update_file_store
+ end
+ end
+
+ describe '#store_file_now!' do
+ subject(:store_file_now!) { instance.store_file_now! }
+
+ it 'calls the dynamic functions' do
+ expect(instance).to receive(:store_file!)
+ expect(instance).to receive(:update_file_store)
+
+ store_file_now!
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index a6a0e074589..b2ea7b22dea 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -88,5 +88,47 @@ RSpec.describe User do
end
end
end
+
+ describe '#redacted_name(viewing_user)' do
+ let_it_be(:viewing_user) { human }
+
+ subject { observed_user.redacted_name(viewing_user) }
+
+ context 'when user is not a project bot' do
+ let(:observed_user) { support_bot }
+
+ it { is_expected.to eq(support_bot.name) }
+ end
+
+ context 'when user is a project_bot' do
+ let(:observed_user) { project_bot }
+
+ context 'when groups are present and user can :read_group' do
+ let_it_be(:group) { create(:group) }
+
+ before do
+ group.add_developer(observed_user)
+ group.add_developer(viewing_user)
+ end
+
+ it { is_expected.to eq(observed_user.name) }
+ end
+
+ context 'when user can :read_project' do
+ let_it_be(:project) { create(:project) }
+
+ before do
+ project.add_developer(observed_user)
+ project.add_developer(viewing_user)
+ end
+
+ it { is_expected.to eq(observed_user.name) }
+ end
+
+ context 'when requester does not have permissions to read project_bot name' do
+ it { is_expected.to eq('****') }
+ end
+ end
+ end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 8842a36f40a..e553e34ab51 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -337,31 +337,6 @@ RSpec.describe Issuable do
it { expect(MergeRequest.to_ability_name).to eq("merge_request") }
end
- describe "#today?" do
- it "returns true when created today" do
- # Avoid timezone differences and just return exactly what we want
- allow(Date).to receive(:today).and_return(issue.created_at.to_date)
- expect(issue.today?).to be_truthy
- end
-
- it "returns false when not created today" do
- allow(Date).to receive(:today).and_return(Date.yesterday)
- expect(issue.today?).to be_falsey
- end
- end
-
- describe "#new?" do
- it "returns false when created 30 hours ago" do
- allow(issue).to receive(:created_at).and_return(Time.current - 30.hours)
- expect(issue.new?).to be_falsey
- end
-
- it "returns true when created 20 hours ago" do
- allow(issue).to receive(:created_at).and_return(Time.current - 20.hours)
- expect(issue.new?).to be_truthy
- end
- end
-
describe "#sort_by_attribute" do
let(:project) { create(:project) }
@@ -1055,6 +1030,22 @@ RSpec.describe Issuable do
end
end
+ describe '#supports_confidentiality?' do
+ where(:issuable_type, :supports_confidentiality) do
+ :issue | true
+ :incident | true
+ :merge_request | false
+ end
+
+ with_them do
+ let(:issuable) { build_stubbed(issuable_type) }
+
+ subject { issuable.supports_confidentiality? }
+
+ it { is_expected.to eq(supports_confidentiality) }
+ end
+ end
+
describe '#severity' do
subject { issuable.severity }
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
index 3e42a3504ac..98b44a2eec2 100644
--- a/spec/models/concerns/pg_full_text_searchable_spec.rb
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe PgFullTextSearchable do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
let(:model_class) do
Class.new(ActiveRecord::Base) do
@@ -76,7 +76,7 @@ RSpec.describe PgFullTextSearchable do
end
describe '.pg_full_text_search' do
- let(:english) { model_class.create!(project: project, title: 'title', description: 'something english') }
+ let(:english) { model_class.create!(project: project, title: 'title', description: 'something description english') }
let(:with_accent) { model_class.create!(project: project, title: 'Jürgen', description: 'Ærøskøbing') }
let(:japanese) { model_class.create!(project: project, title: '日本語 title', description: 'another english description') }
@@ -90,8 +90,19 @@ RSpec.describe PgFullTextSearchable do
expect(model_class.pg_full_text_search('title english')).to contain_exactly(english, japanese)
end
+ it 'searches specified columns only' do
+ matching_object = model_class.create!(project: project, title: 'english', description: 'some description')
+ matching_object.update_search_data!
+
+ expect(model_class.pg_full_text_search('english', matched_columns: %w(title))).to contain_exactly(matching_object)
+ end
+
+ it 'uses prefix matching' do
+ expect(model_class.pg_full_text_search('tit eng')).to contain_exactly(english, japanese)
+ end
+
it 'searches for exact term with quotes' do
- expect(model_class.pg_full_text_search('"something english"')).to contain_exactly(english)
+ expect(model_class.pg_full_text_search('"description english"')).to contain_exactly(english)
end
it 'ignores accents' do
@@ -113,6 +124,27 @@ RSpec.describe PgFullTextSearchable do
expect(model_class.pg_full_text_search('gopher://gitlab.com/gitlab-org/gitlab')).to contain_exactly(with_url)
end
end
+
+ context 'when search term is a path with underscores' do
+ let(:path) { 'browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb' }
+ let(:with_underscore) { model_class.create!(project: project, title: 'issue with path', description: "some #{path} other text") }
+
+ it 'allows searching by the path' do
+ with_underscore.update_search_data!
+
+ expect(model_class.pg_full_text_search(path)).to contain_exactly(with_underscore)
+ end
+ end
+
+ context 'when text has numbers preceded by a dash' do
+ let(:with_dash) { model_class.create!(project: project, title: 'issue with dash', description: 'ABC-123') }
+
+ it 'allows searching by numbers only' do
+ with_dash.update_search_data!
+
+ expect(model_class.pg_full_text_search('123')).to contain_exactly(with_dash)
+ end
+ end
end
describe '#update_search_data!' do
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index 89f34834aa4..f168bedc8eb 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe ProjectFeaturesCompatibility do
let(:features) do
features_enabled + %w(
repository pages operations container_registry package_registry environments feature_flags releases
- monitor
+ monitor infrastructure
)
end
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 790e6936803..fca94b50fee 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -72,9 +72,10 @@ RSpec.describe ShaAttribute do
end
it 'validates column type' do
- if expected_error == :no_error
+ case expected_error
+ when :no_error
expect { load_schema! }.not_to raise_error
- elsif expected_error == :sha_mismatch_error
+ when :sha_mismatch_error
expect { load_schema! }.to raise_error(
described_class::ShaAttributeTypeMismatchError,
/sha_attribute.*#{column_name}.* should be a :binary column/
@@ -89,9 +90,10 @@ RSpec.describe ShaAttribute do
end
it 'validates column type' do
- if expected_error == :no_error
+ case expected_error
+ when :no_error
expect { load_schema! }.not_to raise_error
- elsif expected_error == :sha_mismatch_error
+ when :sha_mismatch_error
expect { load_schema! }.to raise_error(
described_class::Sha256AttributeTypeMismatchError,
/sha256_attribute.*#{column_name}.* should be a :binary column/
diff --git a/spec/models/concerns/subquery_spec.rb b/spec/models/concerns/subquery_spec.rb
new file mode 100644
index 00000000000..95487fd8c2d
--- /dev/null
+++ b/spec/models/concerns/subquery_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Subquery do
+ let_it_be(:projects) { create_list :project, 3 }
+ let_it_be(:project_ids) { projects.map(&:id) }
+ let(:relation) { Project.where(id: projects) }
+
+ subject { relation.subquery(:id) }
+
+ shared_examples 'subquery as array values' do
+ specify { is_expected.to match_array project_ids }
+ specify { expect { subject }.not_to make_queries }
+ end
+
+ shared_examples 'subquery as relation' do
+ it { is_expected.to be_a ActiveRecord::Relation }
+ specify { expect { subject.load }.to make_queries }
+ end
+
+ shared_context 'when array size exceeds max_limit' do
+ subject { relation.subquery(:id, max_limit: 1) }
+ end
+
+ context 'when relation is not loaded' do
+ it_behaves_like 'subquery as relation'
+
+ context 'when array size exceeds max_limit' do
+ include_context 'when array size exceeds max_limit'
+
+ it_behaves_like 'subquery as relation'
+ end
+ end
+
+ context 'when relation is loaded' do
+ before do
+ relation.load
+ end
+
+ it_behaves_like 'subquery as array values'
+
+ context 'when array size exceeds max_limit' do
+ include_context 'when array size exceeds max_limit'
+
+ it_behaves_like 'subquery as relation'
+ end
+
+ context 'with a select' do
+ let(:relation) { Project.where(id: projects).select(:id) }
+
+ it_behaves_like 'subquery as array values'
+
+ context 'and querying with an unloaded column' do
+ subject { relation.subquery(:namespace_id) }
+
+ it { expect { subject }.to raise_error(ActiveModel::MissingAttributeError) }
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index e8db83b7144..e53fdafe3b1 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -214,19 +214,15 @@ end
RSpec.describe Ci::Build, 'TokenAuthenticatable' do
let(:token_field) { :token }
- let(:build) { FactoryBot.build(:ci_build) }
+ let(:build) { FactoryBot.build(:ci_build, :created) }
it_behaves_like 'TokenAuthenticatable'
describe 'generating new token' do
context 'token is not generated yet' do
describe 'token field accessor' do
- it 'makes it possible to access token' do
- expect(build.token).to be_nil
-
- build.save!
-
- expect(build.token).to be_present
+ it 'does not generate a token when saving a build' do
+ expect { build.save! }.not_to change(build, :token).from(nil)
end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 0033e9bbd08..9af53bae204 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -871,6 +871,28 @@ RSpec.describe ContainerRepository, :aggregate_failures do
end
end
+ describe '#set_delete_ongoing_status', :freeze_time do
+ let_it_be(:repository) { create(:container_repository) }
+
+ subject { repository.set_delete_ongoing_status }
+
+ it 'updates deletion status attributes' do
+ expect { subject }.to change(repository, :status).from(nil).to('delete_ongoing')
+ .and change(repository, :delete_started_at).from(nil).to(Time.zone.now)
+ end
+ end
+
+ describe '#set_delete_scheduled_status' do
+ let_it_be(:repository) { create(:container_repository, :status_delete_ongoing, delete_started_at: 3.minutes.ago) }
+
+ subject { repository.set_delete_scheduled_status }
+
+ it 'updates delete attributes' do
+ expect { subject }.to change(repository, :status).from('delete_ongoing').to('delete_scheduled')
+ .and change(repository, :delete_started_at).to(nil)
+ end
+ end
+
context 'registry migration' do
before do
allow(repository.gitlab_api_client).to receive(:supports_gitlab_api?).and_return(true)
@@ -1274,6 +1296,16 @@ RSpec.describe ContainerRepository, :aggregate_failures do
it { is_expected.to contain_exactly(repository1, repository3) }
end
+ describe '.with_stale_delete_at' do
+ let_it_be(:repository1) { create(:container_repository, delete_started_at: 1.day.ago) }
+ let_it_be(:repository2) { create(:container_repository, delete_started_at: 25.minutes.ago) }
+ let_it_be(:repository3) { create(:container_repository, delete_started_at: 1.week.ago) }
+
+ subject { described_class.with_stale_delete_at(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) }
diff --git a/spec/models/dependency_proxy/group_setting_spec.rb b/spec/models/dependency_proxy/group_setting_spec.rb
index c4c4a877d50..4da1fe42ff2 100644
--- a/spec/models/dependency_proxy/group_setting_spec.rb
+++ b/spec/models/dependency_proxy/group_setting_spec.rb
@@ -7,6 +7,11 @@ RSpec.describe DependencyProxy::GroupSetting, type: :model do
it { is_expected.to belong_to(:group) }
end
+ describe 'default values' do
+ it { is_expected.to be_enabled }
+ it { expect(described_class.new(enabled: false)).not_to be_enabled }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:group) }
end
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 635326eeadc..04763accc42 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -427,7 +427,7 @@ RSpec.describe DeployToken do
end
describe '.gitlab_deploy_token' do
- let(:project) { create(:project ) }
+ let(:project) { create(:project) }
subject { project.deploy_tokens.gitlab_deploy_token }
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index b91d836f82f..daa65f528e9 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -388,16 +388,31 @@ RSpec.describe Deployment do
end
context 'when deployment is behind current deployment' do
+ let_it_be(:commits) { project.repository.commits('master', limit: 2) }
+
let!(:deployment) do
- create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
+ create(:deployment, :success, project: project, environment: environment,
+ finished_at: 1.year.ago, sha: commits[0].sha)
end
let!(:last_deployment) do
- create(:deployment, :success, project: project, environment: environment)
+ create(:deployment, :success, project: project, environment: environment, sha: commits[1].sha)
end
it { is_expected.to be_truthy }
end
+
+ context 'when deployment is the same sha as the current deployment' do
+ let!(:deployment) do
+ create(:deployment, :success, project: project, environment: environment, finished_at: 1.year.ago)
+ end
+
+ let!(:last_deployment) do
+ create(:deployment, :success, project: project, environment: environment, sha: deployment.sha)
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
describe '#success?' do
@@ -1323,9 +1338,12 @@ RSpec.describe Deployment do
subject { deployment.tags }
it 'will return tags related to this deployment' do
- expect(project.repository).to receive(:tag_names_contains).with(deployment.sha, limit: 100).and_return(['test'])
+ expect(project.repository).to receive(:refs_by_oid).with(oid: deployment.sha,
+ limit: 100,
+ ref_patterns: [Gitlab::Git::TAG_REF_PREFIX])
+ .and_return(["#{Gitlab::Git::TAG_REF_PREFIX}test"])
- is_expected.to match_array(['test'])
+ is_expected.to match_array(['refs/tags/test'])
end
end
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
index 7a57f895b8a..fdfc4ec7cc4 100644
--- a/spec/models/diff_discussion_spec.rb
+++ b/spec/models/diff_discussion_spec.rb
@@ -128,7 +128,7 @@ RSpec.describe DiffDiscussion do
end
describe '#cache_key' do
- let(:notes_sha) { Digest::SHA1.hexdigest("#{diff_note.post_processed_cache_key}") }
+ let(:notes_sha) { Digest::SHA1.hexdigest(diff_note.post_processed_cache_key.to_s) }
let(:position_sha) { Digest::SHA1.hexdigest(diff_note.position.to_json) }
it 'returns the cache key with the position sha' do
diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb
index db0814af422..8990aa94b47 100644
--- a/spec/models/diff_viewer/server_side_spec.rb
+++ b/spec/models/diff_viewer/server_side_spec.rb
@@ -16,34 +16,6 @@ RSpec.describe DiffViewer::ServerSide do
subject { viewer_class.new(diff_file) }
- describe '#prepare!' do
- before do
- stub_feature_flags(disable_load_entire_blob_for_diff_viewer: feature_flag_enabled)
- end
-
- context 'when the disable_load_entire_blob_for_diff_viewer flag is disabled' do
- let(:feature_flag_enabled) { false }
-
- it 'loads all diff file data' do
- subject
- expect(diff_file).to receive_message_chain(:old_blob, :load_all_data!)
- expect(diff_file).to receive_message_chain(:new_blob, :load_all_data!)
- subject.prepare!
- end
- end
-
- context 'when the disable_load_entire_blob_for_diff_viewer flag is enabled' do
- let(:feature_flag_enabled) { true }
-
- it 'does not load file data' do
- subject
- expect(diff_file).not_to receive(:old_blob)
- expect(diff_file).not_to receive(:new_blob)
- subject.prepare!
- end
- end
- end
-
describe '#render_error' do
context 'when the diff file is stored externally' do
before do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index a442856d993..8a3d43f58e0 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -884,8 +884,8 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
describe '#actions_for' do
let(:deployment) { create(:deployment, :success, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline }
- let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' ) }
- let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' ) }
+ let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME') }
+ let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production') }
it 'returns a list of actions with matching environment' do
expect(environment.actions_for('review/master')).to contain_exactly(review_action)
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index a9aa5698ebb..2f1edf9ab94 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe EnvironmentStatus do
context 'multiple deployments' do
it {
- new_deployment = create(:deployment, :succeed, environment: deployment.environment, sha: deployment.sha )
+ new_deployment = create(:deployment, :succeed, environment: deployment.environment, sha: deployment.sha)
is_expected.to eq(new_deployment)
}
end
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index 30e73d84cfb..d48f6f7f3e4 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -187,39 +187,11 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
- describe '#reactive_cache_limit_enabled?' do
- subject { setting.reactive_cache_limit_enabled? }
-
- it { is_expected.to eq(true) }
-
- context 'when feature flag disabled' do
- before do
- stub_feature_flags(error_tracking_sentry_limit: false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
describe '#sentry_client' do
subject { setting.sentry_client }
it { is_expected.to be_a(ErrorTracking::SentryClient) }
it { is_expected.to have_attributes(url: setting.api_url, token: setting.token) }
-
- describe '#validate_size_guarded_by_feature_flag?' do
- subject { setting.sentry_client.validate_size_guarded_by_feature_flag? }
-
- it { is_expected.to eq(true) }
-
- context 'when feature flag disabled' do
- before do
- stub_feature_flags(error_tracking_sentry_limit: false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
end
describe '#list_sentry_issues' do
diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb
index 67b58c7bf6f..40b7930f02b 100644
--- a/spec/models/event_collection_spec.rb
+++ b/spec/models/event_collection_spec.rb
@@ -5,175 +5,165 @@ require 'spec_helper'
RSpec.describe EventCollection do
include DesignManagementTestHelpers
- shared_examples 'EventCollection examples' do
- describe '#to_a' do
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project_empty_repo, group: group) }
- let_it_be(:projects) { Project.where(id: project.id) }
- let_it_be(:user) { create(:user) }
- let_it_be(:merge_request) { create(:merge_request) }
-
- before do
- enable_design_management
- end
+ describe '#to_a' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project_empty_repo, group: group) }
+ let_it_be(:projects) { Project.where(id: project.id) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:merge_request) { create(:merge_request) }
- context 'with project events' do
- let_it_be(:push_event_payloads) do
- Array.new(9) do
- create(:push_event_payload,
- event: create(:push_event, project: project, author: user))
- end
- end
+ before do
+ enable_design_management
+ end
- let_it_be(:merge_request_events) { create_list(:event, 10, :merged, project: project, target: merge_request) }
- let_it_be(:closed_issue_event) { create(:closed_issue_event, project: project, author: user) }
- let_it_be(:wiki_page_event) { create(:wiki_page_event, project: project) }
- let_it_be(:design_event) { create(:design_event, project: project) }
-
- let(:push_events) { push_event_payloads.map(&:event) }
-
- it 'returns an Array of all event types when no filter is passed', :aggregate_failures do
- most_recent_20_events = [
- wiki_page_event,
- design_event,
- closed_issue_event,
- *push_events,
- *merge_request_events
- ].sort_by(&:id).reverse.take(20)
- events = described_class.new(projects).to_a
-
- expect(events).to be_an_instance_of(Array)
- expect(events).to match_array(most_recent_20_events)
+ context 'with project events' do
+ let_it_be(:push_event_payloads) do
+ Array.new(9) do
+ create(:push_event_payload,
+ event: create(:push_event, project: project, author: user))
end
+ end
- it 'includes the wiki page events when using to_a' do
- events = described_class.new(projects).to_a
+ let_it_be(:merge_request_events) { create_list(:event, 10, :merged, project: project, target: merge_request) }
+ let_it_be(:closed_issue_event) { create(:closed_issue_event, project: project, author: user) }
+ let_it_be(:wiki_page_event) { create(:wiki_page_event, project: project) }
+ let_it_be(:design_event) { create(:design_event, project: project) }
+
+ let(:push_events) { push_event_payloads.map(&:event) }
+
+ it 'returns an Array of all event types when no filter is passed', :aggregate_failures do
+ most_recent_20_events = [
+ wiki_page_event,
+ design_event,
+ closed_issue_event,
+ *push_events,
+ *merge_request_events
+ ].sort_by(&:id).reverse.take(20)
+ events = described_class.new(projects).to_a
+
+ expect(events).to be_an_instance_of(Array)
+ expect(events).to match_array(most_recent_20_events)
+ end
- expect(events).to include(wiki_page_event)
- end
+ it 'includes the wiki page events when using to_a' do
+ events = described_class.new(projects).to_a
- it 'includes the design events' do
- collection = described_class.new(projects)
+ expect(events).to include(wiki_page_event)
+ end
- expect(collection.to_a).to include(design_event)
- expect(collection.all_project_events).to include(design_event)
- end
+ it 'includes the design events' do
+ collection = described_class.new(projects)
- it 'includes the wiki page events when using all_project_events' do
- events = described_class.new(projects).all_project_events
+ expect(collection.to_a).to include(design_event)
+ expect(collection.all_project_events).to include(design_event)
+ end
- expect(events).to include(wiki_page_event)
- end
+ it 'includes the wiki page events when using all_project_events' do
+ events = described_class.new(projects).all_project_events
- it 'applies a limit to the number of events' do
- events = described_class.new(projects).to_a
+ expect(events).to include(wiki_page_event)
+ end
- expect(events.length).to eq(20)
- end
+ it 'applies a limit to the number of events' do
+ events = described_class.new(projects).to_a
- it 'can paginate through events' do
- events = described_class.new(projects, limit: 5, offset: 15).to_a
+ expect(events.length).to eq(20)
+ end
- expect(events.length).to eq(5)
- end
+ it 'can paginate through events' do
+ events = described_class.new(projects, limit: 5, offset: 15).to_a
- it 'returns an empty Array when crossing the maximum page number' do
- events = described_class.new(projects, limit: 1, offset: 15).to_a
+ expect(events.length).to eq(5)
+ end
- expect(events).to be_empty
- end
+ it 'returns an empty Array when crossing the maximum page number' do
+ events = described_class.new(projects, limit: 1, offset: 15).to_a
- it 'allows filtering of events using an EventFilter, returning single item' do
- filter = EventFilter.new(EventFilter::ISSUE)
- events = described_class.new(projects, filter: filter).to_a
+ expect(events).to be_empty
+ end
- expect(events).to contain_exactly(closed_issue_event)
- end
+ it 'allows filtering of events using an EventFilter, returning single item' do
+ filter = EventFilter.new(EventFilter::ISSUE)
+ events = described_class.new(projects, filter: filter).to_a
- it 'allows filtering of events using an EventFilter, returning several items' do
- filter = EventFilter.new(EventFilter::MERGED)
- events = described_class.new(projects, filter: filter).to_a
+ expect(events).to contain_exactly(closed_issue_event)
+ end
- expect(events).to match_array(merge_request_events)
- end
+ it 'allows filtering of events using an EventFilter, returning several items' do
+ filter = EventFilter.new(EventFilter::MERGED)
+ events = described_class.new(projects, filter: filter).to_a
- it 'allows filtering of events using an EventFilter, returning pushes' do
- filter = EventFilter.new(EventFilter::PUSH)
- events = described_class.new(projects, filter: filter).to_a
+ expect(events).to match_array(merge_request_events)
+ end
- expect(events).to match_array(push_events)
- end
+ it 'allows filtering of events using an EventFilter, returning pushes' do
+ filter = EventFilter.new(EventFilter::PUSH)
+ events = described_class.new(projects, filter: filter).to_a
+
+ expect(events).to match_array(push_events)
end
+ end
- context 'with group events' do
- let(:groups) { group.self_and_descendants.public_or_visible_to_user(user) }
- let(:subject) { described_class.new(projects, groups: groups).to_a }
+ context 'with group events' do
+ let(:groups) { group.self_and_descendants.public_or_visible_to_user(user) }
+ let(:subject) { described_class.new(projects, groups: groups).to_a }
- it 'includes also group events' do
- subgroup = create(:group, parent: group)
- event1 = create(:event, project: project, author: user)
- event2 = create(:event, project: nil, group: group, author: user)
- event3 = create(:event, project: nil, group: subgroup, author: user)
+ it 'includes also group events' do
+ subgroup = create(:group, parent: group)
+ event1 = create(:event, project: project, author: user)
+ event2 = create(:event, project: nil, group: group, author: user)
+ event3 = create(:event, project: nil, group: subgroup, author: user)
- expect(subject).to eq([event3, event2, event1])
- end
+ expect(subject).to eq([event3, event2, event1])
+ end
- it 'does not include events from inaccessible groups' do
- subgroup = create(:group, :private, parent: group)
- event1 = create(:event, project: nil, group: group, author: user)
- create(:event, project: nil, group: subgroup, author: user)
+ it 'does not include events from inaccessible groups' do
+ subgroup = create(:group, :private, parent: group)
+ event1 = create(:event, project: nil, group: group, author: user)
+ create(:event, project: nil, group: subgroup, author: user)
- expect(subject).to match_array([event1])
- end
+ expect(subject).to match_array([event1])
+ end
- context 'pagination through events' do
- let_it_be(:project_events) { create_list(:event, 10, project: project) }
- let_it_be(:group_events) { create_list(:event, 10, group: group, author: user) }
+ context 'with pagination through events' do
+ let_it_be(:project_events) { create_list(:event, 10, project: project) }
+ let_it_be(:group_events) { create_list(:event, 10, group: group, author: user) }
- let(:subject) { described_class.new(projects, limit: 10, offset: 5, groups: groups).to_a }
+ let(:subject) { described_class.new(projects, limit: 10, offset: 5, groups: groups).to_a }
- it 'returns recent groups and projects events' do
- recent_events_with_offset = (project_events[5..] + group_events[..4]).reverse
+ it 'returns recent groups and projects events' do
+ recent_events_with_offset = (project_events[5..] + group_events[..4]).reverse
- expect(subject).to eq(recent_events_with_offset)
- end
+ expect(subject).to eq(recent_events_with_offset)
end
+ end
- context 'project exclusive event types' do
- using RSpec::Parameterized::TableSyntax
+ context 'with project exclusive event types' do
+ using RSpec::Parameterized::TableSyntax
- where(:filter, :event) do
- EventFilter::PUSH | lazy { create(:push_event, project: project) }
- EventFilter::MERGED | lazy { create(:event, :merged, project: project, target: merge_request) }
- EventFilter::TEAM | lazy { create(:event, :joined, project: project) }
- EventFilter::ISSUE | lazy { create(:closed_issue_event, project: project) }
- EventFilter::DESIGNS | lazy { create(:design_event, project: project) }
- end
+ where(:filter, :event) do
+ EventFilter::PUSH | lazy { create(:push_event, project: project) }
+ EventFilter::MERGED | lazy { create(:event, :merged, project: project, target: merge_request) }
+ EventFilter::TEAM | lazy { create(:event, :joined, project: project) }
+ EventFilter::ISSUE | lazy { create(:closed_issue_event, project: project) }
+ EventFilter::DESIGNS | lazy { create(:design_event, project: project) }
+ end
- with_them do
- let(:subject) do
- described_class.new(projects, groups: Group.where(id: group.id), filter: EventFilter.new(filter))
- end
+ with_them do
+ let(:subject) do
+ described_class.new(projects, groups: Group.where(id: group.id), filter: EventFilter.new(filter))
+ end
- it "queries only project events" do
- expected_event = event # Forcing lazy evaluation
- expect(subject).to receive(:project_events).with(no_args).and_call_original
- expect(subject).not_to receive(:group_events)
+ it "queries only project events" do
+ expected_event = event # Forcing lazy evaluation
+ expect(subject).to receive(:project_events).with(no_args).and_call_original
+ expect(subject).not_to receive(:group_events)
- expect(subject.to_a).to match_array(expected_event)
- end
+ expect(subject.to_a).to match_array(expected_event)
end
end
end
end
- end
-
- context 'when the optimized_project_and_group_activity_queries FF is on' do
- before do
- stub_feature_flags(optimized_project_and_group_activity_queries: true)
- end
-
- it_behaves_like 'EventCollection examples'
it 'returns no events if no projects are passed' do
events = described_class.new(Project.none).to_a
@@ -181,12 +171,4 @@ RSpec.describe EventCollection do
expect(events).to be_empty
end
end
-
- context 'when the optimized_project_and_group_activity_queries FF is off' do
- before do
- stub_feature_flags(optimized_project_and_group_activity_queries: false)
- end
-
- it_behaves_like 'EventCollection examples'
- end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 9700852e567..9579c4c2d27 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Event do
describe 'after_create :set_last_repository_updated_at' do
context 'with a push event' do
it 'updates the project last_repository_updated_at and updated_at' do
- project.touch(:last_repository_updated_at, time: 1.year.ago) # rubocop: disable Rails/SkipsModelValidations
+ project.touch(:last_repository_updated_at, time: 1.year.ago)
event = create_push_event(project, project.first_owner)
@@ -431,8 +431,6 @@ RSpec.describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member) }
end
-
- include_examples 'visible to author', true
end
context 'private project' do
@@ -866,7 +864,7 @@ RSpec.describe Event do
end
it 'updates the project' do
- project.touch(:last_activity_at, time: 1.year.ago) # rubocop: disable Rails/SkipsModelValidations
+ project.touch(:last_activity_at, time: 1.year.ago)
event = create_push_event(project, project.first_owner)
@@ -882,7 +880,7 @@ RSpec.describe Event do
"project:#{project.id}")
end
- project.touch(:last_activity_at, time: 1.year.ago) # rubocop: disable Rails/SkipsModelValidations
+ project.touch(:last_activity_at, time: 1.year.ago)
create_push_event(project, project.first_owner)
end
diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb
deleted file mode 100644
index de6ce3ba053..00000000000
--- a/spec/models/experiment_spec.rb
+++ /dev/null
@@ -1,428 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Experiment do
- include AfterNextHelpers
-
- subject { build(:experiment) }
-
- describe 'associations' do
- it { is_expected.to have_many(:experiment_users) }
- it { is_expected.to have_many(:experiment_subjects) }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_uniqueness_of(:name) }
- it { is_expected.to validate_length_of(:name).is_at_most(255) }
- end
-
- describe '.add_user' do
- let_it_be(:experiment_name) { :experiment_key }
- let_it_be(:user) { 'a user' }
- let_it_be(:group) { 'a group' }
- let_it_be(:context) { { a: 42 } }
-
- subject(:add_user) { described_class.add_user(experiment_name, group, user, context) }
-
- context 'when an experiment with the provided name does not exist' do
- it 'creates a new experiment record' do
- allow_next_instance_of(described_class) do |experiment|
- allow(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- expect { add_user }.to change(described_class, :count).by(1)
- end
-
- it 'forwards the user, group_type, and context to the instance' do
- expect_next_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- add_user
- end
- end
-
- context 'when an experiment with the provided name already exists' do
- let_it_be(:experiment) { create(:experiment, name: experiment_name) }
-
- it 'does not create a new experiment record' do
- allow_next_found_instance_of(described_class) do |experiment|
- allow(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- expect { add_user }.not_to change(described_class, :count)
- end
-
- it 'forwards the user, group_type, and context to the instance' do
- expect_next_found_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_user_and_group).with(user, group, context)
- end
- add_user
- end
- end
-
- it 'works without the optional context argument' do
- allow_next_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_user_and_group).with(user, group, {})
- end
-
- expect { described_class.add_user(experiment_name, group, user) }.not_to raise_error
- end
- end
-
- describe '.add_group' do
- let_it_be(:experiment_name) { :experiment_key }
- let_it_be(:variant) { :control }
- let_it_be(:group) { build(:group) }
-
- subject(:add_group) { described_class.add_group(experiment_name, variant: variant, group: group) }
-
- 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_subject_and_variant!).with(group, variant)
-
- expect { add_group }.to change(described_class, :count).by(1)
- end
- end
-
- context 'when an experiment with the provided name already exists' do
- before do
- create(:experiment, name: experiment_name)
- end
-
- it 'does not create a new experiment record' do
- expect { add_group }.not_to change(described_class, :count)
- end
- end
- end
-
- describe '.record_conversion_event' do
- let_it_be(:user) { build(:user) }
- let_it_be(:context) { { a: 42 } }
-
- let(:experiment_key) { :test_experiment }
-
- subject(:record_conversion_event) { described_class.record_conversion_event(experiment_key, user, context) }
-
- context 'when no matching experiment exists' do
- it 'creates the experiment and uses it' do
- expect_next_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_conversion_event_for_user)
- end
- expect { record_conversion_event }.to change { described_class.count }.by(1)
- end
-
- context 'but we are unable to successfully create one' do
- let(:experiment_key) { nil }
-
- it 'raises a RecordInvalid error' do
- expect { record_conversion_event }.to raise_error(ActiveRecord::RecordInvalid)
- end
- end
- end
-
- context 'when a matching experiment already exists' do
- before do
- create(:experiment, name: experiment_key)
- end
-
- it 'sends record_conversion_event_for_user to the experiment instance' do
- expect_next_found_instance_of(described_class) do |experiment|
- expect(experiment).to receive(:record_conversion_event_for_user).with(user, context)
- end
- record_conversion_event
- end
- end
- end
-
- shared_examples 'experiment user with context' do
- let_it_be(:context) { { a: 42, 'b' => 34, 'c': { c1: 100, c2: 'c2', e: :e }, d: [1, 3] } }
- let_it_be(:initial_expected_context) { { 'a' => 42, 'b' => 34, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [1, 3] } }
-
- before do
- subject
- experiment.record_user_and_group(user, :experimental, {})
- end
-
- it 'has an initial context with stringified keys' do
- expect(ExperimentUser.last.context).to eq(initial_expected_context)
- end
-
- context 'when updated' do
- before do
- subject
- experiment.record_user_and_group(user, :experimental, new_context)
- end
-
- context 'with an empty context' do
- let_it_be(:new_context) { {} }
-
- it 'keeps the initial context' do
- expect(ExperimentUser.last.context).to eq(initial_expected_context)
- end
- end
-
- context 'with string keys' do
- let_it_be(:new_context) { { f: :some_symbol } }
-
- it 'adds new symbols stringified' do
- expected_context = initial_expected_context.merge('f' => 'some_symbol')
- expect(ExperimentUser.last.context).to eq(expected_context)
- end
- end
-
- context 'with atomic values or array values' do
- let_it_be(:new_context) { { b: 97, d: [99] } }
-
- it 'overrides the values' do
- expected_context = { 'a' => 42, 'b' => 97, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [99] }
- expect(ExperimentUser.last.context).to eq(expected_context)
- end
- end
-
- context 'with nested hashes' do
- let_it_be(:new_context) { { c: { g: 107 } } }
-
- it 'inserts nested additional values in the same keys' do
- expected_context = initial_expected_context.deep_merge('c' => { 'g' => 107 })
- expect(ExperimentUser.last.context).to eq(expected_context)
- end
- end
- end
- end
-
- describe '#record_conversion_event_for_user' do
- let_it_be(:user) { create(:user) }
- let_it_be(:experiment) { create(:experiment) }
- let_it_be(:context) { { a: 42 } }
-
- subject { experiment.record_conversion_event_for_user(user, context) }
-
- context 'when no existing experiment_user record exists for the given user' do
- it 'does not update or create an experiment_user record' do
- expect { subject }.not_to change { ExperimentUser.all.to_a }
- end
- end
-
- context 'when an existing experiment_user exists for the given user' do
- context 'but it has already been converted' do
- let!(:experiment_user) { create(:experiment_user, experiment: experiment, user: user, converted_at: 2.days.ago) }
-
- it 'does not update the converted_at value' do
- expect { subject }.not_to change { experiment_user.converted_at }
- end
-
- it_behaves_like 'experiment user with context' do
- before do
- experiment.record_user_and_group(user, :experimental, context)
- end
- end
- end
-
- context 'and it has not yet been converted' do
- let(:experiment_user) { create(:experiment_user, experiment: experiment, user: user) }
-
- it 'updates the converted_at value' do
- expect { subject }.to change { experiment_user.reload.converted_at }
- end
-
- it_behaves_like 'experiment user with context' do
- before do
- experiment.record_user_and_group(user, :experimental, context)
- end
- end
- end
- end
- end
-
- describe '#record_conversion_event_for_subject' do
- let_it_be(:user) { create(:user) }
- let_it_be(:experiment) { create(:experiment) }
- let_it_be(:context) { { a: 42 } }
-
- subject(:record_conversion) { experiment.record_conversion_event_for_subject(user, context) }
-
- context 'when no existing experiment_subject record exists for the given user' do
- it 'does not update or create an experiment_subject record' do
- expect { record_conversion }.not_to change { ExperimentSubject.all.to_a }
- end
- end
-
- context 'when an existing experiment_subject exists for the given user' do
- context 'but it has already been converted' do
- let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago) }
-
- it 'does not update the converted_at value' do
- expect { record_conversion }.not_to change { experiment_subject.converted_at }
- end
- end
-
- context 'and it has not yet been converted' do
- let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
-
- it 'updates the converted_at value' do
- expect { record_conversion }.to change { experiment_subject.reload.converted_at }
- end
- end
-
- context 'with no existing context' do
- let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user) }
-
- it 'updates the context' do
- expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42)
- end
- end
-
- context 'with an existing context' do
- let(:experiment_subject) { create(:experiment_subject, experiment: experiment, user: user, converted_at: 2.days.ago, context: { b: 1 } ) }
-
- it 'merges the context' do
- expect { record_conversion }.to change { experiment_subject.reload.context }.to('a' => 42, 'b' => 1)
- end
- end
- end
- end
-
- describe '#record_subject_and_variant!' do
- let_it_be(:subject_to_record) { create(:group) }
- let_it_be(:variant) { :control }
- let_it_be(:experiment) { create(:experiment) }
-
- 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 subject' do
- it 'creates an experiment_subject record' do
- 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 subject' do
- let_it_be(:experiment_subject) do
- create(:experiment_subject, experiment: experiment, namespace: subject_to_record, user: nil, variant: :experimental)
- end
-
- context 'when it belongs to the same variant' do
- let(:variant) { :experimental }
-
- it 'does not initiate a transaction' do
- expect(Experiment.connection).not_to receive(:transaction)
-
- subject
- end
- end
-
- context 'but it belonged to a different variant' do
- it 'updates the variant value' do
- 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
- end
-
- describe '#record_user_and_group' do
- let_it_be(:experiment) { create(:experiment) }
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { :control }
- let_it_be(:context) { { a: 42 } }
-
- subject { experiment.record_user_and_group(user, group, context) }
-
- context 'when an experiment_user does not yet exist for the given user' do
- it 'creates a new experiment_user record' do
- expect { subject }.to change(ExperimentUser, :count).by(1)
- end
-
- it 'assigns the correct group_type to the experiment_user' do
- subject
-
- expect(ExperimentUser.last.group_type).to eq('control')
- end
-
- it 'adds the correct context to the experiment_user' do
- subject
-
- expect(ExperimentUser.last.context).to eq({ 'a' => 42 })
- end
- end
-
- context 'when an experiment_user already exists for the given user' do
- before do
- # Create an existing experiment_user for this experiment and the :control group
- experiment.record_user_and_group(user, :control)
- end
-
- it 'does not create a new experiment_user record' do
- expect { subject }.not_to change(ExperimentUser, :count)
- end
-
- context 'when group type or context did not change' do
- let(:context) { {} }
-
- it 'does not initiate a transaction' do
- expect(Experiment.connection).not_to receive(:transaction)
-
- subject
- end
- end
-
- context 'but the group_type and context has changed' do
- let(:group) { :experimental }
-
- it 'updates the existing experiment_user record with group_type' do
- expect { subject }.to change { ExperimentUser.last.group_type }
- end
- end
-
- it_behaves_like 'experiment user with context'
- end
- end
-end
diff --git a/spec/models/experiment_subject_spec.rb b/spec/models/experiment_subject_spec.rb
deleted file mode 100644
index d86dc3cbf65..00000000000
--- a/spec/models/experiment_subject_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-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(:namespace) }
- it { is_expected.to belong_to(:project) }
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:experiment) }
-
- describe 'must_have_one_subject_present' do
- 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
- expect(experiment_subject.errors[:base]).to include(error_message)
- end
-
- it 'passes when user subject is present' do
- experiment_subject.user = build(:user)
- expect(experiment_subject).to be_valid
- end
-
- it 'passes when namespace subject is present' do
- experiment_subject.namespace = build(:group)
- expect(experiment_subject).to be_valid
- end
-
- it 'passes when project subject is present' do
- experiment_subject.project = build(:project)
- expect(experiment_subject).to be_valid
- end
-
- it 'fails when more than one subject is present', :aggregate_failures do
- # two subjects
- experiment_subject.user = build(:user)
- experiment_subject.namespace = build(:group)
- expect(experiment_subject).not_to be_valid
- expect(experiment_subject.errors[:base]).to include(error_message)
-
- # three subjects
- experiment_subject.project = build(:project)
- expect(experiment_subject).not_to be_valid
- expect(experiment_subject.errors[:base]).to include(error_message)
- 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/experiment_user_spec.rb b/spec/models/experiment_user_spec.rb
deleted file mode 100644
index 9201529b145..00000000000
--- a/spec/models/experiment_user_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ExperimentUser do
- describe 'Associations' do
- it { is_expected.to belong_to(:experiment) }
- it { is_expected.to belong_to(:user) }
- end
-
- describe 'Validations' do
- it { is_expected.to validate_presence_of(:group_type) }
- end
-end
diff --git a/spec/models/exported_protected_branch_spec.rb b/spec/models/exported_protected_branch_spec.rb
index 7886a522741..9f862de6ff8 100644
--- a/spec/models/exported_protected_branch_spec.rb
+++ b/spec/models/exported_protected_branch_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe ExportedProtectedBranch do
it 'returns the correct push access levels' do
exported_branch = create(:exported_protected_branch, :developers_can_push)
deploy_key = create(:deploy_key)
- create(:deploy_keys_project, :write_access, project: exported_branch.project, deploy_key: deploy_key )
+ create(:deploy_keys_project, :write_access, project: exported_branch.project, deploy_key: deploy_key)
create(:protected_branch_push_access_level, protected_branch: exported_branch, deploy_key: deploy_key)
dev_push_access_level = exported_branch.push_access_levels.first
diff --git a/spec/models/factories_spec.rb b/spec/models/factories_spec.rb
index c931c96bafd..65b993cca7f 100644
--- a/spec/models/factories_spec.rb
+++ b/spec/models/factories_spec.rb
@@ -2,59 +2,100 @@
require 'spec_helper'
-RSpec.describe 'factories' do
+# `:saas` is used to test `gitlab_subscription` factory.
+# It's not available on FOSS but also this very factory is not.
+RSpec.describe 'factories', :saas do
include Database::DatabaseHelpers
+ # Used in `skipped` and indicates whether to skip any traits including the
+ # plain factory.
+ any = Object.new
+
# https://gitlab.com/groups/gitlab-org/-/epics/5464 tracks the remaining
- # skipped traits.
+ # skipped factories or traits.
#
# Consider adding a code comment if a trait cannot produce a valid object.
- def skipped_traits
- [
- [:audit_event, :unauthenticated],
- [:ci_build_trace_chunk, :fog_with_data],
- [:ci_job_artifact, :remote_store],
- [:ci_job_artifact, :raw],
- [:ci_job_artifact, :gzip],
- [:ci_job_artifact, :correct_checksum],
- [:environment, :non_playable],
- [:composer_cache_file, :object_storage],
- [:debian_project_component_file, :object_storage],
- [:debian_project_distribution, :object_storage],
- [:debian_file_metadatum, :unknown],
- [:issue_customer_relations_contact, :for_contact],
- [:issue_customer_relations_contact, :for_issue],
- [:package_file, :object_storage],
- [:rpm_repository_file, :object_storage],
- [:pages_domain, :without_certificate],
- [:pages_domain, :without_key],
- [:pages_domain, :with_missing_chain],
- [:pages_domain, :with_trusted_chain],
- [:pages_domain, :with_trusted_expired_chain],
- [:pages_domain, :explicit_ecdsa],
- [:project_member, :blocked],
- [:remote_mirror, :ssh],
- [:user_preference, :only_comments],
- [:ci_pipeline_artifact, :remote_store]
- ]
- end
+ skipped = [
+ [:audit_event, :unauthenticated],
+ [:ci_build_trace_chunk, :fog_with_data],
+ [:ci_job_artifact, :remote_store],
+ [:ci_job_artifact, :raw],
+ [:ci_job_artifact, :gzip],
+ [:ci_job_artifact, :correct_checksum],
+ [:dependency_proxy_blob, :remote_store],
+ [:environment, :non_playable],
+ [:composer_cache_file, :object_storage],
+ [:debian_project_component_file, :object_storage],
+ [:debian_project_distribution, :object_storage],
+ [:debian_file_metadatum, :unknown],
+ [:issue_customer_relations_contact, :for_contact],
+ [:issue_customer_relations_contact, :for_issue],
+ [:package_file, :object_storage],
+ [:rpm_repository_file, :object_storage],
+ [:pages_domain, :without_certificate],
+ [:pages_domain, :without_key],
+ [:pages_domain, :with_missing_chain],
+ [:pages_domain, :with_trusted_chain],
+ [:pages_domain, :with_trusted_expired_chain],
+ [:pages_domain, :explicit_ecdsa],
+ [:project_member, :blocked],
+ [:remote_mirror, :ssh],
+ [:user_preference, :only_comments],
+ [:ci_pipeline_artifact, :remote_store],
+ # EE
+ [:dast_profile, :with_dast_site_validation],
+ [:ee_ci_build, :dependency_scanning_report],
+ [:ee_ci_build, :license_scan_v1],
+ [:ee_ci_job_artifact, :v1],
+ [:ee_ci_job_artifact, :v1_1],
+ [:ee_ci_job_artifact, :v2],
+ [:ee_ci_job_artifact, :v2_1],
+ [:geo_ci_secure_file_state, any],
+ [:geo_dependency_proxy_blob_state, any],
+ [:geo_event_log, :geo_event],
+ [:geo_job_artifact_state, any],
+ [:geo_lfs_object_state, any],
+ [:geo_pages_deployment_state, any],
+ [:geo_upload_state, any],
+ [:geo_ci_secure_file_state, any],
+ [:lfs_object, :checksum_failure],
+ [:lfs_object, :checksummed],
+ [:merge_request, :blocked],
+ [:merge_request_diff, :verification_failed],
+ [:merge_request_diff, :verification_succeeded],
+ [:package_file, :verification_failed],
+ [:package_file, :verification_succeeded],
+ [:project, :with_vulnerabilities],
+ [:scan_execution_policy, :with_schedule_and_agent],
+ [:vulnerability, :with_cluster_image_scanning_finding],
+ [:vulnerability, :with_findings],
+ [:vulnerability_export, :finished]
+ ].freeze
shared_examples 'factory' do |factory|
+ skip_any = skipped.include?([factory.name, any])
+
describe "#{factory.name} factory" do
it 'does not raise error when built' do
+ # We use `skip` here because using `build` mostly work even if
+ # factories break when creating them.
+ skip 'Factory skipped linting due to legacy error' if skip_any
+
expect { build(factory.name) }.not_to raise_error
end
it 'does not raise error when created' do
+ pending 'Factory skipped linting due to legacy error' if skip_any
+
expect { create(factory.name) }.not_to raise_error # rubocop:disable Rails/SaveBang
end
factory.definition.defined_traits.map(&:name).each do |trait_name|
+ skip_trait = skip_any || skipped.include?([factory.name, trait_name.to_sym])
+
describe "linting :#{trait_name} trait" do
it 'does not raise error when created' do
- if skipped_traits.include?([factory.name, trait_name.to_sym])
- pending("Trait skipped linting due to legacy error")
- end
+ pending 'Trait skipped linting due to legacy error' if skip_trait
expect { create(factory.name, trait_name) }.not_to raise_error
end
@@ -71,6 +112,7 @@ RSpec.describe 'factories' do
# is being mutated.
skip_factory_defaults = %i[
ci_job_token_project_scope_link
+ ci_subscriptions_project
evidence
exported_protected_branch
fork_network_member
@@ -78,22 +120,27 @@ RSpec.describe 'factories' do
import_state
issue_customer_relations_contact
member_task
+ merge_request_block
milestone_release
namespace
project_namespace
project_repository
+ project_security_setting
prometheus_alert
prometheus_alert_event
prometheus_metric
protected_branch
protected_branch_merge_access_level
protected_branch_push_access_level
+ protected_branch_unprotect_access_level
protected_tag
+ protected_tag_create_access_level
release
release_link
self_managed_prometheus_alert_event
shard
users_star_project
+ vulnerabilities_finding_identifier
wiki_page
wiki_page_meta
].to_set.freeze
@@ -110,6 +157,27 @@ RSpec.describe 'factories' do
without_fd, with_fd = FactoryBot.factories
.partition { |factory| skip_factory_defaults.include?(factory.name) }
+ # Some EE models check licensed features so stub them.
+ shared_context 'with licensed features' do
+ licensed_features = %i[
+ board_milestone_lists
+ board_assignee_lists
+ ].index_with(true)
+
+ if Gitlab.jh?
+ licensed_features.merge! %i[
+ dingtalk_integration
+ feishu_bot_integration
+ ].index_with(true)
+ end
+
+ before do
+ stub_licensed_features(licensed_features)
+ end
+ end
+
+ include_context 'with licensed features' if Gitlab.ee?
+
context 'with factory defaults', factory_default: :keep do
let_it_be(:namespace) { create_default(:namespace).freeze }
let_it_be(:project) { create_default(:project, :repository).freeze }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 68c2d1d3995..6ba450b6d57 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -40,6 +40,7 @@ RSpec.describe Group do
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
+ it { is_expected.to have_many(:protected_branches) }
it { is_expected.to have_one(:crm_settings) }
it { is_expected.to have_one(:group_feature) }
it { is_expected.to have_one(:harbor_integration) }
@@ -98,6 +99,15 @@ RSpec.describe Group do
expect(group).to be_valid
end
+
+ it 'does not allow a subgroup to have the same name as an existing subgroup' do
+ sub_group1 = create(:group, parent: group, name: "SG", path: 'api')
+ sub_group2 = described_class.new(parent: group, name: "SG", path: 'api2')
+
+ expect(sub_group1).to be_valid
+ expect(sub_group2).not_to be_valid
+ expect(sub_group2.errors.full_messages.to_sentence).to eq('Name has already been taken')
+ end
end
end
@@ -1058,28 +1068,45 @@ RSpec.describe Group do
end
context 'with owners from a parent' do
- before do
- parent_group = create(:group)
- create(:group_member, :owner, group: parent_group)
- group.update!(parent: parent_group)
+ context 'when top-level group' do
+ it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+
+ context 'with group sharing' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ before do
+ create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
+ create(:group_member, :owner, group: subgroup)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+ end
end
- it { expect(group.last_owner?(@members[:owner])).to be_falsy }
+ context 'when subgroup' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ it { expect(subgroup.last_owner?(@members[:owner])).to be_truthy }
+
+ context 'with two owners' do
+ before do
+ create(:group_member, :owner, group: group)
+ end
+
+ it { expect(subgroup.last_owner?(@members[:owner])).to be_falsey }
+ end
+ end
end
end
describe '#member_last_blocked_owner?' do
- let_it_be(:blocked_user) { create(:user, :blocked) }
+ let!(:blocked_user) { create(:user, :blocked) }
- let(:member) { blocked_user.group_members.last }
-
- before do
- group.add_member(blocked_user, GroupMember::OWNER)
- end
+ let!(:member) { group.add_member(blocked_user, GroupMember::OWNER) }
context 'when last_blocked_owner is set' do
before do
- expect(group).not_to receive(:members_with_parents)
+ expect(group).not_to receive(:member_owners_excluding_project_bots)
end
it 'returns true' do
@@ -1106,6 +1133,14 @@ RSpec.describe Group do
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
end
+ context 'with another active project_bot owner' do
+ before do
+ group.add_member(create(:user, :project_bot), GroupMember::OWNER)
+ end
+
+ it { expect(group.member_last_blocked_owner?(member)).to be(true) }
+ end
+
context 'with 2 blocked owners' do
before do
group.add_member(create(:user, :blocked), GroupMember::OWNER)
@@ -1115,13 +1150,36 @@ RSpec.describe Group do
end
context 'with owners from a parent' do
- before do
- parent_group = create(:group)
- create(:group_member, :owner, group: parent_group)
- group.update!(parent: parent_group)
+ context 'when top-level group' do
+ it { expect(group.member_last_blocked_owner?(member)).to be(true) }
+
+ context 'with group sharing' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ before do
+ create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
+ create(:group_member, :owner, group: subgroup)
+ end
+
+ it { expect(group.member_last_blocked_owner?(member)).to be(true) }
+ end
end
- it { expect(group.member_last_blocked_owner?(member)).to be(false) }
+ context 'when subgroup' do
+ let!(:subgroup) { create(:group, :nested) }
+
+ let!(:member) { subgroup.add_member(blocked_user, GroupMember::OWNER) }
+
+ it { expect(subgroup.member_last_blocked_owner?(member)).to be(true) }
+
+ context 'with two owners' do
+ before do
+ create(:group_member, :owner, group: subgroup.parent)
+ end
+
+ it { expect(subgroup.member_last_blocked_owner?(member)).to be(false) }
+ end
+ end
end
end
end
@@ -1174,58 +1232,63 @@ RSpec.describe Group do
end
end
- describe '#all_owners_excluding_project_bots' do
+ describe '#member_owners_excluding_project_bots' do
let_it_be(:user) { create(:user) }
- context 'when there is only one owner' do
- let!(:owner) do
- group.add_member(user, GroupMember::OWNER)
- end
+ let!(:member_owner) do
+ group.add_member(user, GroupMember::OWNER)
+ end
- it 'returns the owner' do
- expect(group.all_owners_excluding_project_bots).to contain_exactly(owner)
- end
+ it 'returns the member-owners' do
+ expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
+ end
- context 'and there is also a project_bot owner' do
- before do
- group.add_member(create(:user, :project_bot), GroupMember::OWNER)
- end
+ context 'there is also a project_bot owner' do
+ before do
+ group.add_member(create(:user, :project_bot), GroupMember::OWNER)
+ end
- it 'returns only the human owner' do
- expect(group.all_owners_excluding_project_bots).to contain_exactly(owner)
- end
+ it 'returns only the human member-owners' do
+ expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
end
end
- context 'when there are multiple owners' do
- let_it_be(:user_2) { create(:user) }
+ context 'with owners from a parent' do
+ context 'when top-level group' do
+ context 'with group sharing' do
+ let!(:subgroup) { create(:group, parent: group) }
- let!(:owner) do
- group.add_member(user, GroupMember::OWNER)
- end
+ before do
+ create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
+ subgroup.add_member(user, GroupMember::OWNER)
+ end
- let!(:owner2) do
- group.add_member(user_2, GroupMember::OWNER)
+ it 'returns only direct member-owners' do
+ expect(group.member_owners_excluding_project_bots).to contain_exactly(member_owner)
+ end
+ end
end
- it 'returns both owners' do
- expect(group.all_owners_excluding_project_bots).to contain_exactly(owner, owner2)
- end
+ context 'when subgroup' do
+ let!(:subgroup) { create(:group, parent: group) }
- context 'and there is also a project_bot owner' do
- before do
- group.add_member(create(:user, :project_bot), GroupMember::OWNER)
+ let_it_be(:user_2) { create(:user) }
+
+ let!(:member_owner_2) do
+ subgroup.add_member(user_2, GroupMember::OWNER)
end
- it 'returns only the human owners' do
- expect(group.all_owners_excluding_project_bots).to contain_exactly(owner, owner2)
+ it 'returns member-owners including parents' do
+ expect(subgroup.member_owners_excluding_project_bots).to contain_exactly(member_owner, member_owner_2)
end
end
end
context 'when there are no owners' do
- it 'returns false' do
- expect(group.all_owners_excluding_project_bots).to be_empty
+ let_it_be(:empty_group) { create(:group) }
+
+ it 'returns an empty result' do
+ expect(empty_group.member_owners_excluding_project_bots).to be_empty
end
end
end
@@ -2405,23 +2468,6 @@ RSpec.describe Group do
end
end
- describe '.groups_including_descendants_by' do
- let_it_be(:parent_group1) { create(:group) }
- let_it_be(:parent_group2) { create(:group) }
- let_it_be(:extra_group) { create(:group) }
- let_it_be(:child_group1) { create(:group, parent: parent_group1) }
- let_it_be(:child_group2) { create(:group, parent: parent_group1) }
- let_it_be(:child_group3) { create(:group, parent: parent_group2) }
-
- subject { described_class.groups_including_descendants_by([parent_group2.id, parent_group1.id]) }
-
- shared_examples 'returns the expected groups for a group and its descendants' do
- specify { is_expected.to contain_exactly(parent_group1, parent_group2, child_group1, child_group2, child_group3) }
- end
-
- it_behaves_like 'returns the expected groups for a group and its descendants'
- end
-
describe '.preset_root_ancestor_for' do
let_it_be(:rootgroup, reload: true) { create(:group) }
let_it_be(:subgroup, reload: true) { create(:group, parent: rootgroup) }
@@ -2555,7 +2601,7 @@ RSpec.describe Group do
end
context 'when parent does not allow' do
- let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false ) }
+ let_it_be(:parent, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
let_it_be(:group, reload: true) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
it 'raises exception' do
diff --git a/spec/models/hooks/active_hook_filter_spec.rb b/spec/models/hooks/active_hook_filter_spec.rb
index 1f693ce9fde..47c0fbdb106 100644
--- a/spec/models/hooks/active_hook_filter_spec.rb
+++ b/spec/models/hooks/active_hook_filter_spec.rb
@@ -6,65 +6,102 @@ RSpec.describe ActiveHookFilter do
subject(:filter) { described_class.new(hook) }
describe '#matches?' do
- context 'for push event hooks' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'for various types of branch_filter' do
let(:hook) do
- create(:project_hook, push_events: true, push_events_branch_filter: branch_filter)
+ build(:project_hook, push_events: true, issues_events: true)
end
- context 'branch filter is specified' do
- let(:branch_filter) { 'master' }
+ where(:branch_filter_strategy, :branch_filter, :ref, :expected_matches?) do
+ 'all_branches' | 'master' | 'refs/heads/master' | true
+ 'all_branches' | '' | 'refs/heads/master' | true
+ 'all_branches' | nil | 'refs/heads/master' | true
+ 'all_branches' | '.*' | 'refs/heads/master' | true
+ 'wildcard' | 'master' | 'refs/heads/master' | true
+ 'wildcard' | 'master' | 'refs/heads/my_branch' | false
+ 'wildcard' | 'features/*' | 'refs/heads/features/my-branch' | true
+ 'wildcard' | 'features/*' | 'refs/heads/features/my-branch/something' | true
+ 'wildcard' | 'features/*' | 'refs/heads/master' | false
+ 'wildcard' | nil | 'refs/heads/master' | true
+ 'wildcard' | '' | 'refs/heads/master' | true
+ 'regex' | 'master' | 'refs/heads/master' | true
+ 'regex' | 'master' | 'refs/heads/my_branch' | false
+ 'regex' | 'features/*' | 'refs/heads/xxxx/features/my-branch' | true
+ 'regex' | 'features/*' | 'refs/heads/features/' | true
+ 'regex' | 'features/*' | 'refs/heads/features' | true
+ 'regex' | 'features/.*' | 'refs/heads/features/my-branch' | true
+ 'regex' | 'features/.*' | 'refs/heads/features/my-branch/something' | true
+ 'regex' | 'features/.*' | 'refs/heads/master' | false
+ 'regex' | '(feature|dev)' | 'refs/heads/feature' | true
+ 'regex' | '(feature|dev)' | 'refs/heads/dev' | true
+ 'regex' | '(feature|dev)' | 'refs/heads/master' | false
+ 'regex' | nil | 'refs/heads/master' | true
+ 'regex' | '' | 'refs/heads/master' | true
+ end
- it 'returns true if branch matches' do
- expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be true
+ with_them do
+ before do
+ hook.assign_attributes(
+ push_events_branch_filter: branch_filter,
+ branch_filter_strategy: branch_filter_strategy
+ )
end
- it 'returns false if branch does not match' do
- expect(filter.matches?(:push_hooks, { ref: 'refs/heads/my_branch' })).to be false
- end
+ it { expect(filter.matches?(:push_hooks, { ref: ref })).to be expected_matches? }
+ it { expect(filter.matches?(:issues_events, { ref: ref })).to be true }
+ end
- it 'returns false if ref is nil' do
- expect(filter.matches?(:push_hooks, {})).to be false
+ context 'when the branch filter is a invalid regex' do
+ let(:hook) do
+ build(
+ :project_hook,
+ push_events: true,
+ push_events_branch_filter: 'master',
+ branch_filter_strategy: 'regex'
+ )
end
- context 'branch filter contains wildcard' do
- let(:branch_filter) { 'features/*' }
-
- it 'returns true if branch matches' do
- expect(filter.matches?(:push_hooks, { ref: 'refs/heads/features/my-branch' })).to be true
- expect(filter.matches?(:push_hooks, { ref: 'refs/heads/features/my-branch/something' })).to be true
- end
-
- it 'returns false if branch does not match' do
- expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be false
- end
+ before do
+ allow(hook).to receive(:push_events_branch_filter).and_return("invalid-regex[")
end
- end
- context 'branch filter is not specified' do
- let(:branch_filter) { nil }
-
- it 'returns true' do
- expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be true
- end
+ it { expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be false }
end
- context 'branch filter is empty string' do
- let(:branch_filter) { '' }
+ context 'when the branch filter is not properly set to nil' do
+ let(:hook) do
+ build(
+ :project_hook,
+ push_events: true,
+ branch_filter_strategy: 'all_branches'
+ )
+ end
- it 'acts like branch is not specified' do
- expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be true
+ before do
+ allow(hook).to receive(:push_events_branch_filter).and_return("master")
end
+
+ it { expect(filter.matches?(:push_hooks, { ref: 'refs/heads/feature1' })).to be true }
end
end
- context 'for non-push-events hooks' do
- let(:hook) do
- create(:project_hook, issues_events: true, push_events: false, push_events_branch_filter: '')
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(enhanced_webhook_support_regex: false)
end
- it 'returns true as branch filters are not yet supported for these' do
- expect(filter.matches?(:issues_events, { ref: 'refs/heads/master' })).to be true
+ let(:hook) do
+ build(
+ :project_hook,
+ push_events: true,
+ push_events_branch_filter: '(master)',
+ branch_filter_strategy: 'regex'
+ )
end
+
+ it { expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be false }
+ it { expect(filter.matches?(:push_hooks, { ref: 'refs/heads/(master)' })).to be true }
end
end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 923a6f92424..3d8c377ab21 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -12,14 +12,14 @@ RSpec.describe ProjectHook do
end
it_behaves_like 'includes Limitable concern' do
- subject { build(:project_hook, project: create(:project)) }
+ subject { build(:project_hook) }
end
describe '.for_projects' do
it 'finds related project hooks' do
- hook_a = create(:project_hook)
- hook_b = create(:project_hook)
- hook_c = create(:project_hook)
+ hook_a = create(:project_hook, project: build(:project))
+ hook_b = create(:project_hook, project: build(:project))
+ hook_c = create(:project_hook, project: build(:project))
expect(described_class.for_projects([hook_a.project, hook_b.project]))
.to contain_exactly(hook_a, hook_b)
@@ -30,16 +30,18 @@ RSpec.describe ProjectHook do
describe '.push_hooks' do
it 'returns hooks for push events only' do
- hook = create(:project_hook, push_events: true)
- create(:project_hook, push_events: false)
+ project = build(:project)
+ hook = create(:project_hook, project: project, push_events: true)
+ create(:project_hook, project: project, push_events: false)
expect(described_class.push_hooks).to eq([hook])
end
end
describe '.tag_push_hooks' do
it 'returns hooks for tag push events only' do
- hook = create(:project_hook, tag_push_events: true)
- create(:project_hook, tag_push_events: false)
+ project = build(:project)
+ hook = create(:project_hook, project: project, tag_push_events: true)
+ create(:project_hook, project: project, tag_push_events: false)
expect(described_class.tag_push_hooks).to eq([hook])
end
end
@@ -65,7 +67,7 @@ RSpec.describe ProjectHook do
end
describe '#update_last_failure', :clean_gitlab_redis_shared_state do
- let_it_be(:hook) { create(:project_hook) }
+ let(:hook) { build(:project_hook) }
it 'is a method of this class' do
expect { hook.update_last_failure }.not_to raise_error
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index f4786083b75..ba94730b1dd 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
RSpec.describe SystemHook do
context 'default attributes' do
- let(:system_hook) { build(:system_hook) }
+ let(:system_hook) { described_class.new }
it 'sets defined default parameters' do
attrs = {
@@ -32,10 +32,10 @@ RSpec.describe SystemHook do
end
describe "execute", :sidekiq_might_not_need_inline do
- let(:system_hook) { create(:system_hook) }
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let(:group) { create(:group) }
+ let_it_be(:system_hook) { create(:system_hook) }
+ let_it_be(:user) { create(:user) }
+ let(:project) { build(:project, namespace: user.namespace) }
+ let(:group) { build(:group) }
let(:params) do
{ name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: User.random_password }
end
@@ -145,6 +145,7 @@ RSpec.describe SystemHook do
end
it 'group member update hook' do
+ group = create(:group)
group.add_guest(user)
group.add_maintainer(user)
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
index 3441dfda7d6..fafca144cae 100644
--- a/spec/models/hooks/web_hook_log_spec.rb
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe WebHookLog do
it { is_expected.to validate_presence_of(:web_hook) }
describe '.recent' do
- let(:hook) { create(:project_hook) }
+ let(:hook) { build(:project_hook) }
it 'does not return web hook logs that are too old' do
create(:web_hook_log, web_hook: hook, created_at: 10.days.ago)
@@ -30,8 +30,10 @@ RSpec.describe WebHookLog do
end
describe '#save' do
+ let(:hook) { build(:project_hook) }
+
context 'with basic auth credentials' do
- let(:web_hook_log) { build(:web_hook_log, url: 'http://test:123@example.com') }
+ let(:web_hook_log) { build(:web_hook_log, web_hook: hook, url: 'http://test:123@example.com') }
subject { web_hook_log.save! }
@@ -45,9 +47,9 @@ RSpec.describe WebHookLog do
end
context "with users' emails" do
- let(:author) { create(:user) }
- let(:user) { create(:user) }
- let(:web_hook_log) { create(:web_hook_log, request_data: data) }
+ let(:author) { build(:user) }
+ let(:user) { build(:user) }
+ let(:web_hook_log) { create(:web_hook_log, web_hook: hook, request_data: data) }
let(:data) do
{
user: {
@@ -93,11 +95,12 @@ RSpec.describe WebHookLog do
end
describe '.delete_batch_for' do
- let(:hook) { create(:project_hook) }
+ let_it_be(:hook) { build(:project_hook) }
+ let_it_be(:hook2) { build(:project_hook) }
- before do
+ before_all do
create_list(:web_hook_log, 3, web_hook: hook)
- create_list(:web_hook_log, 3)
+ create_list(:web_hook_log, 3, web_hook: hook2)
end
subject { described_class.delete_batch_for(hook, batch_size: batch_size) }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index da8c10b67a6..db854670cc3 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -34,6 +34,11 @@ RSpec.describe WebHook do
it { is_expected.to allow_value({ 'x' => ('a' * 100) }).for(:url_variables) }
it { is_expected.to allow_value({ 'foo' => 'bar', 'bar' => 'baz' }).for(:url_variables) }
it { is_expected.to allow_value((1..20).to_h { ["k#{_1}", 'value'] }).for(:url_variables) }
+ it { is_expected.to allow_value({ 'MY-TOKEN' => 'bar' }).for(:url_variables) }
+ it { is_expected.to allow_value({ 'my_secr3t-token' => 'bar' }).for(:url_variables) }
+ it { is_expected.to allow_value({ 'x-y-z' => 'bar' }).for(:url_variables) }
+ it { is_expected.to allow_value({ 'x_y_z' => 'bar' }).for(:url_variables) }
+ it { is_expected.to allow_value({ 'f.o.o' => 'bar' }).for(:url_variables) }
it { is_expected.not_to allow_value([]).for(:url_variables) }
it { is_expected.not_to allow_value({ 'foo' => 1 }).for(:url_variables) }
@@ -45,6 +50,10 @@ RSpec.describe WebHook do
it { is_expected.not_to allow_value({ '' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value({ '1foo' => 'foo' }).for(:url_variables) }
it { is_expected.not_to allow_value((1..21).to_h { ["k#{_1}", 'value'] }).for(:url_variables) }
+ it { is_expected.not_to allow_value({ 'MY--TOKEN' => 'foo' }).for(:url_variables) }
+ it { is_expected.not_to allow_value({ 'MY__SECRET' => 'foo' }).for(:url_variables) }
+ it { is_expected.not_to allow_value({ 'x-_y' => 'foo' }).for(:url_variables) }
+ it { is_expected.not_to allow_value({ 'x..y' => 'foo' }).for(:url_variables) }
end
describe 'url' do
@@ -83,7 +92,7 @@ RSpec.describe WebHook do
subject { hook }
before do
- hook.url_variables = { 'one' => 'a', 'two' => 'b' }
+ hook.url_variables = { 'one' => 'a', 'two' => 'b', 'url' => 'http://example.com' }
end
it { is_expected.to allow_value('http://example.com').for(:url) }
@@ -92,6 +101,8 @@ RSpec.describe WebHook do
it { is_expected.to allow_value('http://example.com/{two}').for(:url) }
it { is_expected.to allow_value('http://user:s3cret@example.com/{two}').for(:url) }
it { is_expected.to allow_value('http://{one}:{two}@example.com').for(:url) }
+ it { is_expected.to allow_value('http://{one}').for(:url) }
+ it { is_expected.to allow_value('{url}').for(:url) }
it { is_expected.not_to allow_value('http://example.com/{one}/{two}/{three}').for(:url) }
it { is_expected.not_to allow_value('http://example.com/{foo}').for(:url) }
@@ -113,24 +124,81 @@ RSpec.describe WebHook do
end
describe 'push_events_branch_filter' do
- it { is_expected.to allow_values("good_branch_name", "another/good-branch_name").for(:push_events_branch_filter) }
- it { is_expected.to allow_values("").for(:push_events_branch_filter) }
- it { is_expected.not_to allow_values("bad branch name", "bad~branchname").for(:push_events_branch_filter) }
-
- it 'gets rid of whitespace' do
- hook.push_events_branch_filter = ' branch '
- hook.save!
+ before do
+ subject.branch_filter_strategy = strategy
+ end
+
+ context 'with "all branches" strategy' do
+ let(:strategy) { 'all_branches' }
+
+ it {
+ is_expected.to allow_values(
+ "good_branch_name",
+ "another/good-branch_name",
+ "good branch name",
+ "good~branchname",
+ "good_branchname(",
+ "good_branchname[",
+ ""
+ ).for(:push_events_branch_filter)
+ }
+ end
+
+ context 'with "wildcard" strategy' do
+ let(:strategy) { 'wildcard' }
+
+ it {
+ is_expected.to allow_values(
+ "good_branch_name",
+ "another/good-branch_name",
+ "good_branch_name(",
+ ""
+ ).for(:push_events_branch_filter)
+ }
+
+ it {
+ is_expected.not_to allow_values(
+ "bad branch name",
+ "bad~branchname",
+ "bad_branch_name["
+ ).for(:push_events_branch_filter)
+ }
+
+ it 'gets rid of whitespace' do
+ hook.push_events_branch_filter = ' branch '
+ hook.save!
+
+ expect(hook.push_events_branch_filter).to eq('branch')
+ end
- expect(hook.push_events_branch_filter).to eq('branch')
+ it 'stores whitespace only as empty' do
+ hook.push_events_branch_filter = ' '
+ hook.save!
+ expect(hook.push_events_branch_filter).to eq('')
+ end
end
- it 'stores whitespace only as empty' do
- hook.push_events_branch_filter = ' '
- hook.save!
+ context 'with "regex" strategy' do
+ let(:strategy) { 'regex' }
- expect(hook.push_events_branch_filter).to eq('')
+ it {
+ is_expected.to allow_values(
+ "good_branch_name",
+ "another/good-branch_name",
+ "good branch name",
+ "good~branch~name",
+ ""
+ ).for(:push_events_branch_filter)
+ }
+
+ it { is_expected.not_to allow_values("bad_branch_name(", "bad_branch_name[").for(:push_events_branch_filter) }
end
end
+
+ it "only consider these branch filter strategies are valid" do
+ expected_valid_types = %w[all_branches regex wildcard]
+ expect(described_class.branch_filter_strategies.keys).to contain_exactly(*expected_valid_types)
+ end
end
describe 'encrypted attributes' do
@@ -141,7 +209,7 @@ RSpec.describe WebHook do
describe '.web_hooks_disable_failed?' do
it 'returns true when feature is enabled for parent' do
- second_hook = build(:project_hook, project: create(:project))
+ second_hook = build(:project_hook)
stub_feature_flags(web_hooks_disable_failed: [false, second_hook.project])
expect(described_class.web_hooks_disable_failed?(hook)).to eq(false)
@@ -242,7 +310,7 @@ RSpec.describe WebHook do
end
describe '#executable?' do
- let(:web_hook) { create(:project_hook, project: project) }
+ let_it_be_with_reload(:web_hook) { create(:project_hook, project: project) }
where(:recent_failures, :not_until, :executable) do
[
@@ -389,7 +457,7 @@ RSpec.describe WebHook do
end
end
- describe 'backoff!' do
+ describe '#backoff!' do
context 'when we have not backed off before' do
it 'does not disable the hook' do
expect { hook.backoff! }.not_to change(hook, :executable?).from(true)
@@ -400,6 +468,26 @@ RSpec.describe WebHook do
end
end
+ context 'when the recent failure value is the max value of a smallint' do
+ before do
+ hook.update!(recent_failures: 32767, disabled_until: 1.hour.ago)
+ end
+
+ it 'reduces to MAX_FAILURES' do
+ expect { hook.backoff! }.to change(hook, :recent_failures).to(described_class::MAX_FAILURES)
+ end
+ end
+
+ context 'when the recent failure value is MAX_FAILURES' do
+ before do
+ hook.update!(recent_failures: described_class::MAX_FAILURES, disabled_until: 1.hour.ago)
+ end
+
+ it 'does not change recent_failures' do
+ expect { hook.backoff! }.not_to change(hook, :recent_failures)
+ end
+ end
+
context 'when we have exhausted the grace period' do
before do
hook.update!(recent_failures: described_class::FAILURE_THRESHOLD)
@@ -459,11 +547,21 @@ RSpec.describe WebHook do
end
end
- describe 'failed!' do
+ describe '#failed!' do
it 'increments the failure count' do
expect { hook.failed! }.to change(hook, :recent_failures).by(1)
end
+ context 'when the recent failure value is the max value of a smallint' do
+ before do
+ hook.update!(recent_failures: 32767)
+ end
+
+ it 'does not change recent_failures' do
+ expect { hook.failed! }.not_to change(hook, :recent_failures)
+ end
+ end
+
it 'does not update the hook if the the failure count exceeds the maximum value' do
hook.recent_failures = described_class::MAX_FAILURES
@@ -670,4 +768,14 @@ RSpec.describe WebHook do
expect { described_class.new.update_last_failure }.not_to raise_error
end
end
+
+ describe '#masked_token' do
+ it { expect(hook.masked_token).to be_nil }
+
+ context 'with a token' do
+ let(:hook) { build(:project_hook, :token, project: project) }
+
+ it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
+ end
+ end
end
diff --git a/spec/models/incident_management/timeline_event_spec.rb b/spec/models/incident_management/timeline_event_spec.rb
index d288cc1a75d..036f5affb87 100644
--- a/spec/models/incident_management/timeline_event_spec.rb
+++ b/spec/models/incident_management/timeline_event_spec.rb
@@ -27,6 +27,7 @@ RSpec.describe IncidentManagement::TimelineEvent do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:incident) }
it { is_expected.to validate_presence_of(:note) }
+ it { is_expected.to validate_length_of(:note).is_at_most(280).on(:user_input) }
it { is_expected.to validate_length_of(:note).is_at_most(10_000) }
it { is_expected.to validate_length_of(:note_html).is_at_most(10_000) }
it { is_expected.to validate_presence_of(:occurred_at) }
diff --git a/spec/models/incident_management/timeline_event_tag_spec.rb b/spec/models/incident_management/timeline_event_tag_spec.rb
index cff8ad8469f..1ec4fa30fb5 100644
--- a/spec/models/incident_management/timeline_event_tag_spec.rb
+++ b/spec/models/incident_management/timeline_event_tag_spec.rb
@@ -18,11 +18,46 @@ RSpec.describe IncidentManagement::TimelineEventTag do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
- it { is_expected.to validate_uniqueness_of(:name).scoped_to([:project_id]) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to([:project_id]).ignoring_case_sensitivity }
it { is_expected.to allow_value('Test tag 1').for(:name) }
it { is_expected.not_to allow_value('Test tag, 1').for(:name) }
it { is_expected.not_to allow_value('').for(:name) }
it { is_expected.not_to allow_value('s' * 256).for(:name) }
end
+
+ describe '.pluck_names' do
+ it 'returns the names of the tags' do
+ tag1 = create(:incident_management_timeline_event_tag)
+ tag2 = create(:incident_management_timeline_event_tag)
+
+ expect(described_class.pluck_names).to contain_exactly(tag1.name, tag2.name)
+ end
+ end
+
+ describe 'constants' do
+ it { expect(described_class::START_TIME_TAG_NAME).to eq('Start time') }
+ it { expect(described_class::END_TIME_TAG_NAME).to eq('End time') }
+ end
+
+ describe '#by_names scope' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:tag1) { create(:incident_management_timeline_event_tag, name: 'Test tag 1', project: project) }
+ let_it_be(:tag2) { create(:incident_management_timeline_event_tag, name: 'Test tag 2', project: project) }
+ let_it_be(:tag3) { create(:incident_management_timeline_event_tag, name: 'Test tag 3', project: project2) }
+
+ it 'returns two matching tags' do
+ expect(described_class.by_names(['Test tag 1', 'Test tag 2'])).to contain_exactly(tag1, tag2)
+ end
+
+ it 'returns tags on the project' do
+ expect(project2.incident_management_timeline_event_tags.by_names(['Test tag 1',
+ 'Test tag 3'])).to contain_exactly(tag3)
+ end
+
+ it 'returns one matching tag with case insensitive' do
+ expect(described_class.by_names(['tESt tAg 2'])).to contain_exactly(tag2)
+ end
+ end
end
diff --git a/spec/models/instance_metadata_spec.rb b/spec/models/instance_metadata_spec.rb
index 5fc073c392d..46fd165e065 100644
--- a/spec/models/instance_metadata_spec.rb
+++ b/spec/models/instance_metadata_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe InstanceMetadata do
expect(subject).to have_attributes(
version: Gitlab::VERSION,
revision: Gitlab.revision,
- kas: kind_of(::InstanceMetadata::Kas)
+ kas: kind_of(::InstanceMetadata::Kas),
+ enterprise: Gitlab.ee?
)
end
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index baa3443b4c5..4938e1797af 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -15,6 +15,23 @@ RSpec.describe Integration do
it { is_expected.to have_one(:jira_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::JiraTrackerData') }
end
+ describe 'default values' do
+ it { is_expected.to be_alert_events }
+ it { is_expected.to be_commit_events }
+ it { is_expected.to be_confidential_issues_events }
+ it { is_expected.to be_confidential_note_events }
+ it { is_expected.to be_issues_events }
+ it { is_expected.to be_job_events }
+ it { is_expected.to be_merge_requests_events }
+ it { is_expected.to be_note_events }
+ it { is_expected.to be_pipeline_events }
+ it { is_expected.to be_push_events }
+ it { is_expected.to be_tag_push_events }
+ it { is_expected.to be_wiki_page_events }
+ it { is_expected.not_to be_active }
+ it { expect(subject.category).to eq(:common) }
+ end
+
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) }
@@ -60,10 +77,10 @@ RSpec.describe Integration do
describe 'Scopes' do
describe '.third_party_wikis' do
- let!(:integration1) { create(:jira_integration) }
- let!(:integration2) { create(:redmine_integration) }
- let!(:integration3) { create(:confluence_integration) }
- let!(:integration4) { create(:shimo_integration) }
+ let!(:integration1) { create(:jira_integration, project: project) }
+ let!(:integration2) { create(:redmine_integration, project: project) }
+ let!(:integration3) { create(:confluence_integration, project: project) }
+ let!(:integration4) { create(:shimo_integration, project: project) }
it 'returns the right group integration' do
expect(described_class.third_party_wikis).to contain_exactly(integration3, integration4)
@@ -89,7 +106,7 @@ RSpec.describe Integration do
end
describe '.by_type' do
- let!(:integration1) { create(:jira_integration) }
+ let!(:integration1) { create(:jira_integration, project: project) }
let!(:integration2) { create(:jira_integration) }
let!(:integration3) { create(:redmine_integration) }
@@ -110,7 +127,7 @@ RSpec.describe Integration do
describe '.for_group' do
let!(:integration1) { create(:jira_integration, project_id: nil, group_id: group.id) }
- let!(:integration2) { create(:jira_integration) }
+ let!(:integration2) { create(:jira_integration, project: project) }
it 'returns the right group integration' do
expect(described_class.for_group(group)).to contain_exactly(integration1)
@@ -217,9 +234,19 @@ RSpec.describe Integration do
end
end
+ describe '#chat?' do
+ it 'is true when integration is chat integration' do
+ expect(build(:mattermost_integration).chat?).to eq(true)
+ end
+
+ it 'is false when integration is not chat integration' do
+ expect(build(:integration).chat?).to eq(false)
+ end
+ end
+
describe '.find_or_initialize_non_project_specific_integration' do
let!(:integration_1) { create(:jira_integration, project_id: nil, group_id: group.id) }
- let!(:integration_2) { create(:jira_integration) }
+ let!(:integration_2) { create(:jira_integration, project: project) }
it 'returns the right integration' do
expect(Integration.find_or_initialize_non_project_specific_integration('jira', group_id: group))
@@ -374,7 +401,7 @@ RSpec.describe Integration do
context 'when data is stored in properties' do
let(:properties) { data_params }
let!(:integration) do
- create(:jira_integration, :without_properties_callback, properties: properties.merge(additional: 'something'))
+ create(:jira_integration, :without_properties_callback, project: project, properties: properties.merge(additional: 'something'))
end
it_behaves_like 'integration creation from an integration'
@@ -382,7 +409,7 @@ RSpec.describe Integration do
context 'when data are stored in separated fields' do
let(:integration) do
- create(:jira_integration, data_params.merge(properties: {}))
+ create(:jira_integration, data_params.merge(properties: {}, project: project))
end
it_behaves_like 'integration creation from an integration'
@@ -391,7 +418,7 @@ RSpec.describe Integration do
context 'when data are stored in both properties and separated fields' do
let(:properties) { data_params }
let(:integration) do
- create(:jira_integration, :without_properties_callback, active: true, properties: properties).tap do |integration|
+ create(:jira_integration, :without_properties_callback, project: project, active: true, properties: properties).tap do |integration|
create(:jira_tracker_data, data_params.merge(integration: integration))
end
end
@@ -1233,11 +1260,11 @@ RSpec.describe Integration do
describe '#attributes' do
it 'does not include properties' do
- expect(create(:integration).attributes).not_to have_key('properties')
+ expect(build(:integration, project: project).attributes).not_to have_key('properties')
end
it 'can be used in assign_attributes without nullifying properties' do
- record = create(:integration, :instance, properties: { url: generate(:url) })
+ record = build(:integration, :instance, properties: { url: generate(:url) })
attrs = record.attributes
@@ -1246,7 +1273,7 @@ RSpec.describe Integration do
end
describe '#dup' do
- let(:original) { create(:integration, properties: { one: 1, two: 2, three: 3 }) }
+ let(:original) { build(:integration, project: project, properties: { one: 1, two: 2, three: 3 }) }
it 'results in distinct ciphertexts, but identical properties' do
copy = original.dup
@@ -1259,7 +1286,7 @@ RSpec.describe Integration do
end
context 'when the model supports data-fields' do
- let(:original) { create(:jira_integration, username: generate(:username), url: generate(:url)) }
+ let(:original) { build(:jira_integration, project: project, username: generate(:username), url: generate(:url)) }
it 'creates distinct but identical data-fields' do
copy = original.dup
diff --git a/spec/models/integrations/assembla_spec.rb b/spec/models/integrations/assembla_spec.rb
index 960dfea3dc4..e9f4274952d 100644
--- a/spec/models/integrations/assembla_spec.rb
+++ b/spec/models/integrations/assembla_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe Integrations::Assembla do
include StubRequests
+ it_behaves_like Integrations::ResetSecretFields do
+ let(:integration) { described_class.new }
+ end
+
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/models/integrations/bamboo_spec.rb b/spec/models/integrations/bamboo_spec.rb
index e92226d109f..1d2c90dad51 100644
--- a/spec/models/integrations/bamboo_spec.rb
+++ b/spec/models/integrations/bamboo_spec.rb
@@ -23,6 +23,10 @@ RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
)
end
+ it_behaves_like Integrations::BaseCi
+
+ it_behaves_like Integrations::ResetSecretFields
+
include_context Integrations::EnableSslVerification
describe 'Validations' do
@@ -77,48 +81,6 @@ RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
end
end
- describe 'Callbacks' do
- describe 'before_validation :reset_password' do
- context 'when a password was previously set' do
- it 'resets password if url changed' do
- integration.bamboo_url = 'http://gitlab1.com'
-
- expect(integration).not_to be_valid
- expect(integration.password).to be_nil
- end
-
- it 'does not reset password if username changed' do
- integration.username = 'some_name'
-
- expect(integration).to be_valid
- expect(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
- integration.bamboo_url = 'http://gitlab_edited.com'
- integration.password = 'password'
-
- expect(integration).to be_valid
- expect(integration.password).to eq('password')
- expect(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
- integration.password = nil
-
- integration.bamboo_url = 'http://gitlab_edited.com'
- integration.password = 'password'
- integration.save!
-
- expect(integration.reload).to have_attributes(
- bamboo_url: 'http://gitlab_edited.com',
- password: 'password'
- )
- end
- end
- end
-
describe '#execute' do
it 'runs update and build action' do
stub_update_and_build_request
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index eb503e501d6..b959ead2cae 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -3,11 +3,15 @@
require 'spec_helper'
RSpec.describe Integrations::BaseChatNotification do
+ describe 'default values' do
+ it { expect(subject.category).to eq(:chat) }
+ end
+
describe 'validations' do
before do
allow(subject).to receive(:activated?).and_return(true)
allow(subject).to receive(:default_channel_placeholder).and_return('placeholder')
- allow(subject).to receive(:webhook_placeholder).and_return('placeholder')
+ allow(subject).to receive(:webhook_help).and_return('help')
end
it { is_expected.to validate_presence_of :webhook }
@@ -19,7 +23,7 @@ RSpec.describe Integrations::BaseChatNotification do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ let(:user) { build_stubbed(:user) }
let(:webhook_url) { 'https://example.gitlab.com/' }
let(:data) { Gitlab::DataBuilder::Push.build_sample(subject.project, user) }
@@ -44,7 +48,7 @@ RSpec.describe Integrations::BaseChatNotification do
context 'with an empty repository' do
it 'returns true' do
- subject.project = create(:project, :empty_repo)
+ subject.project = build_stubbed(:project, :empty_repo)
expect(chat_integration).to receive(:notify).and_return(true)
expect(chat_integration.execute(data)).to be true
@@ -61,9 +65,9 @@ RSpec.describe Integrations::BaseChatNotification do
end
context 'when the data object has a label' do
- let_it_be(:label) { create(:label, name: 'Bug') }
- let_it_be(:label_2) { create(:label, name: 'Community contribution') }
- let_it_be(:label_3) { create(:label, name: 'Backend') }
+ let_it_be(:label) { create(:label, project: project, name: 'Bug') }
+ let_it_be(:label_2) { create(:label, project: project, name: 'Community contribution') }
+ let_it_be(:label_3) { create(:label, project: project, name: 'Backend') }
let_it_be(:issue) { create(:labeled_issue, project: project, labels: [label, label_2, label_3]) }
let_it_be(:note) { create(:note, noteable: issue, project: project) }
@@ -93,7 +97,7 @@ RSpec.describe Integrations::BaseChatNotification do
it_behaves_like 'notifies the chat integration'
context 'MergeRequest events' do
- let(:data) { create(:merge_request, labels: [label]).to_hook_data(user) }
+ let(:data) { build_stubbed(:merge_request, source_project: project, labels: [label]).to_hook_data(user) }
it_behaves_like 'notifies the chat integration'
end
@@ -280,9 +284,9 @@ RSpec.describe Integrations::BaseChatNotification do
end
end
- describe '#webhook_placeholder' do
+ describe '#webhook_help' do
it 'raises an error' do
- expect { subject.webhook_placeholder }.to raise_error(NotImplementedError)
+ expect { subject.webhook_help }.to raise_error(NotImplementedError)
end
end
diff --git a/spec/models/integrations/base_issue_tracker_spec.rb b/spec/models/integrations/base_issue_tracker_spec.rb
index 37f7d99717c..e1a764cd7cb 100644
--- a/spec/models/integrations/base_issue_tracker_spec.rb
+++ b/spec/models/integrations/base_issue_tracker_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe Integrations::BaseIssueTracker do
let_it_be_with_refind(:project) { create :project }
+ describe 'default values' do
+ it { expect(subject.category).to eq(:issue_tracker) }
+ end
+
describe 'Validations' do
describe 'only one issue tracker per project' do
before do
diff --git a/spec/models/integrations/base_third_party_wiki_spec.rb b/spec/models/integrations/base_third_party_wiki_spec.rb
index 11e044c2a18..dbead636cb9 100644
--- a/spec/models/integrations/base_third_party_wiki_spec.rb
+++ b/spec/models/integrations/base_third_party_wiki_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
RSpec.describe Integrations::BaseThirdPartyWiki do
+ describe 'default values' do
+ it { expect(subject.category).to eq(:third_party_wiki) }
+ end
+
describe 'Validations' do
let_it_be_with_reload(:project) { create(:project) }
diff --git a/spec/models/integrations/buildkite_spec.rb b/spec/models/integrations/buildkite_spec.rb
index c720dc6d418..5f62c68bd2b 100644
--- a/spec/models/integrations/buildkite_spec.rb
+++ b/spec/models/integrations/buildkite_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
include StubRequests
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
subject(:integration) do
described_class.create!(
@@ -18,6 +18,10 @@ RSpec.describe Integrations::Buildkite, :use_clean_rails_memory_store_caching do
)
end
+ it_behaves_like Integrations::BaseCi
+
+ it_behaves_like Integrations::ResetSecretFields
+
it_behaves_like Integrations::HasWebHook do
let(:hook_url) { 'https://webhook.buildkite.com/deliver/{webhook_token}' }
end
diff --git a/spec/models/integrations/campfire_spec.rb b/spec/models/integrations/campfire_spec.rb
index a6bcd22b6f6..ae923cd38fc 100644
--- a/spec/models/integrations/campfire_spec.rb
+++ b/spec/models/integrations/campfire_spec.rb
@@ -34,8 +34,8 @@ RSpec.describe Integrations::Campfire do
end
describe "#execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let(:user) { build_stubbed(:user) }
+ let(:project) { build_stubbed(:project, :repository) }
before do
@campfire_integration = described_class.new
diff --git a/spec/models/integrations/chat_message/pipeline_message_spec.rb b/spec/models/integrations/chat_message/pipeline_message_spec.rb
index a63cc0b6d83..f3388853b37 100644
--- a/spec/models/integrations/chat_message/pipeline_message_spec.rb
+++ b/spec/models/integrations/chat_message/pipeline_message_spec.rb
@@ -44,13 +44,18 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
before do
test_commit = double("A test commit", committer: args[:user], title: "A test commit message")
- test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url])
+ test_project = build(:project, name: args[:project][:name])
+
+ allow(test_project).to receive(:commit_by).and_return(test_commit)
+ allow(test_project).to receive(:web_url).and_return(args[:project][:web_url])
allow(test_project).to receive(:avatar_url).with(no_args).and_return("/avatar")
allow(test_project).to receive(:avatar_url).with(only_path: false).and_return(args[:project][:avatar_url])
allow(Project).to receive(:find) { test_project }
- test_pipeline = double("A test pipeline",
- has_yaml_errors?: has_yaml_errors, yaml_errors: "yaml error description here")
+ test_pipeline = build(:ci_empty_pipeline, name: 'Build pipeline')
+
+ allow(test_pipeline).to receive(:has_yaml_errors?).and_return(has_yaml_errors)
+ allow(test_pipeline).to receive(:yaml_errors).and_return("yaml error description here")
allow(Ci::Pipeline).to receive(:find) { test_pipeline }
allow(Gitlab::UrlBuilder).to receive(:build).with(test_commit).and_return("http://example.com/commit")
@@ -69,6 +74,24 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
)
end
+ it 'returns pipeline name' do
+ name_field = subject.attachments.first[:fields].find { |a| a[:title] == 'Pipeline name' }
+
+ expect(name_field[:value]).to eq('Build pipeline')
+ end
+
+ context 'when pipeline_name feature flag is disabled' do
+ before do
+ stub_feature_flags(pipeline_name: false)
+ end
+
+ it 'does not return pipeline name' do
+ name_field = subject.attachments.first[:fields].find { |a| a[:title] == 'Pipeline name' }
+
+ expect(name_field).to be nil
+ end
+ end
+
context "when the pipeline failed" do
before do
args[:object_attributes][:status] = 'failed'
@@ -204,8 +227,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
expect(subject.attachments.first[:title_link]).to eq("http://example.gitlab.com/-/pipelines/123")
end
- it "returns two attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(2)
+ it "returns three attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(3)
end
it "returns the commit message as the attachment's second field property" do
@@ -232,8 +255,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
]
end
- it "returns four attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(4)
+ it "returns five attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(5)
end
it "returns the stage name and link to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
@@ -337,8 +360,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
context "when the CI config file contains a YAML error" do
let(:has_yaml_errors) { true }
- it "returns three attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(3)
+ it "returns four attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(4)
end
it "returns the YAML error deatils as the attachment's third field property" do
diff --git a/spec/models/integrations/confluence_spec.rb b/spec/models/integrations/confluence_spec.rb
index e2f9316bc95..999a532527d 100644
--- a/spec/models/integrations/confluence_spec.rb
+++ b/spec/models/integrations/confluence_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Integrations::Confluence do
+ let_it_be(:project) { create(:project) }
+
describe 'Validations' do
before do
subject.active = active
@@ -40,7 +42,6 @@ RSpec.describe Integrations::Confluence do
describe '#help' do
it 'can correctly return a link to the project wiki when active' do
- project = create(:project)
subject.project = project
subject.active = true
@@ -62,8 +63,6 @@ RSpec.describe Integrations::Confluence do
end
describe 'Caching has_confluence on project_settings' do
- let(:project) { create(:project) }
-
subject { project.project_setting.has_confluence? }
it 'sets the property to true when integration is active' do
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
index 71a5bbc4db1..65ecd9bee83 100644
--- a/spec/models/integrations/datadog_spec.rb
+++ b/spec/models/integrations/datadog_spec.rb
@@ -73,7 +73,15 @@ RSpec.describe Integrations::Datadog do
it { is_expected.to validate_presence_of(:datadog_site) }
it { is_expected.not_to validate_presence_of(:api_url) }
+ it { is_expected.to allow_value('data-dog-hq.com').for(:datadog_site) }
+ it { is_expected.to allow_value('dataDOG.com').for(:datadog_site) }
it { is_expected.not_to allow_value('datadog hq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('-datadoghq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('.datadoghq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('datadoghq.com_').for(:datadog_site) }
+ it { is_expected.not_to allow_value('data-dog').for(:datadog_site) }
+ it { is_expected.not_to allow_value('datadoghq.com-').for(:datadog_site) }
+ it { is_expected.not_to allow_value('datadoghq.com.').for(:datadog_site) }
end
context 'with custom api_url' do
diff --git a/spec/models/integrations/discord_spec.rb b/spec/models/integrations/discord_spec.rb
index eb90acc73be..138a56d1872 100644
--- a/spec/models/integrations/discord_spec.rb
+++ b/spec/models/integrations/discord_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Integrations::Discord do
let_it_be(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ let(:user) { build_stubbed(:user) }
let(:webhook_url) { "https://example.gitlab.com/" }
let(:sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
diff --git a/spec/models/integrations/drone_ci_spec.rb b/spec/models/integrations/drone_ci_spec.rb
index f3203a6e69d..6ff6888e0d3 100644
--- a/spec/models/integrations/drone_ci_spec.rb
+++ b/spec/models/integrations/drone_ci_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
subject(:integration) { described_class.new }
+ let_it_be(:project) { create(:project, :repository, name: 'project') }
+
+ it_behaves_like Integrations::BaseCi
+
it_behaves_like Integrations::ResetSecretFields do
let(:integration) { subject }
end
@@ -43,7 +47,6 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
)
end
- let(:project) { create(:project, :repository, name: 'project') }
let(:path) { project.full_path }
let(:drone_url) { 'http://drone.example.com' }
let(:sha) { '2ab7834c' }
@@ -192,7 +195,7 @@ RSpec.describe Integrations::DroneCi, :use_clean_rails_memory_store_caching do
describe "execute" do
include_context :drone_ci_integration
- let(:user) { create(:user, username: 'username') }
+ let(:user) { build(:user, username: 'username') }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
diff --git a/spec/models/integrations/emails_on_push_spec.rb b/spec/models/integrations/emails_on_push_spec.rb
index 15aa105e379..b3fe6bf9506 100644
--- a/spec/models/integrations/emails_on_push_spec.rb
+++ b/spec/models/integrations/emails_on_push_spec.rb
@@ -87,8 +87,8 @@ RSpec.describe Integrations::EmailsOnPush do
end
describe '#execute' do
+ let_it_be(:project) { create(:project, :repository) }
let(:push_data) { { object_kind: 'push' } }
- let(:project) { create(:project, :repository) }
let(:integration) { create(:emails_on_push_integration, project: project) }
let(:recipients) { 'test@gitlab.com' }
diff --git a/spec/models/integrations/hangouts_chat_spec.rb b/spec/models/integrations/hangouts_chat_spec.rb
index 828bcdf5d8f..288478b494e 100644
--- a/spec/models/integrations/hangouts_chat_spec.rb
+++ b/spec/models/integrations/hangouts_chat_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Integrations::HangoutsChat do
end
context 'with issue events' do
- let(:issues_sample_data) { create(:issue).to_hook_data(user) }
+ let(:issues_sample_data) { create(:issue, project: project).to_hook_data(user) }
it "adds thread key for issue events" do
expect(chat_integration.execute(issues_sample_data)).to be(true)
@@ -58,7 +58,7 @@ RSpec.describe Integrations::HangoutsChat do
end
context 'with merge events' do
- let(:merge_sample_data) { create(:merge_request).to_hook_data(user) }
+ let(:merge_sample_data) { create(:merge_request, source_project: project).to_hook_data(user) }
it "adds thread key for merge events" do
expect(chat_integration.execute(merge_sample_data)).to be(true)
@@ -71,7 +71,7 @@ RSpec.describe Integrations::HangoutsChat do
context 'with wiki page events' do
let(:wiki_page_sample_data) do
- Gitlab::DataBuilder::WikiPage.build(create(:wiki_page, message: 'foo'), user, 'create')
+ Gitlab::DataBuilder::WikiPage.build(create(:wiki_page, project: project, message: 'foo'), user, 'create')
end
it "adds thread key for wiki page events" do
diff --git a/spec/models/integrations/harbor_spec.rb b/spec/models/integrations/harbor_spec.rb
index 9ab37a92e89..b4580028112 100644
--- a/spec/models/integrations/harbor_spec.rb
+++ b/spec/models/integrations/harbor_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe Integrations::Harbor do
end
context 'ci variables' do
- let(:harbor_integration) { create(:harbor_integration) }
+ let(:harbor_integration) { build_stubbed(:harbor_integration) }
it 'returns vars when harbor_integration is activated' do
ci_vars = [
@@ -85,9 +85,12 @@ RSpec.describe Integrations::Harbor do
expect(harbor_integration.ci_variables).to match_array(ci_vars)
end
- it 'returns [] when harbor_integration is inactive' do
- harbor_integration.update!(active: false)
- expect(harbor_integration.ci_variables).to match_array([])
+ context 'when harbor_integration is inactive' do
+ let(:harbor_integration) { build_stubbed(:harbor_integration, active: false) }
+
+ it 'returns []' do
+ expect(harbor_integration.ci_variables).to match_array([])
+ end
end
context 'with robot username' do
diff --git a/spec/models/integrations/jenkins_spec.rb b/spec/models/integrations/jenkins_spec.rb
index 200de1305e2..0264982f0dc 100644
--- a/spec/models/integrations/jenkins_spec.rb
+++ b/spec/models/integrations/jenkins_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Integrations::Jenkins do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:jenkins_integration) { described_class.new(jenkins_params) }
let(:jenkins_url) { 'http://jenkins.example.com/' }
let(:jenkins_hook_url) { jenkins_url + 'project/my_project' }
@@ -23,6 +23,12 @@ RSpec.describe Integrations::Jenkins do
}
end
+ it_behaves_like Integrations::BaseCi
+
+ it_behaves_like Integrations::ResetSecretFields do
+ let(:integration) { jenkins_integration }
+ end
+
include_context Integrations::EnableSslVerification do
let(:integration) { jenkins_integration }
end
@@ -38,7 +44,7 @@ RSpec.describe Integrations::Jenkins do
expect(jenkins_integration.tag_push_events).to eq(false)
end
- describe 'username validation' do
+ describe 'Validations' do
let(:jenkins_integration) do
described_class.create!(
active: active,
@@ -57,28 +63,44 @@ RSpec.describe Integrations::Jenkins do
context 'when the integration is active' do
let(:active) { true }
- context 'when password was not touched' do
- before do
- allow(subject).to receive(:password_touched?).and_return(false)
+ describe '#username' do
+ context 'when password was not touched' do
+ before do
+ allow(subject).to receive(:password_touched?).and_return(false)
+ end
+
+ it { is_expected.not_to validate_presence_of :username }
end
- it { is_expected.not_to validate_presence_of :username }
- end
+ context 'when password was touched' do
+ before do
+ allow(subject).to receive(:password_touched?).and_return(true)
+ end
- context 'when password was touched' do
- before do
- allow(subject).to receive(:password_touched?).and_return(true)
+ it { is_expected.to validate_presence_of :username }
end
- it { is_expected.to validate_presence_of :username }
+ context 'when password is blank' do
+ it 'does not validate the username' do
+ expect(subject).not_to validate_presence_of :username
+
+ subject.password = ''
+ subject.save!
+ end
+ end
end
- context 'when password is blank' do
- it 'does not validate the username' do
- expect(subject).not_to validate_presence_of :username
+ describe '#password' do
+ it 'does not validate the presence of password if username is nil' do
+ subject.username = nil
+
+ expect(subject).not_to validate_presence_of(:password)
+ end
+
+ it 'validates the presence of password if username is present' do
+ subject.username = 'john'
- subject.password = ''
- subject.save!
+ expect(subject).to validate_presence_of(:password)
end
end
end
@@ -87,6 +109,7 @@ RSpec.describe Integrations::Jenkins do
let(:active) { false }
it { is_expected.not_to validate_presence_of :username }
+ it { is_expected.not_to validate_presence_of :password }
end
end
@@ -144,8 +167,7 @@ RSpec.describe Integrations::Jenkins do
describe '#test' do
it 'returns the right status' do
- user = create(:user, username: 'username')
- project = create(:project, name: 'project')
+ user = build(:user, username: 'username')
push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
jenkins_integration = described_class.create!(jenkins_params)
stub_request(:post, jenkins_hook_url).with(headers: { 'Authorization' => jenkins_authorization })
@@ -157,9 +179,9 @@ RSpec.describe Integrations::Jenkins do
end
describe '#execute' do
- let(:user) { create(:user, username: 'username') }
- let(:namespace) { create(:group, :private) }
- let(:project) { create(:project, :private, name: 'project', namespace: namespace) }
+ let(:user) { build(:user, username: 'username') }
+ let_it_be(:namespace) { create(:group, :private) }
+ let_it_be(:project) { create(:project, :private, name: 'project', namespace: namespace) }
let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:jenkins_integration) { described_class.create!(jenkins_params) }
@@ -191,82 +213,4 @@ RSpec.describe Integrations::Jenkins do
).to have_been_made.once
end
end
-
- describe 'Stored password invalidation' do
- let(:project) { create(:project) }
-
- context 'when a password was previously set' do
- let(:jenkins_integration) do
- described_class.create!(
- project: project,
- properties: {
- jenkins_url: 'http://jenkins.example.com/',
- username: 'jenkins',
- password: 'password'
- }
- )
- end
-
- it 'resets password if url changed' do
- jenkins_integration.jenkins_url = 'http://jenkins-edited.example.com/'
- jenkins_integration.valid?
-
- expect(jenkins_integration.password).to be_nil
- end
-
- it 'resets password if username is blank' do
- jenkins_integration.username = ''
- jenkins_integration.valid?
-
- expect(jenkins_integration.password).to be_nil
- end
-
- it 'does not reset password if username changed' do
- jenkins_integration.username = 'some_name'
- jenkins_integration.valid?
-
- expect(jenkins_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
- jenkins_integration.jenkins_url = 'http://jenkins_edited.example.com/'
- jenkins_integration.password = 'password'
- jenkins_integration.valid?
-
- expect(jenkins_integration.password).to eq('password')
- expect(jenkins_integration.jenkins_url).to eq('http://jenkins_edited.example.com/')
- end
-
- it 'resets password if url changed, even if setter called multiple times' do
- jenkins_integration.jenkins_url = 'http://jenkins1.example.com/'
- jenkins_integration.jenkins_url = 'http://jenkins1.example.com/'
- jenkins_integration.valid?
-
- expect(jenkins_integration.password).to be_nil
- end
- end
-
- context 'when no password was previously set' do
- let(:jenkins_integration) do
- described_class.create!(
- project: create(:project),
- properties: {
- jenkins_url: 'http://jenkins.example.com/',
- username: 'jenkins'
- }
- )
- end
-
- it 'saves password if new url is set together with password' do
- jenkins_integration.jenkins_url = 'http://jenkins_edited.example.com/'
- jenkins_integration.password = 'password'
- jenkins_integration.save!
-
- expect(jenkins_integration.reload).to have_attributes(
- jenkins_url: 'http://jenkins_edited.example.com/',
- password: 'password'
- )
- end
- end
- end
end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index 9f928442b28..819dad9d46d 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Integrations::Jira do
end
before do
- WebMock.stub_request(:get, /serverInfo/).to_return(body: server_info_results.to_json )
+ WebMock.stub_request(:get, /serverInfo/).to_return(body: server_info_results.to_json)
end
it_behaves_like Integrations::ResetSecretFields do
@@ -162,7 +162,7 @@ RSpec.describe Integrations::Jira do
end
describe '#fields' do
- let(:integration) { create(:jira_integration) }
+ let(:integration) { jira_integration }
subject(:fields) { integration.fields }
@@ -172,7 +172,7 @@ RSpec.describe Integrations::Jira do
end
describe '#sections' do
- let(:integration) { create(:jira_integration) }
+ let(:integration) { jira_integration }
subject(:sections) { integration.sections.map { |s| s[:type] } }
@@ -332,28 +332,7 @@ RSpec.describe Integrations::Jira do
# we need to make sure we are able to read both from properties and jira_tracker_data table
# TODO: change this as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
context 'overriding properties' do
- let(:access_params) do
- { url: url, api_url: api_url, username: username, password: password,
- jira_issue_transition_id: transition_id }
- end
-
- let(:data_params) do
- {
- url: url, api_url: api_url,
- username: username, password: password,
- jira_issue_transition_id: transition_id
- }
- end
-
shared_examples 'handles jira fields' do
- let(:data_params) do
- {
- url: url, api_url: api_url,
- username: username, password: password,
- jira_issue_transition_id: transition_id
- }
- end
-
context 'reading data' do
it 'reads data correctly' do
expect(integration.url).to eq(url)
@@ -449,32 +428,40 @@ RSpec.describe Integrations::Jira do
end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
- context 'when data are stored in properties' do
- let(:properties) { data_params }
- let!(:integration) do
- create(:jira_integration, :without_properties_callback, properties: properties.merge(additional: 'something'))
+ context 'with properties' do
+ let(:data_params) do
+ {
+ url: url, api_url: api_url,
+ username: username, password: password,
+ jira_issue_transition_id: transition_id
+ }
end
- it_behaves_like 'handles jira fields'
- end
+ context 'when data are stored in properties' do
+ let(:integration) do
+ create(:jira_integration, :without_properties_callback, project: project, properties: data_params.merge(additional: 'something'))
+ end
- context 'when data are stored in separated fields' do
- let(:integration) do
- create(:jira_integration, data_params.merge(properties: {}))
+ it_behaves_like 'handles jira fields'
end
- it_behaves_like 'handles jira fields'
- end
-
- context 'when data are stored in both properties and separated fields' do
- let(:properties) { data_params }
- let(:integration) do
- create(:jira_integration, :without_properties_callback, properties: properties).tap do |integration|
- create(:jira_tracker_data, data_params.merge(integration: integration))
+ context 'when data are stored in separated fields' do
+ let(:integration) do
+ create(:jira_integration, data_params.merge(properties: {}, project: project))
end
+
+ it_behaves_like 'handles jira fields'
end
- it_behaves_like 'handles jira fields'
+ context 'when data are stored in both properties and separated fields' do
+ let(:integration) do
+ create(:jira_integration, :without_properties_callback, properties: data_params, project: project).tap do |integration|
+ create(:jira_tracker_data, data_params.merge(integration: integration))
+ end
+ end
+
+ it_behaves_like 'handles jira fields'
+ end
end
end
@@ -872,7 +859,7 @@ RSpec.describe Integrations::Jira do
end
context 'when resource is a merge request' do
- let(:resource) { create(:merge_request) }
+ let_it_be(:resource) { create(:merge_request, source_project: project) }
let(:commit_id) { resource.diff_head_sha }
it_behaves_like 'close_issue'
@@ -1084,7 +1071,7 @@ RSpec.describe Integrations::Jira do
end
it 'removes trailing slashes from url' do
- integration = described_class.new(url: 'http://jira.test.com/path/')
+ integration = described_class.new(url: 'http://jira.test.com/path/', project: project)
expect(integration.url).to eq('http://jira.test.com/path')
end
@@ -1105,7 +1092,7 @@ RSpec.describe Integrations::Jira do
end
context 'generating external URLs' do
- let(:integration) { described_class.new(url: 'http://jira.test.com/path/') }
+ let(:integration) { described_class.new(url: 'http://jira.test.com/path/', project: project) }
describe '#web_url' do
it 'handles paths, slashes, and query string' do
diff --git a/spec/models/integrations/mattermost_slash_commands_spec.rb b/spec/models/integrations/mattermost_slash_commands_spec.rb
index b6abe00469b..070adb9ba93 100644
--- a/spec/models/integrations/mattermost_slash_commands_spec.rb
+++ b/spec/models/integrations/mattermost_slash_commands_spec.rb
@@ -6,9 +6,9 @@ RSpec.describe Integrations::MattermostSlashCommands do
it_behaves_like Integrations::BaseSlashCommands
describe 'Mattermost API' do
- let(:project) { create(:project) }
+ let_it_be_with_reload(:project) { create(:project) }
let(:integration) { project.build_mattermost_slash_commands_integration }
- let(:user) { create(:user) }
+ let(:user) { build_stubbed(:user) }
before do
session = ::Mattermost::Session.new(nil)
diff --git a/spec/models/integrations/microsoft_teams_spec.rb b/spec/models/integrations/microsoft_teams_spec.rb
index b6de2bb7176..c61cc732372 100644
--- a/spec/models/integrations/microsoft_teams_spec.rb
+++ b/spec/models/integrations/microsoft_teams_spec.rb
@@ -17,35 +17,8 @@ RSpec.describe Integrations::MicrosoftTeams do
let(:chat_integration) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
- describe 'Validations' do
- context 'when integration is active' do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of(:webhook) }
-
- it_behaves_like 'issue tracker integration URL attribute', :webhook
- end
-
- context 'when integration is inactive' do
- before do
- subject.active = false
- end
-
- it { is_expected.not_to validate_presence_of(:webhook) }
- end
- end
-
- describe '.supported_events' do
- it 'does not support deployment_events' do
- expect(described_class.supported_events).not_to include('deployment')
- end
- end
-
describe "#execute" do
- let(:user) { create(:user) }
-
+ let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository, :wiki_repo) }
before do
@@ -145,8 +118,8 @@ RSpec.describe Integrations::MicrosoftTeams do
end
describe "Note events" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, creator: user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, creator: user) }
before do
allow(chat_integration).to receive_messages(
@@ -221,8 +194,7 @@ RSpec.describe Integrations::MicrosoftTeams do
end
describe 'Pipeline events' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/models/integrations/mock_ci_spec.rb b/spec/models/integrations/mock_ci_spec.rb
index d29c63b3a97..83954812bfe 100644
--- a/spec/models/integrations/mock_ci_spec.rb
+++ b/spec/models/integrations/mock_ci_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe Integrations::MockCi do
subject(:integration) { described_class.new(project: project, mock_service_url: generate(:url)) }
+ it_behaves_like Integrations::BaseCi
+
include_context Integrations::EnableSslVerification
describe '#commit_status' do
diff --git a/spec/models/integrations/packagist_spec.rb b/spec/models/integrations/packagist_spec.rb
index e078debd126..e00de0f7418 100644
--- a/spec/models/integrations/packagist_spec.rb
+++ b/spec/models/integrations/packagist_spec.rb
@@ -3,50 +3,76 @@
require 'spec_helper'
RSpec.describe Integrations::Packagist do
- let(:packagist_params) do
- {
- active: true,
- project: project,
- properties: {
- username: packagist_username,
- token: packagist_token,
- server: packagist_server
- }
- }
- end
-
- let(:packagist_hook_url) do
- "#{packagist_server}/api/update-package?username=#{packagist_username}&apiToken=#{packagist_token}"
- end
-
- let(:packagist_token) { 'verySecret' }
- let(:packagist_username) { 'theUser' }
- let(:packagist_server) { 'https://packagist.example.com' }
- let(:project) { create(:project) }
-
it_behaves_like Integrations::HasWebHook do
- let(:integration) { described_class.new(packagist_params) }
- let(:hook_url) { "#{packagist_server}/api/update-package?username={username}&apiToken={token}" }
+ let_it_be(:project) { create(:project) }
+
+ let(:integration) { build(:packagist_integration, project: project) }
+ let(:hook_url) { "#{integration.server}/api/update-package?username={username}&apiToken={token}" }
end
it_behaves_like Integrations::ResetSecretFields do
- let(:integration) { described_class.new(packagist_params) }
+ let(:integration) { build(:packagist_integration) }
end
describe '#execute' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
- let(:packagist_integration) { described_class.create!(packagist_params) }
+ let(:project) { build(:project) }
+ let(:integration) { build(:packagist_integration, project: project) }
+
+ let(:packagist_hook_url) do
+ "#{integration.server}/api/update-package?username=#{integration.username}&apiToken=#{integration.token}"
+ end
before do
stub_request(:post, packagist_hook_url)
end
it 'calls Packagist API' do
- packagist_integration.execute(push_sample_data)
+ user = create(:user)
+ push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+ integration.execute(push_sample_data)
expect(a_request(:post, packagist_hook_url)).to have_been_made.once
end
end
+
+ describe '#test' do
+ let(:integration) { build(:packagist_integration) }
+ let(:test_data) { { foo: 'bar' } }
+
+ subject(:result) { integration.test(test_data) }
+
+ context 'when test request executes without errors' do
+ before do
+ allow(integration).to receive(:execute).with(test_data).and_return(
+ ServiceResponse.success(message: 'success message', payload: { http_status: http_status })
+ )
+ end
+
+ context 'when response is a 200' do
+ let(:http_status) { 200 }
+
+ it 'return failure result' do
+ is_expected.to eq(success: false, result: 'success message')
+ end
+ end
+
+ context 'when response is a 202' do
+ let(:http_status) { 202 }
+
+ it 'return success result' do
+ is_expected.to eq(success: true, result: 'success message')
+ end
+ end
+ end
+
+ context 'when test request executes with errors' do
+ before do
+ allow(integration).to receive(:execute).with(test_data).and_raise(StandardError, 'error message')
+ end
+
+ it 'return failure result' do
+ is_expected.to eq(success: false, result: 'error message')
+ end
+ end
+ end
end
diff --git a/spec/models/integrations/pipelines_email_spec.rb b/spec/models/integrations/pipelines_email_spec.rb
index d70f104b965..37a3849a768 100644
--- a/spec/models/integrations/pipelines_email_spec.rb
+++ b/spec/models/integrations/pipelines_email_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do
)
end
- let(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:project) { create(:project, :repository) }
let(:recipients) { 'test@gitlab.com' }
let(:receivers) { [recipients] }
diff --git a/spec/models/integrations/prometheus_spec.rb b/spec/models/integrations/prometheus_spec.rb
index 3971511872b..3c3850854b3 100644
--- a/spec/models/integrations/prometheus_spec.rb
+++ b/spec/models/integrations/prometheus_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
let(:integration) { project.prometheus_integration }
+ it_behaves_like Integrations::BaseMonitoring
+
context 'redirects' do
it 'does not follow redirects' do
redirect_to = 'https://redirected.example.com'
@@ -217,7 +219,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
expect(integration.prometheus_client).to be_nil
end
- context 'with self monitoring project and internal Prometheus URL' do
+ context 'with self-monitoring project and internal Prometheus URL' do
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: false)
stub_application_setting(self_monitoring_project_id: project.id)
@@ -308,7 +310,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
end
context 'cluster belongs to project' do
- let(:cluster) { create(:cluster, projects: [project]) }
+ let_it_be(:cluster) { create(:cluster, projects: [project]) }
it 'returns true' do
expect(integration.prometheus_available?).to be(true)
@@ -319,7 +321,7 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching,
let_it_be(:group) { create(:group) }
let(:project) { create(:project, :with_prometheus_integration, group: group) }
- let(:cluster) { create(:cluster_for_group, groups: [group]) }
+ let_it_be(:cluster) { create(:cluster_for_group, groups: [group]) }
it 'returns true' do
expect(integration.prometheus_available?).to be(true)
diff --git a/spec/models/integrations/pushover_spec.rb b/spec/models/integrations/pushover_spec.rb
index 716a00c5bcf..8286fd20669 100644
--- a/spec/models/integrations/pushover_spec.rb
+++ b/spec/models/integrations/pushover_spec.rb
@@ -29,8 +29,8 @@ RSpec.describe Integrations::Pushover do
describe 'Execute' do
let(:pushover) { described_class.new }
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let(:user) { build_stubbed(:user) }
+ let(:project) { build_stubbed(:project, :repository) }
let(:sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
diff --git a/spec/models/integrations/shimo_spec.rb b/spec/models/integrations/shimo_spec.rb
index 41f3f3c0c16..be626012ab2 100644
--- a/spec/models/integrations/shimo_spec.rb
+++ b/spec/models/integrations/shimo_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ::Integrations::Shimo do
describe '#fields' do
- let(:shimo_integration) { create(:shimo_integration) }
+ let(:shimo_integration) { build(:shimo_integration) }
it 'returns custom fields' do
expect(shimo_integration.fields.pluck(:name)).to eq(%w[external_wiki_url])
@@ -12,7 +12,7 @@ RSpec.describe ::Integrations::Shimo do
end
describe '#create' do
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
let(:external_wiki_url) { 'https://shimo.example.com/desktop' }
let(:params) { { active: true, project: project, external_wiki_url: external_wiki_url } }
@@ -40,7 +40,7 @@ RSpec.describe ::Integrations::Shimo do
end
describe 'Caching has_shimo on project_settings' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
subject { project.project_setting.has_shimo? }
diff --git a/spec/models/integrations/slack_slash_commands_spec.rb b/spec/models/integrations/slack_slash_commands_spec.rb
index ff89d2c6a40..22cbaa777cd 100644
--- a/spec/models/integrations/slack_slash_commands_spec.rb
+++ b/spec/models/integrations/slack_slash_commands_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Integrations::SlackSlashCommands do
describe '#trigger' do
context 'when an auth url is generated' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
let(:params) do
{
team_domain: 'http://domain.tld',
diff --git a/spec/models/integrations/slack_spec.rb b/spec/models/integrations/slack_spec.rb
index ed282f1d39d..a12bc7f4831 100644
--- a/spec/models/integrations/slack_spec.rb
+++ b/spec/models/integrations/slack_spec.rb
@@ -3,142 +3,6 @@
require 'spec_helper'
RSpec.describe Integrations::Slack do
- it_behaves_like Integrations::SlackMattermostNotifier, "Slack"
-
- describe '#execute' do
- let(:slack_integration) { create(:integrations_slack, branches_to_be_notified: 'all', project_id: project.id) }
- let(:project) { create_default(:project, :repository, :wiki_repo) }
-
- before do
- stub_request(:post, slack_integration.webhook)
- end
-
- it 'uses only known events', :aggregate_failures do
- described_class::SUPPORTED_EVENTS_FOR_USAGE_LOG.each do |action|
- expect(Gitlab::UsageDataCounters::HLLRedisCounter.known_event?("i_ecosystem_slack_service_#{action}_notification")).to be true
- end
- end
-
- context 'hook data includes a user object' do
- let_it_be(:user) { create_default(:user) }
-
- shared_examples 'increases the usage data counter' do |event_name|
- subject(:execute) { slack_integration.execute(data) }
-
- it 'increases the usage data counter' do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(event_name, values: user.id).and_call_original
-
- execute
- end
-
- it_behaves_like 'Snowplow event tracking' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
- let(:category) { 'Integrations::Slack' }
- let(:action) { 'perform_integrations_action' }
- let(:namespace) { project.namespace }
- let(:label) { 'redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly' }
- let(:property) { event_name }
- end
- end
-
- context 'event is not supported for usage log' do
- let_it_be(:pipeline) { create(:ci_pipeline) }
-
- let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
-
- it 'does not increase the usage data counter' do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with('i_ecosystem_slack_service_pipeline_notification', values: user.id)
-
- slack_integration.execute(data)
- end
- end
-
- context 'issue notification' do
- let_it_be(:issue) { create(:issue) }
-
- let(:data) { issue.to_hook_data(user) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_issue_notification'
- end
-
- context 'push notification' do
- let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_push_notification'
- end
-
- context 'deployment notification' do
- let_it_be(:deployment) { create(:deployment, user: user) }
-
- let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification'
- end
-
- context 'wiki_page notification' do
- let(: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') }
-
- before do
- # Skip this method that is not relevant to this test to prevent having
- # to update project which is frozen
- allow(project.wiki).to receive(:after_wiki_activity)
- end
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_wiki_page_notification'
- end
-
- context 'merge_request notification' do
- let_it_be(:merge_request) { create(:merge_request) }
-
- let(:data) { merge_request.to_hook_data(user) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_merge_request_notification'
- end
-
- context 'note notification' do
- let_it_be(:issue_note) { create(:note_on_issue, note: 'issue note') }
-
- let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_note_notification'
- end
-
- context 'tag_push notification' do
- let(:oldrev) { Gitlab::Git::BLANK_SHA }
- let(:newrev) { '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b' } # gitlab-test: git rev-parse refs/tags/v1.1.0
- let(:ref) { 'refs/tags/v1.1.0' }
- let(:data) { Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_tag_push_notification'
- end
-
- context 'confidential note notification' do
- let_it_be(:confidential_issue_note) { create(:note_on_issue, note: 'issue note', confidential: true) }
-
- let(:data) { Gitlab::DataBuilder::Note.build(confidential_issue_note, user) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_note_notification'
- end
-
- context 'confidential issue notification' do
- let_it_be(:issue) { create(:issue, confidential: true) }
-
- let(:data) { issue.to_hook_data(user) }
-
- it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_issue_notification'
- end
- end
-
- context 'hook data does not include a user' do
- let(:data) { Gitlab::DataBuilder::Pipeline.build(create(:ci_pipeline)) }
-
- it 'does not increase the usage data counter' do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
-
- slack_integration.execute(data)
- end
- end
- end
+ it_behaves_like Integrations::SlackMattermostNotifier, 'Slack'
+ it_behaves_like Integrations::BaseSlackNotification, factory: :integrations_slack
end
diff --git a/spec/models/integrations/teamcity_spec.rb b/spec/models/integrations/teamcity_spec.rb
index da559264c1e..e32088a2f79 100644
--- a/spec/models/integrations/teamcity_spec.rb
+++ b/spec/models/integrations/teamcity_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
let(:teamcity_url) { 'https://gitlab.teamcity.com' }
let(:teamcity_full_url) { 'https://gitlab.teamcity.com/httpAuth/app/rest/builds/branch:unspecified:any,revision:123' }
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
subject(:integration) do
described_class.create!(
@@ -22,6 +22,10 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
)
end
+ it_behaves_like Integrations::BaseCi
+
+ it_behaves_like Integrations::ResetSecretFields
+
include_context Integrations::EnableSslVerification do
describe '#enable_ssl_verification' do
before do
@@ -120,50 +124,6 @@ RSpec.describe Integrations::Teamcity, :use_clean_rails_memory_store_caching do
end
end
- describe 'Callbacks' do
- let(:teamcity_integration) { integration }
-
- describe 'before_validation :reset_password' do
- context 'when a password was previously set' do
- it 'resets password if url changed' do
- teamcity_integration.teamcity_url = 'http://gitlab1.com'
- teamcity_integration.valid?
-
- expect(teamcity_integration.password).to be_nil
- end
-
- it 'does not reset password if username changed' do
- teamcity_integration.username = 'some_name'
- teamcity_integration.valid?
-
- expect(teamcity_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
- teamcity_integration.teamcity_url = 'http://gitlab_edited.com'
- teamcity_integration.password = 'password'
- teamcity_integration.valid?
-
- expect(teamcity_integration.password).to eq('password')
- expect(teamcity_integration.teamcity_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
- teamcity_integration.password = nil
-
- teamcity_integration.teamcity_url = 'http://gitlab_edited.com'
- teamcity_integration.password = 'password'
- teamcity_integration.save!
-
- expect(teamcity_integration.reload).to have_attributes(
- teamcity_url: 'http://gitlab_edited.com',
- password: 'password'
- )
- end
- end
- end
-
describe '#build_page' do
it 'returns the contents of the reactive cache' do
stub_reactive_cache(integration, { build_page: 'foo' }, 'sha', 'ref')
diff --git a/spec/models/integrations/zentao_spec.rb b/spec/models/integrations/zentao_spec.rb
index 1a32453819d..2fa4df0e900 100644
--- a/spec/models/integrations/zentao_spec.rb
+++ b/spec/models/integrations/zentao_spec.rb
@@ -7,15 +7,14 @@ RSpec.describe Integrations::Zentao do
let(:api_url) { 'https://jihudemo.zentao.net' }
let(:api_token) { 'ZENTAO_TOKEN' }
let(:zentao_product_xid) { '3' }
- let(:zentao_integration) { create(:zentao_integration) }
+ let(:zentao_integration) { build(:zentao_integration, project: project) }
+ let_it_be(:project) { create(:project, :repository) }
it_behaves_like Integrations::ResetSecretFields do
let(:integration) { zentao_integration }
end
describe 'set_default_data' do
- let(:project) { create(:project, :repository) }
-
context 'when gitlab.yml was initialized' do
it 'is prepopulated with the settings' do
settings = {
@@ -35,7 +34,6 @@ RSpec.describe Integrations::Zentao do
end
describe '#create' do
- let(:project) { create(:project, :repository) }
let(:params) do
{
project: project,
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e7b2212ebff..aea8bdaf343 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -25,6 +25,7 @@ RSpec.describe Issue do
it { is_expected.to have_many(:design_versions) }
it { is_expected.to have_one(:sentry_issue) }
it { is_expected.to have_one(:alert_management_alert) }
+ it { is_expected.to have_many(:alert_management_alerts) }
it { is_expected.to have_many(:resource_milestone_events) }
it { is_expected.to have_many(:resource_state_events) }
it { is_expected.to have_and_belong_to_many(:prometheus_alert_events) }
@@ -654,7 +655,7 @@ RSpec.describe Issue do
let_it_be(:authorized_issue_a) { create(:issue, project: authorized_project) }
let_it_be(:authorized_issue_b) { create(:issue, project: authorized_project) }
let_it_be(:authorized_issue_c) { create(:issue, project: authorized_project2) }
- let_it_be(:authorized_incident_a) { create(:incident, project: authorized_project ) }
+ let_it_be(:authorized_incident_a) { create(:incident, project: authorized_project) }
let_it_be(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
@@ -863,7 +864,7 @@ RSpec.describe Issue do
describe '.to_branch_name' do
it 'parameterizes arguments and joins with dashes' do
- expect(described_class.to_branch_name(123, 'foo bar', '!@#$%', 'f!o@o#b$a%r^')).to eq('123-foo-bar-f-o-o-b-a-r')
+ expect(described_class.to_branch_name(123, 'foo bar!@#$%f!o@o#b$a%r^')).to eq('123-foo-bar-f-o-o-b-a-r')
end
it 'preserves the case in the first argument' do
@@ -871,7 +872,7 @@ RSpec.describe Issue do
end
it 'truncates branch name to at most 100 characters' do
- expect(described_class.to_branch_name('a' * 101)).to eq('a' * 100)
+ expect(described_class.to_branch_name('a' * 101, 'a')).to eq('a' * 100)
end
it 'truncates dangling parts of the branch name' do
@@ -883,6 +884,13 @@ RSpec.describe Issue do
# 100 characters would've got us "999-lorem...lacus-custom-fri".
expect(branch_name).to eq('999-lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-mauris-sit-amet-ipsum-id-lacus-custom')
end
+
+ it 'takes issue branch template into account' do
+ project = create(:project)
+ project.project_setting.update!(issue_branch_template: 'feature-%{id}-%{title}')
+
+ expect(described_class.to_branch_name(123, 'issue title', project: project)).to eq('feature-123-issue-title')
+ end
end
describe '#to_branch_name' do
@@ -1785,4 +1793,22 @@ RSpec.describe Issue do
end
end
end
+
+ describe '#full_search' do
+ context 'when searching non-english terms' do
+ [
+ 'abc 中文語',
+ '中文語cn',
+ '中文語',
+ 'Привет'
+ ].each do |term|
+ it 'adds extra where clause to match partial index' do
+ expect(described_class.full_search(term).to_sql).to include(
+ "AND (issues.title NOT SIMILAR TO '[\\u0000-\\u02FF\\u1E00-\\u1EFF\\u2070-\\u218F]*' " \
+ "OR issues.description NOT SIMILAR TO '[\\u0000-\\u02FF\\u1E00-\\u1EFF\\u2070-\\u218F]*')"
+ )
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb
index e57d3e78a4e..09a4a7a488c 100644
--- a/spec/models/jira_connect_installation_spec.rb
+++ b/spec/models/jira_connect_installation_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe JiraConnectInstallation do
end
describe '#oauth_authorization_url' do
- let_it_be(:installation) { create(:jira_connect_installation) }
+ let(:installation) { build(:jira_connect_installation) }
subject { installation.oauth_authorization_url }
@@ -82,7 +82,7 @@ RSpec.describe JiraConnectInstallation do
it { is_expected.to eq('http://test.host') }
context 'with instance_url' do
- let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://gitlab.example.com') }
+ let(:installation) { build(:jira_connect_installation, instance_url: 'https://gitlab.example.com') }
it { is_expected.to eq('https://gitlab.example.com') }
@@ -97,42 +97,42 @@ RSpec.describe JiraConnectInstallation do
end
describe 'audience_url' do
- let_it_be(:installation) { create(:jira_connect_installation) }
+ let(:installation) { build(:jira_connect_installation) }
subject(:audience) { installation.audience_url }
it { is_expected.to eq(nil) }
context 'when proxy installation' do
- let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://example.com') }
+ let(:installation) { build(:jira_connect_installation, instance_url: 'https://example.com') }
it { is_expected.to eq('https://example.com/-/jira_connect') }
end
end
describe 'audience_installed_event_url' do
- let_it_be(:installation) { create(:jira_connect_installation) }
+ let(:installation) { build(:jira_connect_installation) }
subject(:audience) { installation.audience_installed_event_url }
it { is_expected.to eq(nil) }
context 'when proxy installation' do
- let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://example.com') }
+ let(:installation) { build(:jira_connect_installation, instance_url: 'https://example.com') }
it { is_expected.to eq('https://example.com/-/jira_connect/events/installed') }
end
end
describe 'proxy?' do
- let_it_be(:installation) { create(:jira_connect_installation) }
+ let(:installation) { build(:jira_connect_installation) }
subject { installation.proxy? }
it { is_expected.to eq(false) }
context 'when instance_url is present' do
- let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://example.com') }
+ let(:installation) { build(:jira_connect_installation, instance_url: 'https://example.com') }
it { is_expected.to eq(true) }
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 04df8ecc882..2ecd10cccc6 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -7,6 +7,12 @@ RSpec.describe Member do
using RSpec::Parameterized::TableSyntax
+ describe 'default values' do
+ subject(:member) { build(:project_member) }
+
+ it { expect(member.notification_level).to eq(NotificationSetting.levels[:global]) }
+ end
+
describe 'Associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:member_namespace) }
@@ -213,7 +219,7 @@ RSpec.describe Member do
describe 'Scopes & finders' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:group) { create(:group) }
- let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval ) }
+ let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval) }
let_it_be(:blocked_pending_approval_project_member) { create(:project_member, :invited, :developer, project: project, invite_email: blocked_pending_approval_user.email) }
let_it_be(:awaiting_group_member) { create(:group_member, :awaiting, group: group) }
let_it_be(:awaiting_project_member) { create(:project_member, :awaiting, project: project) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 363830d21dd..77bc6d9753f 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -3,6 +3,12 @@
require 'spec_helper'
RSpec.describe GroupMember do
+ describe 'default values' do
+ subject(:goup_member) { build(:group_member) }
+
+ it { expect(goup_member.source_type).to eq(described_class::SOURCE_TYPE) }
+ end
+
context 'scopes' do
let_it_be(:user_1) { create(:user) }
let_it_be(:user_2) { create(:user) }
diff --git a/spec/models/members/last_group_owner_assigner_spec.rb b/spec/models/members/last_group_owner_assigner_spec.rb
index 429cf4190cf..a0a829221de 100644
--- a/spec/models/members/last_group_owner_assigner_spec.rb
+++ b/spec/models/members/last_group_owner_assigner_spec.rb
@@ -7,14 +7,10 @@ RSpec.describe LastGroupOwnerAssigner do
let_it_be(:user, reload: true) { create(:user) }
let_it_be(:group) { create(:group) }
- let(:group_member) { user.members.last }
+ let!(:group_member) { group.add_owner(user) }
subject(:assigner) { described_class.new(group, [group_member]) }
- before do
- group.add_owner(user)
- end
-
it "avoids extra database queries utilizing memoization", :aggregate_failures do
control = ActiveRecord::QueryRecorder.new { assigner.execute }
count_queries = control.occurrences_by_line_method.first[1][:occurrences].find_all { |i| i.include?('SELECT COUNT') }
@@ -56,6 +52,40 @@ RSpec.describe LastGroupOwnerAssigner do
.from(nil).to(false)
end
end
+
+ context 'with owners from a parent' do
+ context 'when top-level group' do
+ context 'with group sharing' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ before do
+ create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
+ create(:group_member, :owner, group: subgroup)
+ end
+
+ specify do
+ expect { assigner.execute }.to change(group_member, :last_owner)
+ .from(nil).to(true)
+ .and change(group_member, :last_blocked_owner)
+ .from(nil).to(false)
+ end
+ end
+ end
+
+ context 'when subgroup' do
+ let!(:subgroup) { create(:group, parent: group) }
+ let!(:group_member_2) { subgroup.add_owner(user) }
+
+ subject(:assigner) { described_class.new(subgroup, [group_member_2]) }
+
+ specify do
+ expect { assigner.execute }.to change(group_member_2, :last_owner)
+ .from(nil).to(false)
+ .and change(group_member_2, :last_blocked_owner)
+ .from(nil).to(false)
+ end
+ end
+ end
end
context "when there are blocked owners" do
@@ -93,6 +123,54 @@ RSpec.describe LastGroupOwnerAssigner do
.from(nil).to(false)
end
end
+
+ context 'with owners from a parent' do
+ context 'when top-level group' do
+ context 'with group sharing' do
+ let!(:subgroup) { create(:group, parent: group) }
+
+ before do
+ create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
+ create(:group_member, :owner, group: subgroup)
+ end
+
+ specify do
+ expect { assigner.execute }.to change(group_member, :last_owner)
+ .from(nil).to(false)
+ .and change(group_member, :last_blocked_owner)
+ .from(nil).to(true)
+ end
+ end
+ end
+
+ context 'when subgroup' do
+ let!(:subgroup) { create(:group, :nested) }
+
+ let!(:group_member) { subgroup.add_owner(user) }
+
+ subject(:assigner) { described_class.new(subgroup, [group_member]) }
+
+ specify do
+ expect { assigner.execute }.to change(group_member, :last_owner)
+ .from(nil).to(false)
+ .and change(group_member, :last_blocked_owner)
+ .from(nil).to(true)
+ end
+
+ context 'with two owners' do
+ before do
+ create(:group_member, :owner, group: subgroup.parent)
+ end
+
+ specify do
+ expect { assigner.execute }.to change(group_member, :last_owner)
+ .from(nil).to(false)
+ .and change(group_member, :last_blocked_owner)
+ .from(nil).to(false)
+ end
+ end
+ end
+ end
end
context 'when there are bot members' do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index ad6f3ca5428..e56c6b38992 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -13,6 +13,10 @@ RSpec.describe ProjectMember do
it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
+ describe 'default values' do
+ it { expect(described_class.new.source_type).to eq('Project') }
+ end
+
describe 'delegations' do
it { is_expected.to delegate_method(:namespace_id).to(:project) }
end
diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb
index f107a56c1b6..7e127caa649 100644
--- a/spec/models/merge_request_diff_file_spec.rb
+++ b/spec/models/merge_request_diff_file_spec.rb
@@ -203,16 +203,6 @@ RSpec.describe MergeRequestDiffFile do
end
end
- context 'when externally_stored_diffs_caching_export feature flag is disabled' do
- it 'calls #diff' do
- stub_feature_flags(externally_stored_diffs_caching_export: false)
-
- expect(file).to receive(:diff)
-
- file.utf8_diff
- end
- end
-
context 'when diff is not stored externally' do
it 'calls #diff' do
expect(file).to receive(:diff)
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e9e8bd9bfea..22fed716897 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1097,6 +1097,19 @@ RSpec.describe MergeRequestDiff do
it 'returns a non-empty CommitCollection' do
expect(mr.merge_request_diff.commits.commits.size).to be > 0
end
+
+ context 'with a page' do
+ it 'returns a limited number of commits for page' do
+ expect(mr.merge_request_diff.commits(limit: 1, page: 1).map(&:sha)).to eq(
+ %w[
+ b83d6e391c22777fca1ed3012fce84f633d7fed0
+ ])
+ expect(mr.merge_request_diff.commits(limit: 1, page: 2).map(&:sha)).to eq(
+ %w[
+ 498214de67004b1da3d820901307bed2a68a8ef6
+ ])
+ end
+ end
end
describe '.latest_diff_for_merge_requests' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 32518b867cb..cf4f58f558c 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -232,10 +232,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
context 'for branch' do
- before do
- stub_feature_flags(stricter_mr_branch_name: false)
- end
-
where(:branch_name, :valid) do
'foo' | true
'foo:bar' | false
@@ -278,6 +274,34 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
describe 'callbacks' do
+ describe '#ensure_merge_request_diff' do
+ let(:merge_request) { build(:merge_request) }
+
+ context 'when async_merge_request_diff_creation is true' do
+ before do
+ merge_request.skip_ensure_merge_request_diff = true
+ end
+
+ it 'does not create a merge_request_diff after create' do
+ merge_request.save!
+
+ expect(merge_request.merge_request_diff).to be_empty
+ end
+ end
+
+ context 'when async_merge_request_diff_creation is false' do
+ before do
+ merge_request.skip_ensure_merge_request_diff = false
+ end
+
+ it 'creates merge_request_diff after create' do
+ merge_request.save!
+
+ expect(merge_request.merge_request_diff).not_to be_empty
+ end
+ end
+ end
+
describe '#ensure_merge_request_metrics' do
let(:merge_request) { create(:merge_request) }
@@ -3228,14 +3252,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#mergeable_state?' do
it_behaves_like 'for mergeable_state'
-
- context 'when merge state caching is off' do
- before do
- stub_feature_flags(mergeability_caching: false)
- end
-
- it_behaves_like 'for mergeable_state'
- end
end
describe "#public_merge_status" do
@@ -4213,14 +4229,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
transition!
end
-
- context 'when trigger_mr_subscription_on_merge_status_change is disabled' do
- before do
- stub_feature_flags(trigger_mr_subscription_on_merge_status_change: false)
- end
-
- it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription'
- end
end
shared_examples 'for an invalid state transition' do
@@ -4984,6 +4992,19 @@ RSpec.describe MergeRequest, factory_default: :keep do
expect(subject.commits.size).to eq(29)
end
end
+
+ context 'with a page' do
+ it 'returns a limited number of commits for page' do
+ expect(subject.commits(limit: 1, page: 1).map(&:sha)).to eq(
+ %w[
+ b83d6e391c22777fca1ed3012fce84f633d7fed0
+ ])
+ expect(subject.commits(limit: 1, page: 2).map(&:sha)).to eq(
+ %w[
+ 498214de67004b1da3d820901307bed2a68a8ef6
+ ])
+ end
+ end
end
context 'new merge request' do
@@ -5114,17 +5135,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
it 'returns false' do
- expect(merge_request.diffable_merge_ref?).to eq(true)
- end
-
- context 'display_merge_conflicts_in_diff is disabled' do
- before do
- stub_feature_flags(display_merge_conflicts_in_diff: false)
- end
-
- it 'returns false' do
- expect(merge_request.diffable_merge_ref?).to eq(false)
- end
+ expect(merge_request.diffable_merge_ref?).to eq(false)
end
end
end
diff --git a/spec/models/metrics/dashboard/annotation_spec.rb b/spec/models/metrics/dashboard/annotation_spec.rb
index 4b7492016f3..9b8601e4052 100644
--- a/spec/models/metrics/dashboard/annotation_spec.rb
+++ b/spec/models/metrics/dashboard/annotation_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe Metrics::Dashboard::Annotation do
end
context 'annotation with shared ownership' do
- subject { build(:metrics_dashboard_annotation, :with_cluster, environment: build(:environment) ) }
+ subject { build(:metrics_dashboard_annotation, :with_cluster, environment: build(:environment)) }
it 'reports error about both shared ownership' do
subject.valid?
diff --git a/spec/models/ml/candidate_metric_spec.rb b/spec/models/ml/candidate_metric_spec.rb
index 5ee6030fb8e..9f9a6e8e3ba 100644
--- a/spec/models/ml/candidate_metric_spec.rb
+++ b/spec/models/ml/candidate_metric_spec.rb
@@ -6,4 +6,17 @@ RSpec.describe Ml::CandidateMetric do
describe 'associations' do
it { is_expected.to belong_to(:candidate) }
end
+
+ describe 'scope :latest' do
+ let_it_be(:candidate) { create(:ml_candidates) }
+ let!(:metric1) { create(:ml_candidate_metrics, candidate: candidate) }
+ let!(:metric2) { create(:ml_candidate_metrics, candidate: candidate ) }
+ let!(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate) }
+
+ subject { described_class.latest }
+
+ it 'fetches only the last metric for the name' do
+ expect(subject).to match_array([metric2, metric3] )
+ end
+ end
end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index 3bf1e80a152..b35496363fe 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Ml::Candidate, factory_default: :keep do
- let_it_be(:candidate) { create_default(:ml_candidates, :with_metrics_and_params) }
+ let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) }
describe 'associations' do
it { is_expected.to belong_to(:experiment) }
@@ -12,10 +12,14 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
it { is_expected.to have_many(:metrics) }
end
- describe '#new' do
- it 'iid is not null' do
- expect(candidate.iid).not_to be_nil
- end
+ describe '.artifact_root' do
+ subject { candidate.artifact_root }
+
+ it { is_expected.to eq("/ml_candidate_#{candidate.iid}/-/") }
+ end
+
+ describe 'default values' do
+ it { expect(described_class.new.iid).to be_present }
end
describe '#by_project_id_and_iid' do
@@ -40,4 +44,26 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
it { is_expected.to be_nil }
end
end
+
+ describe "#latest_metrics" do
+ let_it_be(:candidate2) { create(:ml_candidates, experiment: candidate.experiment) }
+ let!(:metric1) { create(:ml_candidate_metrics, candidate: candidate2) }
+ let!(:metric2) { create(:ml_candidate_metrics, candidate: candidate2 ) }
+ let!(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate2) }
+
+ subject { candidate2.latest_metrics }
+
+ it 'fetches only the last metric for the name' do
+ expect(subject).to match_array([metric2, metric3] )
+ end
+ end
+
+ describe "#including_metrics_and_params" do
+ subject { described_class.including_metrics_and_params.find_by(id: candidate.id) }
+
+ it 'loads latest metrics and params', :aggregate_failures do
+ expect(subject.association_cached?(:latest_metrics)).to be(true)
+ expect(subject.association_cached?(:params)).to be(true)
+ end
+ end
end
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index a4446bfedd1..17c49e13c85 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe NamespaceSetting, type: :model do
describe '#prevent_sharing_groups_outside_hierarchy' do
let(:settings) { create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true) }
- let!(:group) { create(:group, parent: parent, namespace_settings: settings ) }
+ let!(:group) { create(:group, parent: parent, namespace_settings: settings) }
subject(:group_sharing_setting) { settings.prevent_sharing_groups_outside_hierarchy }
@@ -133,7 +133,7 @@ RSpec.describe NamespaceSetting, type: :model do
context 'when :show_diff_preview_in_email is false' do
it 'returns false' do
settings = create(:namespace_settings, show_diff_preview_in_email: false)
- group = create(:group, namespace_settings: settings )
+ group = create(:group, namespace_settings: settings)
expect(group.show_diff_preview_in_email?).to be_falsey
end
@@ -142,7 +142,7 @@ RSpec.describe NamespaceSetting, type: :model do
context 'when :show_diff_preview_in_email is true' do
it 'returns true' do
settings = create(:namespace_settings, show_diff_preview_in_email: true)
- group = create(:group, namespace_settings: settings )
+ group = create(:group, namespace_settings: settings)
expect(group.show_diff_preview_in_email?).to be_truthy
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index c6d028af22d..0516d446945 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -1623,7 +1623,7 @@ RSpec.describe Namespace do
describe '#share_with_group_lock with subgroups' do
context 'when creating a subgroup' do
- let(:subgroup) { create(:group, parent: root_group ) }
+ let(:subgroup) { create(:group, parent: root_group) }
context 'under a parent with "Share with group lock" enabled' do
let(:root_group) { create(:group, share_with_group_lock: true) }
@@ -1644,7 +1644,7 @@ RSpec.describe Namespace do
context 'when enabling the parent group "Share with group lock"' do
let(:root_group) { create(:group) }
- let!(:subgroup) { create(:group, parent: root_group ) }
+ let!(:subgroup) { create(:group, parent: root_group) }
it 'the subgroup "Share with group lock" becomes enabled' do
root_group.update!(share_with_group_lock: true)
@@ -1657,7 +1657,7 @@ RSpec.describe Namespace do
let(:root_group) { create(:group, share_with_group_lock: true) }
context 'and the subgroup "Share with group lock" is enabled' do
- let(:subgroup) { create(:group, parent: root_group, share_with_group_lock: true ) }
+ let(:subgroup) { create(:group, parent: root_group, share_with_group_lock: true) }
it 'the subgroup "Share with group lock" does not change' do
root_group.update!(share_with_group_lock: false)
@@ -1667,7 +1667,7 @@ RSpec.describe Namespace do
end
context 'but the subgroup "Share with group lock" is disabled' do
- let(:subgroup) { create(:group, parent: root_group ) }
+ let(:subgroup) { create(:group, parent: root_group) }
it 'the subgroup "Share with group lock" does not change' do
root_group.update!(share_with_group_lock: false)
@@ -1682,7 +1682,7 @@ RSpec.describe Namespace do
let(:root_group) { create(:group, share_with_group_lock: true) }
context 'when the subgroup "Share with group lock" is enabled' do
- let(:subgroup) { create(:group, share_with_group_lock: true ) }
+ let(:subgroup) { create(:group, share_with_group_lock: true) }
it 'the subgroup "Share with group lock" does not change' do
subgroup.parent = root_group
@@ -1708,7 +1708,7 @@ RSpec.describe Namespace do
let(:root_group) { create(:group) }
context 'when the subgroup "Share with group lock" is enabled' do
- let(:subgroup) { create(:group, share_with_group_lock: true ) }
+ let(:subgroup) { create(:group, share_with_group_lock: true) }
it 'the subgroup "Share with group lock" does not change' do
subgroup.parent = root_group
@@ -1826,7 +1826,7 @@ RSpec.describe Namespace do
group.update!(parent: parent)
- expect(group.full_path_before_last_save).to eq("#{group.path_before_last_save}")
+ expect(group.full_path_before_last_save).to eq(group.path_before_last_save.to_s)
end
end
@@ -2356,7 +2356,7 @@ RSpec.describe Namespace do
end
end
- describe 'storage_enforcement_date' do
+ describe 'storage_enforcement_date', :freeze_time do
let_it_be(:namespace) { create(:group) }
before do
@@ -2364,7 +2364,7 @@ RSpec.describe Namespace do
end
it 'returns correct date' do
- expect(namespace.storage_enforcement_date).to eql(Date.new(2022, 10, 19))
+ expect(namespace.storage_enforcement_date).to eql(3.months.from_now.to_date)
end
context 'when :storage_banner_bypass_date_check is enabled' do
@@ -2372,7 +2372,7 @@ RSpec.describe Namespace do
stub_feature_flags(namespace_storage_limit_bypass_date_check: true)
end
- it 'returns the current date', :freeze_time do
+ it 'returns the current date' do
expect(namespace.storage_enforcement_date).to eq(Date.current)
end
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index a393aace39c..16894bf28f1 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -6,10 +6,24 @@ RSpec.describe Network::Graph do
let(:project) { create(:project, :repository) }
let!(:note_on_commit) { create(:note_on_commit, project: project) }
- it '#initialize' do
- graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
+ describe '#initialize' do
+ let(:graph) do
+ described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
+ end
+
+ it 'has initialized' do
+ expect(graph).to be_a(described_class)
+ end
- expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
+ context 'when disable_network_graph_note_counts is disabled' do
+ before do
+ stub_feature_flags(disable_network_graph_notes_count: false)
+ end
+
+ it 'initializes the notes hash' do
+ expect(graph.notes).to eq({ note_on_commit.commit_id => 1 })
+ end
+ end
end
describe '#commits' do
@@ -19,7 +33,7 @@ RSpec.describe Network::Graph do
commits = graph.commits
expect(commits).not_to be_empty
- expect(commits).to all( be_kind_of(Network::Commit) )
+ expect(commits).to all(be_kind_of(Network::Commit))
end
it 'sorts commits by commit date (descending)' do
@@ -42,7 +56,7 @@ RSpec.describe Network::Graph do
parent_indexes = commit.parent_ids.map { |parent_id| commit_ids.find_index(parent_id) }.compact
# All parents of the current commit should appear after it
- expect(parent_indexes).to all( be > index )
+ expect(parent_indexes).to all(be > index)
end
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1b44da75c40..7c71080d63e 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -23,6 +23,10 @@ RSpec.describe Note do
it { is_expected.to include_module(Sortable) }
end
+ describe 'default values' do
+ it { expect(described_class.new).not_to be_system }
+ end
+
describe 'validation' do
it { is_expected.to validate_length_of(:note).is_at_most(1_000_000) }
it { is_expected.to validate_presence_of(:note) }
@@ -1619,70 +1623,6 @@ RSpec.describe Note do
end
end
- describe '#noteable_assignee_or_author?' do
- let(:user) { create(:user) }
- let(:noteable) { create(:issue) }
- let(:note) { create(:note, project: noteable.project, noteable: noteable) }
-
- subject { note.noteable_assignee_or_author?(user) }
-
- shared_examples 'assignee check' do
- context 'when the provided user is one of the assignees' do
- before do
- note.noteable.update!(assignees: [user, create(:user)])
- end
-
- it 'returns true' do
- expect(subject).to be_truthy
- end
- end
- end
-
- shared_examples 'author check' do
- context 'when the provided user is the author' do
- before do
- note.noteable.update!(author: user)
- end
-
- it 'returns true' do
- expect(subject).to be_truthy
- end
- end
-
- context 'when the provided user is neither author nor assignee' do
- it 'returns true' do
- expect(subject).to be_falsey
- end
- end
- end
-
- context 'when user is nil' do
- let(:user) { nil }
-
- it 'returns false' do
- expect(subject).to be_falsey
- end
- end
-
- context 'when noteable is an issue' do
- it_behaves_like 'author check'
- it_behaves_like 'assignee check'
- end
-
- context 'when noteable is a merge request' do
- let(:noteable) { create(:merge_request) }
-
- it_behaves_like 'author check'
- it_behaves_like 'assignee check'
- end
-
- context 'when noteable is a snippet' do
- let(:noteable) { create(:personal_snippet) }
-
- it_behaves_like 'author check'
- end
- end
-
describe 'banzai_render_context' do
let(:project) { build(:project_empty_repo) }
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index cc601fb30c2..730a9045d7f 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -5,6 +5,12 @@ require 'spec_helper'
RSpec.describe NotificationSetting do
it_behaves_like 'having unique enum values'
+ describe 'default values' do
+ subject(:notification_setting) { build(:notification_setting) }
+
+ it { expect(notification_setting.level).to eq('global') }
+ end
+
describe "Associations" do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:source) }
diff --git a/spec/models/oauth_access_token_spec.rb b/spec/models/oauth_access_token_spec.rb
index a4540ac95bc..92e1ae8ac60 100644
--- a/spec/models/oauth_access_token_spec.rb
+++ b/spec/models/oauth_access_token_spec.rb
@@ -46,42 +46,11 @@ RSpec.describe OauthAccessToken do
expect(described_class.by_token(plaintext_token)).to be_a(OauthAccessToken)
end
end
-
- context 'when hash_oauth_secrets is disabled' do
- let(:hashed_token) { create(:oauth_access_token, application_id: app_one.id) }
-
- before do
- hashed_token
- stub_feature_flags(hash_oauth_tokens: false)
- end
-
- it 'stores the token in plaintext' do
- expect(token.token).to eq(token.plaintext_token)
- end
-
- it 'finds a token by plaintext token' do
- expect(described_class.by_token(token.plaintext_token)).to be_a(OauthAccessToken)
- end
-
- it 'does not find a token that was previously stored as hashed' do
- expect(described_class.by_token(hashed_token.plaintext_token)).to be_nil
- end
- end
end
describe '.matching_token_for' do
it 'does not find existing tokens' do
expect(described_class.matching_token_for(app_one, token.resource_owner, token.scopes)).to be_nil
end
-
- context 'when hash oauth tokens is disabled' do
- before do
- stub_feature_flags(hash_oauth_tokens: false)
- end
-
- it 'finds an existing token' do
- expect(described_class.matching_token_for(app_one, token.resource_owner, token.scopes)).to be_present
- end
- end
end
end
diff --git a/spec/models/operations/feature_flag_spec.rb b/spec/models/operations/feature_flag_spec.rb
index 85a475f5c53..dd1ff95a16d 100644
--- a/spec/models/operations/feature_flag_spec.rb
+++ b/spec/models/operations/feature_flag_spec.rb
@@ -16,6 +16,11 @@ RSpec.describe Operations::FeatureFlag do
it { is_expected.to have_many(:strategies) }
end
+ describe 'default values' do
+ it { expect(described_class.new).to be_active }
+ it { expect(described_class.new.version).to eq('new_version_flag') }
+ end
+
describe '.reference_pattern' do
subject { described_class.reference_pattern }
diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb
index 9554fc3bb1b..c665f738ead 100644
--- a/spec/models/packages/package_file_spec.rb
+++ b/spec/models/packages/package_file_spec.rb
@@ -314,7 +314,7 @@ RSpec.describe Packages::PackageFile, type: :model do
# to `1`.
expect(package_file)
.to receive(:update_column)
- .with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL)
+ .with('file_store', ::Packages::PackageFileUploader::Store::LOCAL)
expect { subject }.to change { package_file.size }.from(nil).to(3513)
end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 0edb04224a3..241c585099c 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -969,12 +969,12 @@ RSpec.describe Packages::Package, type: :model do
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 ) }
+ 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])
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 463ec904e9a..e5f2e849a0a 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -209,6 +209,10 @@ RSpec.describe PagesDomain do
expect(subject.wildcard).to eq(false)
end
+ it 'defaults auto_ssl_enabled to false' do
+ expect(subject.auto_ssl_enabled).to eq(false)
+ end
+
it 'defaults scope to project' do
expect(subject.scope).to eq('project')
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 67e7d444d25..9d4c53f8d55 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -108,7 +108,7 @@ RSpec.describe PersonalAccessToken do
describe '.last_used_before' do
context 'last_used_*' do
let_it_be(:date) { DateTime.new(2022, 01, 01) }
- let_it_be(:token) { create(:personal_access_token, last_used_at: date ) }
+ let_it_be(:token) { create(:personal_access_token, last_used_at: date) }
# This token should never occur in the following tests and indicates that filtering was done correctly with it
let_it_be(:never_used_token) { create(:personal_access_token) }
@@ -194,47 +194,6 @@ RSpec.describe PersonalAccessToken do
end
end
- describe 'Redis storage' do
- let(:user_id) { 123 }
- let(:token) { 'KS3wegQYXBLYhQsciwsj' }
-
- context 'reading encrypted data' do
- before do
- subject.redis_store!(user_id, token)
- end
-
- it 'returns stored data' do
- expect(subject.redis_getdel(user_id)).to eq(token)
- end
- end
-
- context 'reading unencrypted data' do
- before do
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(described_class.redis_shared_state_key(user_id),
- token,
- ex: PersonalAccessToken::REDIS_EXPIRY_TIME)
- end
- end
-
- it 'returns stored data unmodified' do
- expect(subject.redis_getdel(user_id)).to eq(token)
- end
- end
-
- context 'after deletion' do
- before do
- subject.redis_store!(user_id, token)
-
- expect(subject.redis_getdel(user_id)).to eq(token)
- end
-
- it 'token is removed' do
- expect(subject.redis_getdel(user_id)).to be_nil
- end
- end
- end
-
context "validations" do
let(:personal_access_token) { build(:personal_access_token) }
@@ -365,7 +324,7 @@ RSpec.describe PersonalAccessToken do
describe '.simple_sorts' do
it 'includes overridden keys' do
- expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc expires_at_desc expires_at_asc_id_desc))
+ expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc_id_desc))
end
end
@@ -373,18 +332,6 @@ RSpec.describe PersonalAccessToken do
let_it_be(:earlier_token) { create(:personal_access_token, expires_at: 2.days.ago) }
let_it_be(:later_token) { create(:personal_access_token, expires_at: 1.day.ago) }
- describe '.order_expires_at_asc' do
- it 'returns ordered list in asc order of expiry date' do
- expect(described_class.order_expires_at_asc).to match [earlier_token, later_token]
- end
- end
-
- describe '.order_expires_at_desc' do
- it 'returns ordered list in desc order of expiry date' do
- expect(described_class.order_expires_at_desc).to match [later_token, earlier_token]
- end
- end
-
describe '.order_expires_at_asc_id_desc' do
let_it_be(:earlier_token_2) { create(:personal_access_token, expires_at: 2.days.ago) }
diff --git a/spec/models/preloaders/labels_preloader_spec.rb b/spec/models/preloaders/labels_preloader_spec.rb
index 86e64d114c7..07f148a0a6c 100644
--- a/spec/models/preloaders/labels_preloader_spec.rb
+++ b/spec/models/preloaders/labels_preloader_spec.rb
@@ -40,10 +40,11 @@ RSpec.describe Preloaders::LabelsPreloader do
def access_data(labels)
labels.each do |label|
- if label.is_a?(ProjectLabel)
+ case label
+ when ProjectLabel
label.project.project_feature
label.lazy_subscription(user, label.project)
- elsif label.is_a?(GroupLabel)
+ when GroupLabel
label.group.route
label.lazy_subscription(user)
end
diff --git a/spec/models/preloaders/project_root_ancestor_preloader_spec.rb b/spec/models/preloaders/project_root_ancestor_preloader_spec.rb
index bb0de24abe5..2462e305597 100644
--- a/spec/models/preloaders/project_root_ancestor_preloader_spec.rb
+++ b/spec/models/preloaders/project_root_ancestor_preloader_spec.rb
@@ -63,6 +63,14 @@ RSpec.describe Preloaders::ProjectRootAncestorPreloader do
it_behaves_like 'executes N matching DB queries', 0, :full_path
end
+
+ context 'when projects are an array and not an ActiveRecord::Relation' do
+ before do
+ described_class.new(projects, :namespace, additional_preloads).execute
+ end
+
+ it_behaves_like 'executes N matching DB queries', 4
+ end
end
context 'when the preloader is not used' do
diff --git a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
index 7411bc95147..1cfeeac49cd 100644
--- a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
@@ -28,34 +28,58 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
end
end
- describe '#execute', :request_store do
+ shared_examples '#execute' do
let(:projects_arg) { projects }
- before do
- Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_arg, user).execute
- end
-
- it 'avoids N+1 queries' do
- expect { query }.not_to make_queries
- end
-
- context 'when projects is an array of IDs' do
- let(:projects_arg) { projects.map(&:id) }
+ context 'when user is present' do
+ before do
+ Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_arg, user).execute
+ end
it 'avoids N+1 queries' do
expect { query }.not_to make_queries
end
+
+ context 'when projects is an array of IDs' do
+ let(:projects_arg) { projects.map(&:id) }
+
+ it 'avoids N+1 queries' do
+ expect { query }.not_to make_queries
+ end
+ end
+
+ # Test for handling of SQL table name clashes.
+ context 'when projects is a relation including project_authorizations' do
+ let(:projects_arg) do
+ Project.where(id: ProjectAuthorization.where(project_id: projects).select(:project_id))
+ end
+
+ it 'avoids N+1 queries' do
+ expect { query }.not_to make_queries
+ end
+ end
end
- # Test for handling of SQL table name clashes.
- context 'when projects is a relation including project_authorizations' do
- let(:projects_arg) do
- Project.where(id: ProjectAuthorization.where(project_id: projects).select(:project_id))
+ context 'when user is not present' do
+ before do
+ Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_arg, nil).execute
end
- it 'avoids N+1 queries' do
- expect { query }.not_to make_queries
+ it 'does not avoid N+1 queries' do
+ expect { query }.to make_queries
+ end
+ end
+ end
+
+ describe '#execute', :request_store do
+ include_examples '#execute'
+
+ context 'when projects_preloader_fix is disabled' do
+ before do
+ stub_feature_flags(projects_preloader_fix: false)
end
+
+ include_examples '#execute'
end
end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index 55fe28ceb6f..df89e97a41f 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -86,6 +86,25 @@ RSpec.describe ProjectAuthorization do
end
end
+ shared_examples_for 'does not log any detail' do
+ it 'does not log any detail' do
+ expect(Gitlab::AppLogger).not_to receive(:info)
+
+ execute
+ end
+ end
+
+ shared_examples_for 'logs the detail' do
+ it 'logs the detail' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ entire_size: 3,
+ message: 'Project authorizations refresh performed with delay'
+ )
+
+ execute
+ end
+ end
+
describe '.insert_all_in_batches' do
let_it_be(:user) { create(:user) }
let_it_be(:project_1) { create(:project) }
@@ -100,6 +119,8 @@ RSpec.describe ProjectAuthorization do
]
end
+ subject(:execute) { described_class.insert_all_in_batches(attributes, per_batch_size) }
+
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
@@ -110,7 +131,7 @@ RSpec.describe ProjectAuthorization do
specify do
expect(described_class).not_to receive(:sleep)
- described_class.insert_all_in_batches(attributes, per_batch_size)
+ execute
expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end
@@ -123,11 +144,13 @@ RSpec.describe ProjectAuthorization do
expect(described_class).to receive(:insert_all).twice.and_call_original
expect(described_class).to receive(:sleep).twice
- described_class.insert_all_in_batches(attributes, per_batch_size)
+ execute
expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end
+ it_behaves_like 'logs the detail'
+
context 'when the GitLab installation does not have a replica database configured' do
before do
# Configure as if a replica database is not enabled
@@ -135,6 +158,7 @@ RSpec.describe ProjectAuthorization do
end
it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch'
+ it_behaves_like 'does not log any detail'
end
end
@@ -142,6 +166,7 @@ RSpec.describe ProjectAuthorization do
let(:per_batch_size) { 5 }
it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch'
+ it_behaves_like 'does not log any detail'
end
end
@@ -154,6 +179,14 @@ RSpec.describe ProjectAuthorization do
let(:user_ids) { [user_1.id, user_2.id, user_3.id] }
+ subject(:execute) do
+ described_class.delete_all_in_batches_for_project(
+ project: project,
+ user_ids: user_ids,
+ per_batch: per_batch_size
+ )
+ end
+
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
@@ -171,11 +204,7 @@ RSpec.describe ProjectAuthorization do
specify do
expect(described_class).not_to receive(:sleep)
- described_class.delete_all_in_batches_for_project(
- project: project,
- user_ids: user_ids,
- per_batch: per_batch_size
- )
+ execute
expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end
@@ -187,15 +216,13 @@ RSpec.describe ProjectAuthorization do
it 'removes the project authorizations of the specified users in the current project, with a delay between each batch' do
expect(described_class).to receive(:sleep).twice
- described_class.delete_all_in_batches_for_project(
- project: project,
- user_ids: user_ids,
- per_batch: per_batch_size
- )
+ execute
expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end
+ it_behaves_like 'logs the detail'
+
context 'when the GitLab installation does not have a replica database configured' do
before do
# Configure as if a replica database is not enabled
@@ -203,6 +230,7 @@ RSpec.describe ProjectAuthorization do
end
it_behaves_like 'removes the project authorizations of the specified users in the current project, without a delay between each batch'
+ it_behaves_like 'does not log any detail'
end
end
@@ -210,6 +238,7 @@ RSpec.describe ProjectAuthorization do
let(:per_batch_size) { 5 }
it_behaves_like 'removes the project authorizations of the specified users in the current project, without a delay between each batch'
+ it_behaves_like 'does not log any detail'
end
end
@@ -222,6 +251,14 @@ RSpec.describe ProjectAuthorization do
let(:project_ids) { [project_1.id, project_2.id, project_3.id] }
+ subject(:execute) do
+ described_class.delete_all_in_batches_for_user(
+ user: user,
+ project_ids: project_ids,
+ per_batch: per_batch_size
+ )
+ end
+
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
@@ -239,11 +276,7 @@ RSpec.describe ProjectAuthorization do
specify do
expect(described_class).not_to receive(:sleep)
- described_class.delete_all_in_batches_for_user(
- user: user,
- project_ids: project_ids,
- per_batch: per_batch_size
- )
+ execute
expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end
@@ -255,15 +288,13 @@ RSpec.describe ProjectAuthorization do
it 'removes the project authorizations of the specified projects from the current user, with a delay between each batch' do
expect(described_class).to receive(:sleep).twice
- described_class.delete_all_in_batches_for_user(
- user: user,
- project_ids: project_ids,
- per_batch: per_batch_size
- )
+ execute
expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end
+ it_behaves_like 'logs the detail'
+
context 'when the GitLab installation does not have a replica database configured' do
before do
# Configure as if a replica database is not enabled
@@ -271,6 +302,7 @@ RSpec.describe ProjectAuthorization do
end
it_behaves_like 'removes the project authorizations of the specified projects from the current user, without a delay between each batch'
+ it_behaves_like 'does not log any detail'
end
end
@@ -278,6 +310,7 @@ RSpec.describe ProjectAuthorization do
let(:per_batch_size) { 5 }
it_behaves_like 'removes the project authorizations of the specified projects from the current user, without a delay between each batch'
+ it_behaves_like 'does not log any detail'
end
end
end
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 406485d8cc8..5a32e103e0f 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 '#separated_caches' do
+ it 'is true by default' do
+ expect(described_class.new.separated_caches).to be_truthy
+ end
+ end
+
describe '#default_git_depth' do
let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 5730ca58e9e..94a2e2fe3f9 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -6,6 +6,10 @@ RSpec.describe ProjectSetting, type: :model do
using RSpec::Parameterized::TableSyntax
it { is_expected.to belong_to(:project) }
+ describe 'default values' do
+ it { expect(subject.legacy_open_source_license_available).to be_truthy }
+ end
+
describe 'scopes' do
let_it_be(:project_1) { create(:project) }
let_it_be(:project_2) { create(:project) }
@@ -20,6 +24,7 @@ RSpec.describe ProjectSetting, type: :model do
describe 'validations' do
it { is_expected.not_to allow_value(nil).for(:target_platforms) }
it { is_expected.to allow_value([]).for(:target_platforms) }
+ it { is_expected.to validate_length_of(:issue_branch_template).is_at_most(255) }
it { is_expected.not_to allow_value(nil).for(:suggested_reviewers_enabled) }
it { is_expected.to allow_value(true).for(:suggested_reviewers_enabled) }
@@ -70,7 +75,7 @@ RSpec.describe ProjectSetting, type: :model do
describe '#show_diff_preview_in_email?' do
context 'when a project is a top-level namespace' do
- let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: false) }
+ let(:project_settings) { create(:project_setting, show_diff_preview_in_email: false) }
let(:project) { create(:project, project_setting: project_settings) }
context 'when show_diff_preview_in_email is disabled' do
@@ -80,7 +85,7 @@ RSpec.describe ProjectSetting, type: :model do
end
context 'when show_diff_preview_in_email is enabled' do
- let(:project_settings ) { create(:project_setting, show_diff_preview_in_email: true) }
+ let(:project_settings) { create(:project_setting, show_diff_preview_in_email: true) }
it 'returns true' do
settings = create(:project_setting, show_diff_preview_in_email: true)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 75887e49dc9..8cccc9ad83e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -38,6 +38,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:hooks) }
it { is_expected.to have_many(:protected_branches) }
it { is_expected.to have_many(:exported_protected_branches) }
+ it { is_expected.to have_one(:wiki_repository).class_name('Projects::WikiRepository').inverse_of(:project) }
it { is_expected.to have_one(:slack_integration) }
it { is_expected.to have_one(:microsoft_teams_integration) }
it { is_expected.to have_one(:mattermost_integration) }
@@ -597,7 +598,7 @@ RSpec.describe Project, factory_default: :keep do
end
it 'contains errors related to the project being deleted' do
- expect(new_project.errors.full_messages.first).to eq(_('The project is still being deleted. Please try again later.'))
+ expect(new_project.errors.full_messages).to include(_('The project is still being deleted. Please try again later.'))
end
end
@@ -862,6 +863,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to delegate_method(:environments_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:feature_flags_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:releases_access_level).to(:project_feature) }
+ it { is_expected.to delegate_method(:infrastructure_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:maven_package_requests_forwarding).to(:namespace) }
it { is_expected.to delegate_method(:pypi_package_requests_forwarding).to(:namespace) }
it { is_expected.to delegate_method(:npm_package_requests_forwarding).to(:namespace) }
@@ -1352,7 +1354,7 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#open_issues_count', :aggregate_failures do
+ describe '#open_issues_count' do
let(:project) { build(:project) }
it 'provides the issue count' do
@@ -1657,6 +1659,33 @@ RSpec.describe Project, factory_default: :keep do
expect(project.reload.star_count).to eq(0)
end
+ it 'does not count stars from blocked users' do
+ user1 = create(:user)
+ user2 = create(:user)
+ project = create(:project, :public)
+
+ expect(project.star_count).to eq(0)
+
+ user1.toggle_star(project)
+ expect(project.reload.star_count).to eq(1)
+
+ user2.toggle_star(project)
+ project.reload
+ expect(project.reload.star_count).to eq(2)
+
+ user1.block
+ project.reload
+ expect(project.reload.star_count).to eq(1)
+
+ user2.block
+ project.reload
+ expect(project.reload.star_count).to eq(0)
+
+ user1.activate
+ project.reload
+ expect(project.reload.star_count).to eq(1)
+ end
+
it 'counts stars on the right project' do
user = create(:user)
project1 = create(:project, :public)
@@ -2981,44 +3010,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#uses_external_project_ci_config?' do
- subject(:uses_external_project_ci_config) { project.uses_external_project_ci_config? }
-
- let(:project) { build(:project) }
-
- context 'when ci_config_path is configured with external project' do
- before do
- project.ci_config_path = '.gitlab-ci.yml@hello/world'
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when ci_config_path is nil' do
- before do
- project.ci_config_path = nil
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when ci_config_path is configured with a file in the project' do
- before do
- project.ci_config_path = 'hello/world/gitlab-ci.yml'
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when ci_config_path is configured with remote file' do
- before do
- project.ci_config_path = 'https://example.org/file.yml'
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
describe '#latest_successful_build_for_ref' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create_pipeline(project) }
@@ -4507,7 +4498,7 @@ RSpec.describe Project, factory_default: :keep do
project_2 = create(:project, :public, :merge_requests_disabled)
project_3 = create(:project, :public, :issues_disabled)
project_4 = create(:project, :public)
- project_4.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
+ project_4.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE)
project_ids = described_class.ids_with_issuables_available_for(user).pluck(:id)
@@ -5798,7 +5789,7 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#has_active_integrations?' do
- let_it_be(:project) { create(:project) }
+ let_it_be_with_refind(:project) { create(:project) }
it { expect(project.has_active_integrations?).to eq(false) }
@@ -5808,6 +5799,20 @@ RSpec.describe Project, factory_default: :keep do
expect(project.has_active_integrations?(:merge_request_hooks)).to eq(false)
expect(project.has_active_integrations?).to eq(true)
end
+
+ it 'caches matching integrations' do
+ create(:custom_issue_tracker_integration, push_events: true, merge_requests_events: false, project: project)
+
+ expect(project.has_active_integrations?(:merge_request_hooks)).to eq(false)
+ expect(project.has_active_integrations?).to eq(true)
+
+ count = ActiveRecord::QueryRecorder.new do
+ expect(project.has_active_integrations?(:merge_request_hooks)).to eq(false)
+ expect(project.has_active_integrations?).to eq(true)
+ end.count
+
+ expect(count).to eq(0)
+ end
end
describe '#badges' do
@@ -7074,7 +7079,7 @@ RSpec.describe Project, factory_default: :keep do
subject { project.self_monitoring? }
- context 'when the project is instance self monitoring' do
+ context 'when the project is instance self-monitoring' do
before do
stub_application_setting(self_monitoring_project_id: project.id)
end
@@ -7082,7 +7087,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to be true }
end
- context 'when the project is not self monitoring' do
+ context 'when the project is not self-monitoring' do
it { is_expected.to be false }
end
end
@@ -7124,7 +7129,7 @@ RSpec.describe Project, factory_default: :keep do
describe '#export_in_progress?' do
let(:project) { build(:project) }
- let!(:project_export_job ) { create(:project_export_job, project: project) }
+ let!(:project_export_job) { create(:project_export_job, project: project) }
context 'when project export is enqueued' do
it { expect(project.export_in_progress?).to be false }
@@ -7149,7 +7154,7 @@ RSpec.describe Project, factory_default: :keep do
describe '#export_status' do
let(:project) { build(:project) }
- let!(:project_export_job ) { create(:project_export_job, project: project) }
+ let!(:project_export_job) { create(:project_export_job, project: project) }
context 'when project export is enqueued' do
it { expect(project.export_status).to eq :queued }
@@ -7173,7 +7178,7 @@ RSpec.describe Project, factory_default: :keep do
end
context 'when project export is being regenerated' do
- let!(:new_project_export_job ) { create(:project_export_job, project: project) }
+ let!(:new_project_export_job) { create(:project_export_job, project: project) }
before do
finish_job(project_export_job)
@@ -7475,15 +7480,6 @@ RSpec.describe Project, factory_default: :keep do
end
end
- describe '#ci_config_external_project' do
- subject(:ci_config_external_project) { project.ci_config_external_project }
-
- let(:other_project) { create(:project) }
- let(:project) { build(:project, ci_config_path: ".gitlab-ci.yml@#{other_project.full_path}") }
-
- it { is_expected.to eq(other_project) }
- end
-
describe '#enabled_group_deploy_keys' do
let_it_be(:project) { create(:project) }
diff --git a/spec/models/projects/wiki_repository_spec.rb b/spec/models/projects/wiki_repository_spec.rb
new file mode 100644
index 00000000000..6868e1f5fb9
--- /dev/null
+++ b/spec/models/projects/wiki_repository_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::WikiRepository do
+ subject { described_class.new(project: build(:project)) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).inverse_of(:wiki_repository) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_uniqueness_of(:project) }
+ end
+end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index b88367b9ca2..b623d534f29 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -7,11 +7,54 @@ RSpec.describe ProtectedBranch do
describe 'Associations' do
it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:group) }
end
describe 'Validation' do
- it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
+
+ describe '#validate_either_project_or_top_group' do
+ context 'when protected branch does not have project or group association' do
+ it 'validate failed' do
+ subject.assign_attributes(project: nil, group: nil)
+ subject.validate
+
+ expect(subject.errors).to include(:base)
+ end
+ end
+
+ context 'when protected branch is associated with both project and group' do
+ it 'validate failed' do
+ subject.assign_attributes(project: build(:project), group: build(:group))
+ subject.validate
+
+ expect(subject.errors).to include(:base)
+ end
+ end
+
+ context 'when protected branch is associated with a subgroup' do
+ it 'validate failed' do
+ subject.assign_attributes(project: nil, group: build(:group, :nested))
+ subject.validate
+
+ expect(subject.errors).to include(:base)
+ end
+ end
+ end
+ end
+
+ describe 'set a group' do
+ context 'when associated with group' do
+ it 'create successfully' do
+ expect { subject.group = build(:group) }.not_to raise_error
+ end
+ end
+
+ context 'when associated with other namespace' do
+ it 'create failed with `ActiveRecord::AssociationTypeMismatch`' do
+ expect { subject.group = build(:namespace) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
+ end
+ end
end
describe "#matches?" do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 6fbf69ec23a..93872bcd827 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Repository do
include RepoHelpers
- include GitHelpers
TestBlob = Struct.new(:path)
@@ -463,7 +462,7 @@ RSpec.describe Repository do
repository.delete_branch(branch)
expect(subject).not_to be_empty
- expect(subject).to all( be_a(::Commit) )
+ expect(subject).to all(be_a(::Commit))
expect(subject.size).to eq(1)
end
end
@@ -482,7 +481,7 @@ RSpec.describe Repository do
end
it 'returns only Commit instances' do
- expect(subject).to all( be_a(Commit) )
+ expect(subject).to all(be_a(Commit))
end
context 'when some commits are not found ' do
@@ -2978,7 +2977,7 @@ RSpec.describe Repository do
it 'returns false for invalid commit IDs' do
expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false)
- expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
+ expect(repository.ancestor?(Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
end
end
diff --git a/spec/models/serverless/domain_cluster_spec.rb b/spec/models/serverless/domain_cluster_spec.rb
index fdae0483c19..487385c62c1 100644
--- a/spec/models/serverless/domain_cluster_spec.rb
+++ b/spec/models/serverless/domain_cluster_spec.rb
@@ -5,6 +5,16 @@ require 'spec_helper'
RSpec.describe ::Serverless::DomainCluster do
subject { create(:serverless_domain_cluster) }
+ describe 'default values' do
+ subject(:domain_cluster) { build(:serverless_domain_cluster) }
+
+ before do
+ allow(::Serverless::Domain).to receive(:generate_uuid).and_return('randomtoken')
+ end
+
+ it { expect(domain_cluster.uuid).to eq('randomtoken') }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:pages_domain) }
it { is_expected.to validate_presence_of(:knative) }
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index a40c7c5c892..564710b31d0 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -21,37 +21,18 @@ RSpec.describe SpamLog do
end
context 'when admin mode is enabled', :enable_admin_mode do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal', :sidekiq_inline do
- spam_log = build(:spam_log)
- user = spam_log.user
-
- perform_enqueued_jobs do
- spam_log.remove_user(deleted_by: admin)
- end
-
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- ).to be_exists
- end
- end
+ it 'initiates user removal', :sidekiq_inline do
+ spam_log = build(:spam_log)
+ user = spam_log.user
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ perform_enqueued_jobs do
+ spam_log.remove_user(deleted_by: admin)
end
- it 'removes the user', :sidekiq_inline do
- spam_log = build(:spam_log)
- user = spam_log.user
-
- perform_enqueued_jobs do
- spam_log.remove_user(deleted_by: admin)
- end
-
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ ).to be_exists
end
end
diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb
index a484952bfe9..533e6e4bd7b 100644
--- a/spec/models/terraform/state_spec.rb
+++ b/spec/models/terraform/state_spec.rb
@@ -10,6 +10,12 @@ RSpec.describe Terraform::State do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:project_id) }
+ it { is_expected.to validate_presence_of(:uuid) }
+
+ describe 'default values' do
+ it { expect(described_class.new.uuid).to be_present }
+ it { expect(described_class.new(uuid: 'test').uuid).to eq('test') }
+ end
describe 'scopes' do
describe '.ordered_by_name' do
diff --git a/spec/models/terraform/state_version_spec.rb b/spec/models/terraform/state_version_spec.rb
index 22b1397f30a..477041117cb 100644
--- a/spec/models/terraform/state_version_spec.rb
+++ b/spec/models/terraform/state_version_spec.rb
@@ -10,6 +10,15 @@ RSpec.describe Terraform::StateVersion do
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:build).class_name('Ci::Build').optional }
+ describe 'default attributes' do
+ before do
+ allow(Terraform::StateUploader).to receive(:default_store).and_return(5)
+ end
+
+ it { expect(described_class.new.file_store).to eq(5) }
+ it { expect(described_class.new(file_store: 3).file_store).to eq(3) }
+ end
+
describe 'scopes' do
describe '.ordered_by_version_desc' do
let(:terraform_state) { create(:terraform_state) }
diff --git a/spec/models/time_tracking/timelog_category_spec.rb b/spec/models/time_tracking/timelog_category_spec.rb
index d8b938e9d68..ac2fb651134 100644
--- a/spec/models/time_tracking/timelog_category_spec.rb
+++ b/spec/models/time_tracking/timelog_category_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe TimeTracking::TimelogCategory, type: :model do
it { is_expected.to belong_to(:namespace).with_foreign_key('namespace_id') }
end
+ describe 'default values' do
+ it { expect(described_class.new.color).to eq(described_class::DEFAULT_COLOR) }
+ end
+
describe 'validations' do
subject { create(:timelog_category) }
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 18b0cb36cc6..23ba0be2fbc 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -498,7 +498,7 @@ RSpec.describe Todo do
describe '.for_internal_notes' do
it 'returns todos created from internal notes' do
- internal_note = create(:note, confidential: true )
+ internal_note = create(:note, confidential: true)
todo = create(:todo, note: internal_note)
create(:todo)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8ebf3d70165..7207ee0b172 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -144,6 +144,7 @@ RSpec.describe User do
it { is_expected.to have_many(:callouts).class_name('Users::Callout') }
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') }
it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout') }
+ it { is_expected.to have_many(:created_projects).dependent(:nullify).class_name('Project') }
describe '#user_detail' do
it 'does not persist `user_detail` by default' do
@@ -2481,6 +2482,30 @@ RSpec.describe User do
end
end
+ describe 'starred_projects' do
+ let_it_be(:project) { create(:project) }
+
+ before do
+ user.toggle_star(project)
+ end
+
+ context 'when blocking a user' do
+ let_it_be(:user) { create(:user) }
+
+ it 'decrements star count of project' do
+ expect { user.block }.to change { project.reload.star_count }.by(-1)
+ end
+ end
+
+ context 'when activating a user' do
+ let_it_be(:user) { create(:user, :blocked) }
+
+ it 'increments star count of project' do
+ expect { user.activate }.to change { project.reload.star_count }.by(1)
+ end
+ end
+ end
+
describe '.instance_access_request_approvers_to_be_notified' do
let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) }
@@ -3031,6 +3056,10 @@ RSpec.describe User do
it 'returns no matches for nil' do
expect(described_class.search(nil)).to be_empty
end
+
+ it 'returns no matches for an array' do
+ expect(described_class.search(%w[the test])).to be_empty
+ end
end
describe '.user_search_minimum_char_limit' do
@@ -6157,172 +6186,28 @@ RSpec.describe User do
end
end
- describe '#authenticatable_salt' do
- let(:user) { create(:user) }
-
- subject(:authenticatable_salt) { user.authenticatable_salt }
-
- it 'uses password_salt' do
- expect(authenticatable_salt).to eq(user.password_salt)
- end
-
- context 'when the encrypted_password is an unknown type' do
- let(:encrypted_password) { '$argon2i$v=19$m=512,t=4,p=2$eM+ZMyYkpDRGaI3xXmuNcQ$c5DeJg3eb5dskVt1mDdxfw' }
-
- before do
- user.update_attribute(:encrypted_password, encrypted_password)
- end
-
- it 'returns the first 30 characters of the encrypted_password' do
- expect(authenticatable_salt).to eq(encrypted_password[0, 29])
- end
- end
-
- context 'when pbkdf2_password_encryption is disabled' do
- before do
- stub_feature_flags(pbkdf2_password_encryption: false)
- end
-
- it 'returns the first 30 characters of the encrypted_password' do
- expect(authenticatable_salt).to eq(user.encrypted_password[0, 29])
- end
- end
- end
-
- def compare_pbkdf2_password(user, password)
- Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.compare(user.encrypted_password, password)
- end
-
describe '#valid_password?' do
subject(:validate_password) { user.valid_password?(password) }
- context 'user with password not in disallowed list' do
- let(:user) { create(:user) }
- let(:password) { user.password }
-
- it { is_expected.to be_truthy }
-
- context 'using a wrong password' do
- let(:password) { 'WRONG PASSWORD' }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when pbkdf2_sha512_encryption is disabled and the user password is pbkdf2+sha512' do
- it 'does not validate correctly' do
- user # Create the user while the feature is enabled
- stub_feature_flags(pbkdf2_password_encryption: false)
-
- expect(validate_password).to be_falsey
- end
- end
- end
-
context 'user with disallowed password' do
let(:user) { create(:user, :disallowed_password) }
let(:password) { user.password }
- it { is_expected.to be_falsey }
-
- context 'using a wrong password' do
- let(:password) { 'WRONG PASSWORD' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'user with a bcrypt password hash' do
- # Manually set a 'known' encrypted password
- let(:password) { User.random_password }
- let(:encrypted_password) { Devise::Encryptor.digest(User, password) }
- let(:user) { create(:user, encrypted_password: encrypted_password) }
-
- shared_examples 'not re-encrypting with PBKDF2' do
- it 'does not re-encrypt with PBKDF2' do
- validate_password
-
- expect(user.reload.encrypted_password).to eq(encrypted_password)
- end
- end
-
- context 'using the wrong password' do
- # password 'WRONG PASSWORD' will not match the bcrypt hash
- let(:password) { 'WRONG PASSWORD' }
- let(:encrypted_password) { Devise::Encryptor.digest(User, User.random_password) }
-
- it { is_expected.to be_falsey }
-
- it_behaves_like 'not re-encrypting with PBKDF2'
-
- context 'when pbkdf2_password_encryption is disabled' do
- before do
- stub_feature_flags(pbkdf2_password_encryption: false)
- end
-
- it { is_expected.to be_falsey }
-
- it_behaves_like 'not re-encrypting with PBKDF2'
- end
- end
-
- context 'using the correct password' do
- it { is_expected.to be_truthy }
-
- it 'validates the password and re-encrypts with PBKDF2' do
- validate_password
-
- current_encrypted_password = user.reload.encrypted_password
-
- expect(compare_pbkdf2_password(user, password)).to eq(true)
- expect { ::BCrypt::Password.new(current_encrypted_password) }
- .to raise_error(::BCrypt::Errors::InvalidHash)
- end
-
- context 'when pbkdf2_password_encryption is disabled' do
- before do
- stub_feature_flags(pbkdf2_password_encryption: false)
- end
-
- it { is_expected.to be_truthy }
-
- it_behaves_like 'not re-encrypting with PBKDF2'
- end
-
- context 'when pbkdf2_password_encryption_write is disabled' do
- before do
- stub_feature_flags(pbkdf2_password_encryption_write: false)
- end
-
- it { is_expected.to be_truthy }
-
- it_behaves_like 'not re-encrypting with PBKDF2'
- end
- end
+ it { is_expected.to eq(false) }
end
- context 'user with password hash that is neither PBKDF2 nor BCrypt' do
- # Manually calculated User.random_password
- let(:password) { "gg_w215TmVXGWSt7RJKXwYTVz886f6SDM3zvzztaJf2mX9ttUE8gRkNJSbWyWRLqxz4LFzxBekPe75ydDcGauE9wqg-acKMRT-WpSYjTm1Rdx-tnssE7CQByJcnxwWNH" }
- # Created with https://argon2.online/ using 'aaaaaaaa' as the salt
- let(:encrypted_password) { "$argon2i$v=19$m=512,t=4,p=2$YWFhYWFhYWE$PvJscKO5XRlevcgRReUg6w" }
- let(:user) { create(:user, encrypted_password: encrypted_password) }
-
- it { is_expected.to be_falsey }
-
- context 'when pbkdf2_password_encryption is disabled' do
- before do
- stub_feature_flags(pbkdf2_password_encryption: false)
- end
+ context 'using a wrong password' do
+ let(:user) { create(:user) }
+ let(:password) { 'WRONG PASSWORD' }
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to eq(false) }
end
context 'user with autogenerated_password' do
let(:user) { build_stubbed(:user, password_automatically_set: true) }
let(:password) { user.password }
- it { is_expected.to be_falsey }
+ it { is_expected.to eq(false) }
end
end
@@ -6377,95 +6262,6 @@ RSpec.describe User do
end
end
- # These entire test section can be removed once the :pbkdf2_password_encryption feature flag is removed.
- describe '#password=' do
- let(:user) { create(:user) }
- let(:password) { User.random_password }
-
- def compare_bcrypt_password(user, password)
- Devise::Encryptor.compare(User, user.encrypted_password, password)
- end
-
- context 'when pbkdf2_password_encryption is enabled' do
- it 'calls PBKDF2 digest and not the default Devise encryptor' do
- expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512).to receive(:digest).at_least(:once).and_call_original
- expect(Devise::Encryptor).not_to receive(:digest)
-
- user.password = password
- end
-
- it 'saves the password in PBKDF2 format' do
- user.password = password
- user.save!
-
- expect(compare_pbkdf2_password(user, password)).to eq(true)
- expect { compare_bcrypt_password(user, password) }.to raise_error(::BCrypt::Errors::InvalidHash)
- end
-
- context 'when pbkdf2_password_encryption_write is disabled' do
- before do
- stub_feature_flags(pbkdf2_password_encryption_write: false)
- end
-
- it 'calls default Devise encryptor and not the PBKDF2 encryptor' do
- expect(Devise::Encryptor).to receive(:digest).at_least(:once).and_call_original
- expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512).not_to receive(:digest)
-
- user.password = password
- end
- end
- end
-
- context 'when pbkdf2_password_encryption is disabled' do
- before do
- stub_feature_flags(pbkdf2_password_encryption: false)
- end
-
- it 'calls default Devise encryptor and not the PBKDF2 encryptor' do
- expect(Devise::Encryptor).to receive(:digest).at_least(:once).and_call_original
- expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512).not_to receive(:digest)
-
- user.password = password
- end
-
- it 'saves the password in BCrypt format' do
- user.password = password
- user.save!
-
- expect { compare_pbkdf2_password(user, password) }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash
- expect(compare_bcrypt_password(user, password)).to eq(true)
- end
- end
- end
-
- describe '#password_strategy' do
- let(:user) { create(:user, encrypted_password: encrypted_password) }
-
- context 'with a PBKDF2+SHA512 encrypted password' do
- let(:encrypted_password) { '$pbkdf2-sha512$20000$boHGAw0hEyI$DBA67J7zNZebyzLtLk2X9wRDbmj1LNKVGnZLYyz6PGrIDGIl45fl/BPH0y1TPZnV90A20i.fD9C3G9Bp8jzzOA' }
-
- it 'extracts the correct strategy', :aggregate_failures do
- expect(user.password_strategy).to eq(:pbkdf2_sha512)
- end
- end
-
- context 'with a BCrypt encrypted password' do
- let(:encrypted_password) { '$2a$10$xLTxCKOa75IU4RQGqqOrTuZOgZdJEzfSzjG6ZSEi/C31TB/yLZYpi' }
-
- it 'extracts the correct strategy', :aggregate_failures do
- expect(user.password_strategy).to eq(:bcrypt)
- end
- end
-
- context 'with an unknown encrypted password' do
- let(:encrypted_password) { '$pbkdf2-sha256$6400$.6UI/S.nXIk8jcbdHx3Fhg$98jZicV16ODfEsEZeYPGHU3kbrUrvUEXOPimVSQDD44' }
-
- it 'returns unknown strategy' do
- expect(user.password_strategy).to eq(:unknown)
- end
- end
- end
-
describe '#password_expired?' do
let(:user) { build(:user, password_expires_at: password_expires_at) }
diff --git a/spec/models/users/calloutable_spec.rb b/spec/models/users/calloutable_spec.rb
index 791fe1c1bc4..7e186445c1b 100644
--- a/spec/models/users/calloutable_spec.rb
+++ b/spec/models/users/calloutable_spec.rb
@@ -15,8 +15,8 @@ RSpec.describe Users::Calloutable do
describe '#dismissed_after?' do
let(:some_feature_name) { Users::Callout.feature_names.keys.second }
- let(:callout_dismissed_month_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.month.ago ) }
- let(:callout_dismissed_day_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.day.ago ) }
+ let(:callout_dismissed_month_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.month.ago) }
+ let(:callout_dismissed_day_ago) { create(:callout, feature_name: some_feature_name, dismissed_at: 1.day.ago) }
it 'returns whether a callout dismissed after specified date' do
expect(callout_dismissed_month_ago.dismissed_after?(15.days.ago)).to eq(false)
diff --git a/spec/models/users/ghost_user_migration_spec.rb b/spec/models/users/ghost_user_migration_spec.rb
index d4a0657c3be..a0b2af6175a 100644
--- a/spec/models/users/ghost_user_migration_spec.rb
+++ b/spec/models/users/ghost_user_migration_spec.rb
@@ -8,7 +8,18 @@ RSpec.describe Users::GhostUserMigration do
it { is_expected.to belong_to(:initiator_user) }
end
- describe 'validation' do
+ describe 'validations' do
it { is_expected.to validate_presence_of(:user_id) }
end
+
+ describe 'scopes' do
+ describe '.consume_order' do
+ let!(:ghost_user_migration_1) { create(:ghost_user_migration, consume_after: Time.current) }
+ let!(:ghost_user_migration_2) { create(:ghost_user_migration, consume_after: 5.minutes.ago) }
+
+ subject { described_class.consume_order.to_a }
+
+ it { is_expected.to eq([ghost_user_migration_2, ghost_user_migration_1]) }
+ end
+ end
end
diff --git a/spec/models/users/namespace_commit_email_spec.rb b/spec/models/users/namespace_commit_email_spec.rb
new file mode 100644
index 00000000000..696dac25f9b
--- /dev/null
+++ b/spec/models/users/namespace_commit_email_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::NamespaceCommitEmail, type: :model do
+ subject { build(:namespace_commit_email) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:namespace) }
+ it { is_expected.to belong_to(:email) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:namespace) }
+ it { is_expected.to validate_presence_of(:email) }
+ end
+
+ it { is_expected.to be_valid }
+end
diff --git a/spec/models/users_star_project_spec.rb b/spec/models/users_star_project_spec.rb
index e41519a2b69..60ec108f77d 100644
--- a/spec/models/users_star_project_spec.rb
+++ b/spec/models/users_star_project_spec.rb
@@ -3,5 +3,85 @@
require 'spec_helper'
RSpec.describe UsersStarProject, type: :model do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:user_active) { create(:user, state: 'active', name: 'user2', private_profile: true) }
+ let_it_be(:user_blocked) { create(:user, state: 'blocked', name: 'user1') }
+
it { is_expected.to belong_to(:project).touch(false) }
+
+ describe 'scopes' do
+ let_it_be(:users_star_project1) { create(:users_star_project, project: project1, user: user_active) }
+ let_it_be(:users_star_project2) { create(:users_star_project, project: project2, user: user_blocked) }
+
+ describe '.all' do
+ it 'returns all records' do
+ expect(described_class.all).to contain_exactly(users_star_project1, users_star_project2)
+ end
+ end
+
+ describe '.with_active_user' do
+ it 'returns only records of active users' do
+ expect(described_class.with_active_user).to contain_exactly(users_star_project1)
+ end
+ end
+
+ describe '.order_user_name_asc' do
+ it 'sorts records by ascending user name' do
+ expect(described_class.order_user_name_asc).to eq([users_star_project2, users_star_project1])
+ end
+ end
+
+ describe '.order_user_name_desc' do
+ it 'sorts records by descending user name' do
+ expect(described_class.order_user_name_desc).to eq([users_star_project1, users_star_project2])
+ end
+ end
+
+ describe '.by_project' do
+ it 'returns only records of given project' do
+ expect(described_class.by_project(project2)).to contain_exactly(users_star_project2)
+ end
+ end
+
+ describe '.with_public_profile' do
+ it 'returns only records of users with public profile' do
+ expect(described_class.with_public_profile).to contain_exactly(users_star_project2)
+ end
+ end
+ end
+
+ describe 'star count hooks' do
+ context 'on after_create' do
+ context 'if user is active' do
+ it 'increments star count of project' do
+ expect { user_active.toggle_star(project1) }.to change { project1.reload.star_count }.by(1)
+ end
+ end
+
+ context 'if user is not active' do
+ it 'does not increment star count of project' do
+ expect { user_blocked.toggle_star(project1) }.not_to change { project1.reload.star_count }
+ end
+ end
+ end
+
+ context 'on after_destory' do
+ context 'if user is active' do
+ let_it_be(:users_star_project) { create(:users_star_project, project: project2, user: user_active) }
+
+ it 'decrements star count of project' do
+ expect { users_star_project.destroy! }.to change { project2.reload.star_count }.by(-1)
+ end
+ end
+
+ context 'if user is not active' do
+ let_it_be(:users_star_project) { create(:users_star_project, project: project2, user: user_blocked) }
+
+ it 'does not decrement star count of project' do
+ expect { users_star_project.destroy! }.not_to change { project2.reload.star_count }
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index e41df7f0f61..6685720778a 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -35,10 +35,10 @@ RSpec.describe WorkItems::Type do
it 'deletes type but not unrelated issues' do
type = create(:work_item_type)
- expect(WorkItems::Type.count).to eq(6)
+ expect(WorkItems::Type.count).to eq(8)
expect { type.destroy! }.not_to change(Issue, :count)
- expect(WorkItems::Type.count).to eq(5)
+ expect(WorkItems::Type.count).to eq(7)
end
end
@@ -69,7 +69,8 @@ RSpec.describe WorkItems::Type do
::WorkItems::Widgets::Hierarchy,
::WorkItems::Widgets::Labels,
::WorkItems::Widgets::Assignees,
- ::WorkItems::Widgets::StartAndDueDate
+ ::WorkItems::Widgets::StartAndDueDate,
+ ::WorkItems::Widgets::Milestone
)
end
end
diff --git a/spec/models/work_items/widgets/hierarchy_spec.rb b/spec/models/work_items/widgets/hierarchy_spec.rb
index cd528772710..c847f2694fe 100644
--- a/spec/models/work_items/widgets/hierarchy_spec.rb
+++ b/spec/models/work_items/widgets/hierarchy_spec.rb
@@ -26,22 +26,6 @@ RSpec.describe WorkItems::Widgets::Hierarchy do
subject { described_class.new(parent_link.work_item).parent }
it { is_expected.to eq(parent_link.work_item_parent) }
-
- context 'when work_items flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it { is_expected.to be_nil }
- end
-
- context 'when work_items flag is enabled for the parent group' do
- before do
- stub_feature_flags(work_items: group)
- end
-
- it { is_expected.to eq(parent_link.work_item_parent) }
- end
end
describe '#children' do
@@ -51,21 +35,5 @@ RSpec.describe WorkItems::Widgets::Hierarchy do
subject { described_class.new(work_item_parent).children }
it { is_expected.to contain_exactly(parent_link1.work_item, parent_link2.work_item) }
-
- context 'when work_items flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it { is_expected.to be_empty }
- end
-
- context 'when work_items flag is enabled for the parent group' do
- before do
- stub_feature_flags(work_items: group)
- end
-
- it { is_expected.to contain_exactly(parent_link1.work_item, parent_link2.work_item) }
- end
end
end
diff --git a/spec/models/work_items/widgets/milestone_spec.rb b/spec/models/work_items/widgets/milestone_spec.rb
new file mode 100644
index 00000000000..7b2d661df29
--- /dev/null
+++ b/spec/models/work_items/widgets/milestone_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::Milestone do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:work_item) { create(:work_item, :issue, project: project, milestone: milestone) }
+
+ describe '.type' do
+ subject { described_class.type }
+
+ it { is_expected.to eq(:milestone) }
+ end
+
+ describe '#type' do
+ subject { described_class.new(work_item).type }
+
+ it { is_expected.to eq(:milestone) }
+ end
+
+ describe '#milestone' do
+ subject { described_class.new(work_item).milestone }
+
+ it { is_expected.to eq(work_item.milestone) }
+ end
+end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index da0427420e4..4a8855f1da7 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -591,34 +591,4 @@ RSpec.describe GlobalPolicy do
it { is_expected.not_to be_allowed(:log_in) }
end
end
-
- describe 'delete runners' do
- context 'when anonymous' do
- let(:current_user) { nil }
-
- it { is_expected.not_to be_allowed(:delete_runners) }
- end
-
- context 'regular user' do
- it { is_expected.not_to be_allowed(:delete_runners) }
- end
-
- context 'when external' do
- let(:current_user) { build(:user, :external) }
-
- it { is_expected.not_to be_allowed(:delete_runners) }
- end
-
- context 'admin user' do
- let_it_be(:current_user) { create(:user, :admin) }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it { is_expected.to be_allowed(:delete_runners) }
- end
-
- context 'when admin mode is disabled' do
- it { is_expected.to be_disallowed(:delete_runners) }
- end
- end
- end
end
diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb
index 27ce683861c..6ae5e43def4 100644
--- a/spec/policies/group_member_policy_spec.rb
+++ b/spec/policies/group_member_policy_spec.rb
@@ -83,6 +83,31 @@ RSpec.describe GroupMemberPolicy do
specify { expect_allowed(:read_group) }
end
+ context 'for access requests' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:user) { create(:user) }
+
+ let(:current_user) { user }
+
+ context 'for own access request' do
+ let(:membership) { create(:group_member, :access_request, group: group, user: user) }
+
+ specify { expect_allowed(:withdraw_member_access_request) }
+ end
+
+ context "for another user's access request" do
+ let(:membership) { create(:group_member, :access_request, group: group, user: create(:user)) }
+
+ specify { expect_disallowed(:withdraw_member_access_request) }
+ end
+
+ context 'for own, valid membership' do
+ let(:membership) { create(:group_member, :developer, group: group, user: user) }
+
+ specify { expect_disallowed(:withdraw_member_access_request) }
+ end
+ end
+
context 'with bot user' do
let(:current_user) { create(:user, :project_bot) }
@@ -100,74 +125,83 @@ RSpec.describe GroupMemberPolicy do
specify { expect_disallowed(:read_group, :destroy_project_bot_member) }
end
- context 'with one owner' do
+ context 'with owner' do
let(:current_user) { owner }
- specify { expect_disallowed(*member_related_permissions) }
- specify { expect_allowed(:read_group) }
- end
+ context 'with group with one owner' do
+ specify { expect_disallowed(*member_related_permissions) }
+ specify { expect_allowed(:read_group) }
+ end
- context 'with one blocked owner' do
- let(:owner) { create(:user, :blocked) }
- let(:current_user) { owner }
+ context 'with group with bot user owner' do
+ before do
+ group.add_owner(create(:user, :project_bot))
+ end
- specify { expect_disallowed(*member_related_permissions) }
- specify { expect_disallowed(:read_group) }
- end
+ specify { expect_disallowed(*member_related_permissions) }
+ end
- context 'with more than one owner' do
- let(:current_user) { owner }
+ context 'with group with more than one owner' do
+ before do
+ group.add_owner(create(:user))
+ end
- before do
- group.add_owner(create(:user))
+ specify { expect_allowed(*member_related_permissions) }
+ specify { expect_disallowed(:destroy_project_bot_member) }
end
- specify { expect_allowed(*member_related_permissions) }
- specify { expect_disallowed(:destroy_project_bot_member) }
- end
+ context 'with group with owners from a parent' do
+ context 'when top-level group' do
+ context 'with group sharing' do
+ let!(:subgroup) { create(:group, :private, parent: group) }
- context 'with the group parent' do
- let(:current_user) { create :user }
- let(:subgroup) { create(:group, :private, parent: group) }
+ before do
+ create(:group_group_link, :owner, shared_group: group, shared_with_group: subgroup)
+ create(:group_member, :owner, group: subgroup)
+ end
- before do
- group.add_owner(owner)
- subgroup.add_owner(current_user)
- end
+ specify { expect_disallowed(*member_related_permissions) }
+ specify { expect_allowed(:read_group) }
+ end
+ end
- it do
- expect_allowed(:destroy_group_member)
- expect_allowed(:update_group_member)
- end
- end
+ context 'when subgroup' do
+ let(:current_user) { create :user }
- context 'without group parent' do
- let(:current_user) { create :user }
- let(:subgroup) { create(:group, :private) }
+ let!(:subgroup) { create(:group, :private, parent: group) }
- before do
- subgroup.add_owner(current_user)
- end
+ before do
+ subgroup.add_owner(current_user)
+ end
- it do
- expect_disallowed(:destroy_group_member)
- expect_disallowed(:update_group_member)
+ specify { expect_allowed(*member_related_permissions) }
+ specify { expect_allowed(:read_group) }
+ end
end
end
- context 'without group parent with two owners' do
- let(:current_user) { create :user }
- let(:other_user) { create :user }
- let(:subgroup) { create(:group, :private) }
+ context 'with blocked owner' do
+ let(:owner) { create(:user, :blocked) }
+ let(:current_user) { owner }
- before do
- subgroup.add_owner(current_user)
- subgroup.add_owner(other_user)
+ specify { expect_disallowed(*member_related_permissions) }
+ specify { expect_disallowed(:read_group) }
+
+ context 'with group with bot user owner' do
+ before do
+ group.add_owner(create(:user, :project_bot))
+ end
+
+ specify { expect_disallowed(*member_related_permissions) }
+ specify { expect_disallowed(:read_group) }
end
- it do
- expect_allowed(:destroy_group_member)
- expect_allowed(:update_group_member)
+ context 'with group with more than one blocked owner' do
+ before do
+ group.add_owner(create(:user, :blocked))
+ end
+
+ specify { expect_allowed(:destroy_group_member) }
end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index c65933c5208..60acacac814 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -258,6 +258,19 @@ RSpec.describe GroupPolicy do
it_behaves_like 'deploy token does not get confused with user' do
let(:user_id) { migration_bot.id }
end
+
+ context 'with no user' do
+ let(:current_user) { nil }
+
+ it :aggregate_failures do
+ expect_disallowed(:read_resource_access_tokens, :destroy_resource_access_tokens)
+ expect_disallowed(*guest_permissions)
+ expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
+ end
end
describe 'private nested group use the highest access level from the group and inherited permissions' do
diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb
index 2bedcf60539..c8c322b02db 100644
--- a/spec/policies/issuable_policy_spec.rb
+++ b/spec/policies/issuable_policy_spec.rb
@@ -31,8 +31,8 @@ RSpec.describe IssuablePolicy, models: true do
expect(policies).to be_allowed(:resolve_note)
end
- it 'allows reading internal notes' do
- expect(policies).to be_allowed(:read_internal_note)
+ it 'does not allow reading internal notes' do
+ expect(policies).to be_disallowed(:read_internal_note)
end
context 'when user is able to read project' do
@@ -94,8 +94,8 @@ RSpec.describe IssuablePolicy, models: true do
let(:issue) { create(:issue, project: project, assignees: [user]) }
let(:policies) { described_class.new(user, issue) }
- it 'allows reading internal notes' do
- expect(policies).to be_allowed(:read_internal_note)
+ it 'does not allow reading internal notes' do
+ expect(policies).to be_disallowed(:read_internal_note)
end
end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index eeaa77a4589..6a261b4ff5b 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -309,42 +309,41 @@ RSpec.describe NotePolicy do
shared_examples_for 'confidential notes permissions' do
it 'does not allow non members to read confidential notes and replies' do
- expect(permissions(non_member, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
+ expect(permissions(non_member, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji, :mark_note_as_confidential)
end
it 'does not allow guests to read confidential notes and replies' do
- expect(permissions(guest, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
+ expect(permissions(guest, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji, :mark_note_as_confidential)
end
it 'allows reporter to read all notes but not resolve and admin them' do
- expect(permissions(reporter, confidential_note)).to be_allowed(:read_note, :award_emoji)
+ expect(permissions(reporter, confidential_note)).to be_allowed(:read_note, :award_emoji, :mark_note_as_confidential)
expect(permissions(reporter, confidential_note)).to be_disallowed(:admin_note, :reposition_note, :resolve_note)
end
it 'allows developer to read and resolve all notes' do
- expect(permissions(developer, confidential_note)).to be_allowed(:read_note, :award_emoji, :resolve_note)
+ expect(permissions(developer, confidential_note)).to be_allowed(:read_note, :award_emoji, :resolve_note, :mark_note_as_confidential)
expect(permissions(developer, confidential_note)).to be_disallowed(:admin_note, :reposition_note)
end
it 'allows maintainers to read all notes and admin them' do
- expect(permissions(maintainer, confidential_note)).to be_allowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
+ expect(permissions(maintainer, confidential_note)).to be_allowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji, :mark_note_as_confidential)
end
context 'when admin mode is enabled', :enable_admin_mode do
it 'allows admins to read all notes and admin them' do
- expect(permissions(admin, confidential_note)).to be_allowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
+ expect(permissions(admin, confidential_note)).to be_allowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji, :mark_note_as_confidential)
end
end
context 'when admin mode is disabled' do
it 'does not allow non members to read confidential notes and replies' do
- expect(permissions(admin, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji)
+ expect(permissions(admin, confidential_note)).to be_disallowed(:read_note, :admin_note, :reposition_note, :resolve_note, :award_emoji, :mark_note_as_confidential)
end
end
- it 'allows noteable author to read and resolve all notes' do
- expect(permissions(author, confidential_note)).to be_allowed(:read_note, :resolve_note, :award_emoji)
- expect(permissions(author, confidential_note)).to be_disallowed(:admin_note, :reposition_note)
+ it 'disallows noteable author to read and resolve all notes' do
+ expect(permissions(author, confidential_note)).to be_disallowed(:read_note, :resolve_note, :award_emoji, :mark_note_as_confidential, :admin_note, :reposition_note)
end
end
@@ -354,9 +353,8 @@ RSpec.describe NotePolicy do
it_behaves_like 'confidential notes permissions'
- it 'allows noteable assignees to read all notes' do
- expect(permissions(assignee, confidential_note)).to be_allowed(:read_note, :award_emoji)
- expect(permissions(assignee, confidential_note)).to be_disallowed(:admin_note, :reposition_note, :resolve_note)
+ it 'disallows noteable assignees to read all notes' do
+ expect(permissions(assignee, confidential_note)).to be_disallowed(:read_note, :award_emoji, :mark_note_as_confidential, :admin_note, :reposition_note, :resolve_note)
end
end
end
diff --git a/spec/policies/project_member_policy_spec.rb b/spec/policies/project_member_policy_spec.rb
index b19ab71fcb5..d7c155b39f5 100644
--- a/spec/policies/project_member_policy_spec.rb
+++ b/spec/policies/project_member_policy_spec.rb
@@ -4,13 +4,14 @@ require 'spec_helper'
RSpec.describe ProjectMemberPolicy do
let(:project) { create(:project) }
- let(:maintainer_user) { create(:user) }
+ let(:maintainer) { create(:user) }
let(:member) { create(:project_member, project: project, user: member_user) }
+ let(:current_user) { maintainer }
- subject { described_class.new(maintainer_user, member) }
+ subject { described_class.new(current_user, member) }
before do
- create(:project_member, :maintainer, project: project, user: maintainer_user)
+ create(:project_member, :maintainer, project: project, user: maintainer)
end
context 'with regular member' do
@@ -40,4 +41,29 @@ RSpec.describe ProjectMemberPolicy do
it { is_expected.not_to be_allowed(:update_project_member) }
it { is_expected.not_to be_allowed(:destroy_project_member) }
end
+
+ context 'for access requests' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+
+ let(:current_user) { user }
+
+ context 'for own access request' do
+ let(:member) { create(:project_member, :access_request, project: project, user: user) }
+
+ specify { expect_allowed(:withdraw_member_access_request) }
+ end
+
+ context "for another user's access request" do
+ let(:member) { create(:project_member, :access_request, project: project, user: create(:user)) }
+
+ specify { expect_disallowed(:withdraw_member_access_request) }
+ end
+
+ context 'for own, valid membership' do
+ let(:member) { create(:project_member, :developer, project: project, user: user) }
+
+ specify { expect_disallowed(:withdraw_member_access_request) }
+ end
+ end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 40ee2e662b2..973ed66b8d8 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -323,7 +323,7 @@ RSpec.describe ProjectPolicy do
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
:create_cluster, :read_cluster, :update_cluster, :admin_cluster,
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
- :destroy_release, :download_code, :build_download_code
+ :download_code, :build_download_code
]
end
@@ -533,6 +533,41 @@ RSpec.describe ProjectPolicy do
end
end
+ context 'with timeline event tags' do
+ context 'when user is member of the project' do
+ it 'allows access to timeline event tags' do
+ expect(described_class.new(owner, project)).to be_allowed(:read_incident_management_timeline_event_tag)
+ expect(described_class.new(developer, project)).to be_allowed(:read_incident_management_timeline_event_tag)
+ expect(described_class.new(guest, project)).to be_allowed(:read_incident_management_timeline_event_tag)
+ expect(described_class.new(admin, project)).to be_allowed(:read_incident_management_timeline_event_tag)
+ end
+ end
+
+ context 'when user is a maintainer/owner' do
+ it 'allows to create timeline event tags' do
+ expect(described_class.new(maintainer, project)).to be_allowed(:admin_incident_management_timeline_event_tag)
+ expect(described_class.new(owner, project)).to be_allowed(:admin_incident_management_timeline_event_tag)
+ end
+ end
+
+ context 'when user is a developer/guest/reporter' do
+ it 'disallows creation' do
+ expect(described_class.new(developer, project)).to be_disallowed(:admin_incident_management_timeline_event_tag)
+ expect(described_class.new(guest, project)).to be_disallowed(:admin_incident_management_timeline_event_tag)
+ expect(described_class.new(reporter, project)).to be_disallowed(:admin_incident_management_timeline_event_tag)
+ end
+ end
+
+ context 'when user is not a member of the project' do
+ let(:project) { private_project }
+
+ it 'disallows access to the timeline event tags' do
+ expect(described_class.new(non_member, project)).to be_disallowed(:read_incident_management_timeline_event_tag)
+ expect(described_class.new(non_member, project)).to be_disallowed(:admin_incident_management_timeline_event_tag)
+ end
+ end
+ end
+
context 'reading a project' do
it 'allows access when a user has read access to the repo' do
expect(described_class.new(owner, project)).to be_allowed(:read_project)
@@ -629,17 +664,7 @@ RSpec.describe ProjectPolicy do
context 'when user is member of the project' do
let(:current_user) { developer }
- context 'when work_items feature flag is enabled' do
- it { expect_allowed(:create_task) }
- end
-
- context 'when work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it { expect_disallowed(:create_task) }
- end
+ it { expect_allowed(:create_task) }
end
end
@@ -2299,6 +2324,74 @@ RSpec.describe ProjectPolicy do
end
end
+ describe 'infrastructure feature' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:guest_permissions) { [] }
+
+ let(:developer_permissions) do
+ guest_permissions + [:read_terraform_state, :read_pod_logs, :read_prometheus]
+ end
+
+ let(:maintainer_permissions) do
+ developer_permissions + [:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :admin_terraform_state, :admin_project_google_cloud]
+ end
+
+ where(:project_visibility, :access_level, :role, :allowed) do
+ :public | ProjectFeature::ENABLED | :maintainer | true
+ :public | ProjectFeature::ENABLED | :developer | true
+ :public | ProjectFeature::ENABLED | :guest | true
+ :public | ProjectFeature::ENABLED | :anonymous | true
+ :public | ProjectFeature::PRIVATE | :maintainer | true
+ :public | ProjectFeature::PRIVATE | :developer | true
+ :public | ProjectFeature::PRIVATE | :guest | true
+ :public | ProjectFeature::PRIVATE | :anonymous | false
+ :public | ProjectFeature::DISABLED | :maintainer | false
+ :public | ProjectFeature::DISABLED | :developer | false
+ :public | ProjectFeature::DISABLED | :guest | false
+ :public | ProjectFeature::DISABLED | :anonymous | false
+ :internal | ProjectFeature::ENABLED | :maintainer | true
+ :internal | ProjectFeature::ENABLED | :developer | true
+ :internal | ProjectFeature::ENABLED | :guest | true
+ :internal | ProjectFeature::ENABLED | :anonymous | false
+ :internal | ProjectFeature::PRIVATE | :maintainer | true
+ :internal | ProjectFeature::PRIVATE | :developer | true
+ :internal | ProjectFeature::PRIVATE | :guest | true
+ :internal | ProjectFeature::PRIVATE | :anonymous | false
+ :internal | ProjectFeature::DISABLED | :maintainer | false
+ :internal | ProjectFeature::DISABLED | :developer | false
+ :internal | ProjectFeature::DISABLED | :guest | false
+ :internal | ProjectFeature::DISABLED | :anonymous | false
+ :private | ProjectFeature::ENABLED | :maintainer | true
+ :private | ProjectFeature::ENABLED | :developer | true
+ :private | ProjectFeature::ENABLED | :guest | true
+ :private | ProjectFeature::ENABLED | :anonymous | false
+ :private | ProjectFeature::PRIVATE | :maintainer | true
+ :private | ProjectFeature::PRIVATE | :developer | true
+ :private | ProjectFeature::PRIVATE | :guest | true
+ :private | ProjectFeature::PRIVATE | :anonymous | false
+ :private | ProjectFeature::DISABLED | :maintainer | false
+ :private | ProjectFeature::DISABLED | :developer | false
+ :private | ProjectFeature::DISABLED | :guest | false
+ :private | ProjectFeature::DISABLED | :anonymous | false
+ end
+
+ with_them do
+ let(:current_user) { user_subject(role) }
+ let(:project) { project_subject(project_visibility) }
+
+ it 'allows/disallows the abilities based on the infrastructure access level' do
+ project.project_feature.update!(infrastructure_access_level: access_level)
+
+ if allowed
+ expect_allowed(*permissions_abilities(role))
+ else
+ expect_disallowed(*permissions_abilities(role))
+ end
+ end
+ end
+ end
+
describe 'access_security_and_compliance' do
context 'when the "Security & Compliance" is enabled' do
before do
@@ -2791,6 +2884,27 @@ RSpec.describe ProjectPolicy do
end
end
+ describe 'read_code' do
+ let(:current_user) { create(:user) }
+
+ before do
+ allow(subject).to receive(:allowed?).and_call_original
+ allow(subject).to receive(:allowed?).with(:download_code).and_return(can_download_code)
+ end
+
+ context 'when the current_user can download_code' do
+ let(:can_download_code) { true }
+
+ it { expect_allowed(:read_code) }
+ end
+
+ context 'when the current_user cannot download_code' do
+ let(:can_download_code) { false }
+
+ it { expect_disallowed(:read_code) }
+ end
+ end
+
private
def project_subject(project_type)
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index b800e7dbc43..d02a94b810e 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -84,6 +84,50 @@ RSpec.describe UserPolicy do
end
end
+ describe "reading a user's associations count" do
+ context 'when current_user is not an admin' do
+ context 'fetching their own data' do
+ subject { described_class.new(current_user, current_user) }
+
+ context 'when current_user is not blocked' do
+ it { is_expected.to be_allowed(:get_user_associations_count ) }
+ end
+
+ context 'when current_user is blocked' do
+ let(:current_user) { create(:user, :blocked) }
+
+ it { is_expected.not_to be_allowed(:get_user_associations_count) }
+ end
+ end
+
+ context "fetching a different user's data" do
+ it { is_expected.not_to be_allowed(:get_user_associations_count) }
+ end
+ end
+
+ context 'when current_user is an admin' do
+ let(:current_user) { admin }
+
+ context 'fetching their own data', :enable_admin_mode do
+ subject { described_class.new(current_user, current_user) }
+
+ context 'when current_user is not blocked' do
+ it { is_expected.to be_allowed(:get_user_associations_count ) }
+ end
+
+ context 'when current_user is blocked' do
+ let(:current_user) { create(:admin, :blocked) }
+
+ it { is_expected.not_to be_allowed(:get_user_associations_count) }
+ end
+ end
+
+ context "fetching a different user's data", :enable_admin_mode do
+ it { is_expected.to be_allowed(:get_user_associations_count) }
+ end
+ end
+ end
+
shared_examples 'changing a user' do |ability|
context "when a regular user tries to destroy another regular user" do
it { is_expected.not_to be_allowed(ability) }
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index 498b2a32a0e..88dafb7ea1f 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -252,6 +252,18 @@ RSpec.describe BlobPresenter do
end
end
+ describe '#highlight_and_trim' do
+ let(:git_blob) { blob.__getobj__ }
+
+ it 'returns trimmed content for longer line' do
+ trimmed_lines = git_blob.data.split("\n").map { |line| line[0, 55] }.join("\n")
+
+ expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "#{trimmed_lines}\n", plain: nil, language: 'ruby', context: { ellipsis_svg: "svg_icon", ellipsis_indexes: [21, 26, 49] })
+
+ presenter.highlight_and_trim(ellipsis_svg: "svg_icon", trim_length: 55)
+ end
+ end
+
describe '#blob_language' do
subject { presenter.blob_language }
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index 396fe7843ba..952de121cc4 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -325,7 +325,7 @@ RSpec.describe Ci::BuildRunnerPresenter do
is_expected.to eq(presenter.variables.to_runner_variables)
end
- context 'when there are variables to expand' do
+ context 'when there is a file variable to expand' do
before_all do
create(:ci_variable, project: project,
key: 'regular_var',
@@ -353,23 +353,50 @@ RSpec.describe Ci::BuildRunnerPresenter do
it 'logs file_variable_is_referenced_in_another_variable' do
expect(Gitlab::AppJsonLogger).to receive(:info).with(
event: 'file_variable_is_referenced_in_another_variable',
- project_id: project.id
+ project_id: project.id,
+ variable: 'file_var'
).once
runner_variables
end
+ end
+
+ context 'when there is a raw variable to expand' do
+ before_all do
+ create(:ci_variable, project: project,
+ key: 'regular_var',
+ value: 'value 1')
+ create(:ci_variable, project: project,
+ key: 'raw_var',
+ value: 'value 2',
+ raw: true)
+ create(:ci_variable, project: project,
+ key: 'var_with_variables',
+ value: 'value 3 and $regular_var and $raw_var and $undefined_var')
+ end
+
+ it 'returns expanded variables without expanding raws' do
+ expect(runner_variables).to include(
+ { key: 'regular_var', value: 'value 1',
+ public: false, masked: false },
+ { key: 'raw_var', value: 'value 2',
+ public: false, masked: false, raw: true },
+ { key: 'var_with_variables', value: 'value 3 and value 1 and $raw_var and $undefined_var',
+ public: false, masked: false }
+ )
+ end
- context 'when the FF ci_stop_expanding_file_vars_for_runners is disabled' do
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
before do
- stub_feature_flags(ci_stop_expanding_file_vars_for_runners: false)
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
end
- it 'returns variables with expanded' do
+ it 'returns expanded variables' do
expect(runner_variables).to include(
{ key: 'regular_var', value: 'value 1',
public: false, masked: false },
- { key: 'file_var', value: 'value 2',
- public: false, masked: false, file: true },
+ { key: 'raw_var', value: 'value 2',
+ public: false, masked: false, raw: true },
{ key: 'var_with_variables', value: 'value 3 and value 1 and value 2 and $undefined_var',
public: false, masked: false }
)
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index 4539c3d06f6..7f4c8120e17 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -90,12 +90,12 @@ RSpec.describe Ci::PipelinePresenter do
end
end
- describe '#name' do
+ describe '#event_type_name' do
before do
allow(pipeline).to receive(:merge_request_event_type) { event_type }
end
- subject { presenter.name }
+ subject { presenter.event_type_name }
context 'for a detached merge request pipeline' do
let(:event_type) { :detached }
diff --git a/spec/presenters/deployments/deployment_presenter_spec.rb b/spec/presenters/deployments/deployment_presenter_spec.rb
index 689451677f4..51da7b9fd12 100644
--- a/spec/presenters/deployments/deployment_presenter_spec.rb
+++ b/spec/presenters/deployments/deployment_presenter_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Deployments::DeploymentPresenter do
describe '#tags' do
it do
- expect(deployment).to receive(:tags).and_return(['test'])
+ expect(deployment).to receive(:tags).and_return(['refs/tags/test'])
expect(presenter.tags).to eq([{ name: 'test', path: 'tags/test' }])
end
end
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
index e17ae218cd3..df43b0279dd 100644
--- a/spec/presenters/issue_presenter_spec.rb
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -26,20 +26,20 @@ RSpec.describe IssuePresenter do
context 'when issue type is task' do
let(:presented_issue) { task }
- context 'when work_items feature flag is enabled' do
+ context 'when use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
it 'returns a work item url for the task' do
expect(presenter.web_url).to eq(project_work_items_url(project, work_items_path: presented_issue.id))
end
end
- context 'when work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'returns an issue url for the task' do
- expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
- end
+ it 'returns a work item url using iid for the task' do
+ expect(presenter.web_url).to eq(
+ project_work_items_url(project, work_items_path: presented_issue.iid, iid_path: true)
+ )
end
end
end
@@ -66,20 +66,20 @@ RSpec.describe IssuePresenter do
context 'when issue type is task' do
let(:presented_issue) { task }
- context 'when work_items feature flag is enabled' do
+ context 'when use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
it 'returns a work item path for the task' do
expect(presenter.issue_path).to eq(project_work_items_path(project, work_items_path: presented_issue.id))
end
end
- context 'when work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'returns an issue path for the task' do
- expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
- end
+ it 'returns a work item path using iid for the task' do
+ expect(presenter.issue_path).to eq(
+ project_work_items_path(project, work_items_path: presented_issue.iid, iid_path: true)
+ )
end
end
end
diff --git a/spec/presenters/packages/npm/package_presenter_spec.rb b/spec/presenters/packages/npm/package_presenter_spec.rb
index 8b99e6d8605..4fa469c7cd2 100644
--- a/spec/presenters/packages/npm/package_presenter_spec.rb
+++ b/spec/presenters/packages/npm/package_presenter_spec.rb
@@ -86,10 +86,10 @@ RSpec.describe ::Packages::Npm::PackagePresenter do
it 'avoids N+1 database queries' do
check_n_plus_one(:versions) do
create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package|
- if has_dependencies
- ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
- create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type)
- end
+ next unless has_dependencies
+
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ create(:packages_dependency_link, package: npm_package, dependency_type: dependency_type)
end
end
end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 832deee6186..c32cc87afbb 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe ProjectPresenter do
expect(release).to be_truthy
expect(presenter.releases_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including("#{project.releases.count}"),
+ label: a_string_including(project.releases.count.to_s),
link: presenter.project_releases_path(project)
)
end
@@ -295,7 +295,7 @@ RSpec.describe ProjectPresenter do
expect(release).to be_truthy
expect(presenter.releases_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including("#{project.releases.count}"),
+ label: a_string_including(project.releases.count.to_s),
link: presenter.project_releases_path(project)
)
end
@@ -329,7 +329,7 @@ RSpec.describe ProjectPresenter do
it 'returns branches data' do
expect(presenter.branches_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including("#{project.repository.branches.size}"),
+ label: a_string_including(project.repository.branches.size.to_s),
link: presenter.project_branches_path(project)
)
end
@@ -339,7 +339,7 @@ RSpec.describe ProjectPresenter do
it 'returns tags data' do
expect(presenter.tags_anchor_data).to have_attributes(
is_link: true,
- label: a_string_including("#{project.repository.tags.size}"),
+ label: a_string_including(project.repository.tags.size.to_s),
link: presenter.project_tags_path(project)
)
end
diff --git a/spec/rack_servers/configs/config.ru b/spec/rack_servers/configs/config.ru
index 63daeb9eec5..c832ee9b6ff 100644
--- a/spec/rack_servers/configs/config.ru
+++ b/spec/rack_servers/configs/config.ru
@@ -2,7 +2,7 @@
app = proc do |env|
if env['REQUEST_METHOD'] == 'GET'
- [200, {}, ["#{Process.pid}"]]
+ [200, {}, [Process.pid.to_s]]
else
Process.kill(env['QUERY_STRING'], Process.pid)
[200, {}, ['Bye!']]
diff --git a/spec/requests/admin/broadcast_messages_controller_spec.rb b/spec/requests/admin/broadcast_messages_controller_spec.rb
index 9101370d42d..eb29092845c 100644
--- a/spec/requests/admin/broadcast_messages_controller_spec.rb
+++ b/spec/requests/admin/broadcast_messages_controller_spec.rb
@@ -3,10 +3,25 @@
require 'spec_helper'
RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode do
+ let(:broadcast_message) { build(:broadcast_message) }
+ let(:broadcast_message_params) { broadcast_message.as_json(root: true, only: [:message, :starts_at, :ends_at]) }
+
+ let_it_be(:invalid_broadcast_message) { { broadcast_message: { message: '' } } }
+ let_it_be(:test_message) { 'you owe me a new acorn' }
+
before do
sign_in(create(:admin))
end
+ describe 'GET #index' do
+ it 'renders index template' do
+ get admin_broadcast_messages_path
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to render_template(:index)
+ end
+ end
+
describe 'POST /preview' do
it 'renders preview partial' do
post preview_admin_broadcast_messages_path, params: { broadcast_message: { message: "Hello, world!" } }
@@ -15,4 +30,78 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode do
expect(response.body).to render_template(:_preview)
end
end
+
+ describe 'POST #create' do
+ context 'when format json' do
+ it 'persists the message and returns ok on success' do
+ post admin_broadcast_messages_path(format: :json), params: broadcast_message_params
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Json.parse(response.body)['message']).to eq(broadcast_message.message)
+ end
+
+ it 'does not persist the message on failure' do
+ post admin_broadcast_messages_path(format: :json), params: invalid_broadcast_message
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::Json.parse(response.body)['errors']).to be_present
+ end
+ end
+
+ context 'when format html' do
+ it 'persists the message and redirects to broadcast_messages on success' do
+ post admin_broadcast_messages_path(format: :html), params: broadcast_message_params
+ expect(response).to redirect_to(admin_broadcast_messages_path)
+ end
+
+ it 'does not persist and renders the index page on failure' do
+ post admin_broadcast_messages_path(format: :html), params: invalid_broadcast_message
+ expect(response.body).to render_template(:index)
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ context 'when format json' do
+ it 'persists the message and returns ok on success' do
+ broadcast_message.save!
+ patch admin_broadcast_message_path(format: :json, id: broadcast_message.id), params: {
+ broadcast_message: { message: test_message }
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Gitlab::Json.parse(response.body)['message']).to eq(test_message)
+ end
+
+ it 'does not persist the message on failure' do
+ broadcast_message.message = test_message
+ broadcast_message.save!
+ patch admin_broadcast_message_path(format: :json, id: broadcast_message.id), params: {
+ broadcast_message: { message: '' }
+ }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(Gitlab::Json.parse(response.body)['errors']).to be_present
+ end
+ end
+
+ context 'when format html' do
+ it 'persists the message and redirects to broadcast_messages on success' do
+ broadcast_message.save!
+ patch admin_broadcast_message_path(id: broadcast_message.id), params: {
+ broadcast_message: { message: test_message }
+ }
+
+ expect(response).to redirect_to(admin_broadcast_messages_path)
+ end
+
+ it 'does not persist and renders the edit page on failure' do
+ broadcast_message.message = test_message
+ broadcast_message.save!
+ patch admin_broadcast_message_path(id: broadcast_message.id), params: {
+ **invalid_broadcast_message
+ }
+
+ expect(response.body).to render_template(:edit)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb
index f89964411f8..4bdc44cb583 100644
--- a/spec/requests/api/admin/ci/variables_spec.rb
+++ b/spec/requests/api/admin/ci/variables_spec.rb
@@ -71,7 +71,8 @@ RSpec.describe ::API::Admin::Ci::Variables do
key: 'TEST_VARIABLE_2',
value: 'PROTECTED_VALUE_2',
protected: true,
- masked: true
+ masked: true,
+ raw: true
}
end.to change { ::Ci::InstanceVariable.count }.by(1)
@@ -80,9 +81,19 @@ RSpec.describe ::API::Admin::Ci::Variables do
expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
expect(json_response['masked']).to be_truthy
+ expect(json_response['raw']).to be_truthy
expect(json_response['variable_type']).to eq('env_var')
end
+ it 'masks the new value when logging' do
+ masked_params = { 'key' => 'VAR_KEY', 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
+
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+
+ post api("/admin/ci/variables", user),
+ params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
+ end
+
it 'creates variable with optional attributes', :aggregate_failures do
expect do
post api('/admin/ci/variables', admin),
@@ -98,6 +109,7 @@ RSpec.describe ::API::Admin::Ci::Variables do
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
expect(json_response['masked']).to be_falsey
+ expect(json_response['raw']).to be_falsey
expect(json_response['variable_type']).to eq('file')
end
@@ -153,7 +165,8 @@ RSpec.describe ::API::Admin::Ci::Variables do
variable_type: 'file',
value: 'VALUE_1_UP',
protected: true,
- masked: true
+ masked: true,
+ raw: true
}
expect(response).to have_gitlab_http_status(:ok)
@@ -161,6 +174,16 @@ RSpec.describe ::API::Admin::Ci::Variables do
expect(variable.reload).to be_protected
expect(json_response['variable_type']).to eq('file')
expect(json_response['masked']).to be_truthy
+ expect(json_response['raw']).to be_truthy
+ end
+
+ it 'masks the new value when logging' do
+ masked_params = { 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
+
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+
+ put api("/admin/ci/variables/#{variable.key}", admin),
+ params: { value: 'SENSITIVE', protected: true, masked: true }
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/api/alert_management_alerts_spec.rb b/spec/requests/api/alert_management_alerts_spec.rb
index 99293e5ae95..680a3883387 100644
--- a/spec/requests/api/alert_management_alerts_spec.rb
+++ b/spec/requests/api/alert_management_alerts_spec.rb
@@ -140,7 +140,7 @@ RSpec.describe API::AlertManagementAlerts do
project.send("add_#{user_role}", user)
end
- it_behaves_like "#{params[:expected_status]}"
+ it_behaves_like params[:expected_status].to_s
end
context 'file size too large' do
@@ -245,7 +245,7 @@ RSpec.describe API::AlertManagementAlerts do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) unless public_project
end
- it_behaves_like "#{params[:expected_status]}"
+ it_behaves_like params[:expected_status].to_s
end
end
@@ -293,7 +293,7 @@ RSpec.describe API::AlertManagementAlerts do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) unless public_project
end
- it_behaves_like "#{params[:expected_status]}"
+ it_behaves_like params[:expected_status].to_s
end
context 'when user has access' do
@@ -368,7 +368,7 @@ RSpec.describe API::AlertManagementAlerts do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) unless public_project
end
- it_behaves_like "#{params[:expected_status]}"
+ it_behaves_like params[:expected_status].to_s
end
context 'when user has access' do
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 817e1324c7c..4d7256a1f03 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe API::Boards do
let_it_be(:non_member) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
- let_it_be(:board_parent, reload: true) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let_it_be(:board_parent, reload: true) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let_it_be(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: board_parent)
@@ -97,7 +97,7 @@ RSpec.describe API::Boards do
describe "POST /groups/:id/boards/:board_id/lists" do
let_it_be(:group) { create(:group) }
- let_it_be(:board_parent) { create(:group, parent: group ) }
+ let_it_be(:board_parent) { create(:group, parent: group) }
let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" }
let_it_be(:board) { create(:board, group: board_parent) }
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index 2bf242f06ed..da9eb6b2216 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -389,8 +389,7 @@ RSpec.describe API::Ci::JobArtifacts do
end
end
- context 'when Google CDN is enabled' do
- let(:cdn_enabled) { true }
+ context 'when Google CDN is configured' do
let(:cdn_config) do
{
'provider' => 'Google',
@@ -401,7 +400,6 @@ RSpec.describe API::Ci::JobArtifacts do
end
before do
- stub_feature_flags(ci_job_artifacts_cdn: cdn_enabled)
stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
uploader: JobArtifactUploader,
proxy_download: proxy_download,
@@ -418,18 +416,6 @@ RSpec.describe API::Ci::JobArtifacts do
expect(response.redirect_url).to start_with("https://cdn.example.org/#{artifact.file.path}")
end
-
- context 'when ci_job_artifacts_cdn feature flag is disabled' do
- let(:cdn_enabled) { false }
-
- it 'returns the file remote URL' do
- expect(Gitlab::ApplicationContext).to receive(:push).with(artifact_used_cdn: false).and_call_original
-
- subject
-
- expect(response).to redirect_to(artifact.file.url)
- end
- end
end
context 'authorized user' do
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 0e17db516f4..c1b7461f444 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -606,6 +606,32 @@ RSpec.describe API::Ci::Jobs do
end
end
end
+
+ context 'when ci_debug_services is set to true' do
+ before_all do
+ create(:ci_instance_variable, key: 'CI_DEBUG_SERVICES', value: true)
+ end
+
+ where(:public_builds, :user_project_role, :expected_status) do
+ true | 'developer' | :ok
+ true | 'guest' | :forbidden
+ false | 'developer' | :ok
+ false | 'guest' | :forbidden
+ end
+
+ with_them do
+ before do
+ project.update!(public_builds: public_builds)
+ project.add_role(user, user_project_role)
+
+ get api("/projects/#{project.id}/jobs/#{job.id}/trace", api_user)
+ end
+
+ it 'renders successfully to authorized users' do
+ expect(response).to have_gitlab_http_status(expected_status)
+ end
+ end
+ end
end
describe 'POST /projects/:id/jobs/:job_id/cancel' do
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 697fe16e222..c9d06f37c8b 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -940,7 +940,12 @@ RSpec.describe API::Ci::Pipelines do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" })
+ expect(json_response).to contain_exactly({
+ "variable_type" => "env_var",
+ "key" => "foo",
+ "value" => "bar",
+ "raw" => false
+ })
end
end
end
@@ -961,7 +966,12 @@ RSpec.describe API::Ci::Pipelines do
subject
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" })
+ expect(json_response).to contain_exactly({
+ "variable_type" => "env_var",
+ "key" => "foo",
+ "value" => "bar",
+ "raw" => false
+ })
end
end
diff --git a/spec/requests/api/ci/resource_groups_spec.rb b/spec/requests/api/ci/resource_groups_spec.rb
index 87df71f6096..2a67a3e4322 100644
--- a/spec/requests/api/ci/resource_groups_spec.rb
+++ b/spec/requests/api/ci/resource_groups_spec.rb
@@ -56,6 +56,31 @@ RSpec.describe API::Ci::ResourceGroups do
expect(Time.parse(json_response['updated_at'])).to be_like_time(resource_group.updated_at)
end
+ context 'when resource group key contains multiple dots' do
+ let!(:resource_group) { create(:ci_resource_group, project: project, key: 'test..test') }
+
+ it 'returns the resource group', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(resource_group.id)
+ expect(json_response['key']).to eq(resource_group.key)
+ end
+ end
+
+ context 'when resource group key contains a slash' do
+ let!(:resource_group) { create(:ci_resource_group, project: project, key: 'test/test') }
+ let(:key) { 'test%2Ftest' }
+
+ it 'returns the resource group', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(resource_group.id)
+ expect(json_response['key']).to eq(resource_group.key)
+ end
+ end
+
context 'when user is reporter' do
let(:user) { reporter }
@@ -98,6 +123,25 @@ RSpec.describe API::Ci::ResourceGroups do
expect(json_response[0]['status']).to eq(upcoming_processable.status)
end
+ context 'when resource group key contains a slash' do
+ let_it_be(:resource_group) { create(:ci_resource_group, project: project, key: 'test/test') }
+ let_it_be(:upcoming_processable) do
+ create(:ci_processable,
+ :waiting_for_resource,
+ resource_group: resource_group)
+ end
+
+ let(:key) { 'test%2Ftest' }
+
+ it 'returns the resource group', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response[0]['id']).to eq(upcoming_processable.id)
+ expect(json_response[0]['name']).to eq(upcoming_processable.name)
+ end
+ end
+
context 'when user is reporter' do
let(:user) { reporter }
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index cd8c3dd2806..9af0541bd2c 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -238,7 +238,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'authorization token is invalid' do
it 'responds with forbidden' do
- authorize_artifacts(token: 'invalid', filesize: 100 )
+ authorize_artifacts(token: 'invalid', filesize: 100)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -881,11 +881,11 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
- shared_examples 'forbidden request' do
- it 'responds with forbidden' do
+ shared_examples 'unauthorized request' do
+ it 'responds with unauthorized' do
download_artifact
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
@@ -899,7 +899,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
job.success!
end
- it_behaves_like 'successful artifact download'
+ it_behaves_like 'unauthorized request'
end
end
@@ -916,7 +916,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
dependent_job.success!
end
- it_behaves_like 'forbidden request'
+ it_behaves_like 'unauthorized request'
end
end
@@ -942,7 +942,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let(:token) { ci_build.token }
- it_behaves_like 'forbidden request'
+ it_behaves_like 'unauthorized request'
end
context 'when using a token from a cross pipeline build' do
@@ -981,19 +981,23 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
let!(:unrelated_ci_build) { create(:ci_build, :running, user: create(:user)) }
let(:token) { unrelated_ci_build.token }
- it_behaves_like 'forbidden request'
+ it 'responds with forbidden' do
+ download_artifact
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
context 'when using runnners token' do
let(:token) { job.project.runners_token }
- it_behaves_like 'forbidden request'
+ it_behaves_like 'unauthorized request'
end
context 'when using an invalid token' do
let(:token) { 'invalid-token' }
- it_behaves_like 'forbidden request'
+ it_behaves_like 'unauthorized request'
end
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 d4f734e7bdd..1cb4cc93ea5 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -462,7 +462,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state 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' } )
+ expect(runner.reload.config).to eq({ 'gpus' => 'all' })
end
it "sets the runner's ip_address" do
diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
index e1dc347f8dd..b8e4370fd46 100644
--- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb
+++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
@@ -118,7 +118,7 @@ RSpec.describe API::Ci::Runners do
end
include_context 'when authorized', 'group' do
- let_it_be(:user) { create_default(:group_member, :owner, user: create(:user), group: group ).user }
+ let_it_be(:user) { create_default(:group_member, :owner, user: create(:user), group: group).user }
def get_token
group.reload.runners_token
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index 69f26d3f257..dd9894f2972 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -399,7 +399,7 @@ RSpec.describe API::Ci::Runners do
it 'unrelated runner attribute on an existing runner with too many tags' do
# This test ensures that it is possible to update any attribute on a runner that currently fails the
# validation that ensures that there aren't too many tags associated with a runner
- existing_invalid_shared_runner = build(:ci_runner, :instance, tag_list: (1..::Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } )
+ existing_invalid_shared_runner = build(:ci_runner, :instance, tag_list: (1..::Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" })
existing_invalid_shared_runner.save!(validate: false)
active = existing_invalid_shared_runner.active
diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb
index 0b8116d5e20..b0bca6e9125 100644
--- a/spec/requests/api/ci/secure_files_spec.rb
+++ b/spec/requests/api/ci/secure_files_spec.rb
@@ -143,6 +143,18 @@ RSpec.describe API::Ci::SecureFiles do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(secure_file.name)
+ expect(json_response['expires_at']).to be nil
+ expect(json_response['metadata']).to be nil
+ end
+
+ it 'returns project secure file details with metadata when supported' do
+ secure_file_with_metadata = create(:ci_secure_file_with_metadata, project: project)
+ get api("/projects/#{project.id}/secure_files/#{secure_file_with_metadata.id}", maintainer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(secure_file_with_metadata.name)
+ expect(json_response['expires_at']).to eq('2022-04-26T19:20:40.000Z')
+ expect(json_response['metadata'].keys).to match_array(%w[id issuer subject expires_at])
end
it 'responds with 404 Not Found if requesting non-existing secure file' do
diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb
index 953dcb8a483..f9b7880a4c4 100644
--- a/spec/requests/api/ci/triggers_spec.rb
+++ b/spec/requests/api/ci/triggers_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe API::Ci::Triggers do
end
it 'validates variables needs to be a map of key-valued strings' do
- post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { key: %w(1 2) }, ref: 'master')
+ post api("/projects/#{project.id}/trigger/pipeline"), params: options.merge(variables: { 'TRIGGER_KEY' => %w(1 2) }, ref: 'master')
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb
index 74ed8c1551d..cafb841995d 100644
--- a/spec/requests/api/ci/variables_spec.rb
+++ b/spec/requests/api/ci/variables_spec.rb
@@ -46,6 +46,7 @@ RSpec.describe API::Ci::Variables do
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
expect(json_response['masked']).to eq(variable.masked?)
+ expect(json_response['raw']).to eq(variable.raw?)
expect(json_response['variable_type']).to eq('env_var')
end
@@ -115,7 +116,7 @@ RSpec.describe API::Ci::Variables do
context 'authorized user with proper permissions' do
it 'creates variable' do
expect do
- post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true }
+ post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true, raw: true }
end.to change { project.variables.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -123,12 +124,22 @@ RSpec.describe API::Ci::Variables do
expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
expect(json_response['masked']).to be_truthy
+ expect(json_response['raw']).to be_truthy
expect(json_response['variable_type']).to eq('env_var')
end
+ it 'masks the new value when logging' do
+ masked_params = { 'key' => 'VAR_KEY', 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
+
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+
+ post api("/projects/#{project.id}/variables", user),
+ params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
+ end
+
it 'creates variable with optional attributes' do
expect do
- post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
end.to change { project.variables.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -136,6 +147,7 @@ RSpec.describe API::Ci::Variables do
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
expect(json_response['masked']).to be_falsey
+ expect(json_response['raw']).to be_falsey
expect(json_response['variable_type']).to eq('file')
end
@@ -206,6 +218,15 @@ RSpec.describe API::Ci::Variables do
expect(updated_variable.variable_type).to eq('file')
end
+ it 'masks the new value when logging' do
+ masked_params = { 'value' => '[FILTERED]', 'protected' => 'true' }
+
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+
+ put api("/projects/#{project.id}/variables/#{variable.key}", user),
+ params: { value: 'SENSITIVE', protected: true }
+ end
+
it 'responds with 404 Not Found if requesting non-existing variable' do
put api("/projects/#{project.id}/variables/non_existing_variable", user)
diff --git a/spec/requests/api/clusters/agent_tokens_spec.rb b/spec/requests/api/clusters/agent_tokens_spec.rb
index 2dca21ca6f1..a33bef53b14 100644
--- a/spec/requests/api/clusters/agent_tokens_spec.rb
+++ b/spec/requests/api/clusters/agent_tokens_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Clusters::AgentTokens do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:agent_token_one) { create(:cluster_agent_token, agent: agent) }
- let_it_be(:agent_token_two) { create(:cluster_agent_token, agent: agent) }
+ let_it_be(:revoked_agent_token) { create(:cluster_agent_token, :revoked, agent: agent) }
let_it_be(:project) { agent.project }
let_it_be(:user) { agent.created_by_user }
let_it_be(:unauthorized_user) { create(:user) }
@@ -17,7 +17,7 @@ RSpec.describe API::Clusters::AgentTokens do
describe 'GET /projects/:id/cluster_agents/:agent_id/tokens' do
context 'with authorized user' do
- it 'returns tokens' do
+ it 'returns tokens regardless of status' do
get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user)
aggregate_failures "testing response" do
@@ -27,10 +27,16 @@ RSpec.describe API::Clusters::AgentTokens do
expect(json_response.count).to eq(2)
expect(json_response.first['name']).to eq(agent_token_one.name)
expect(json_response.first['agent_id']).to eq(agent.id)
- expect(json_response.second['name']).to eq(agent_token_two.name)
+ expect(json_response.second['name']).to eq(revoked_agent_token.name)
expect(json_response.second['agent_id']).to eq(agent.id)
end
end
+
+ it 'returns a not_found error if agent_id does not exist' do
+ get api("/projects/#{project.id}/cluster_agents/#{non_existing_record_id}/tokens", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
context 'with unauthorized user' do
diff --git a/spec/requests/api/dependency_proxy_spec.rb b/spec/requests/api/dependency_proxy_spec.rb
index a8617fcb0bf..7af4ed08cb8 100644
--- a/spec/requests/api/dependency_proxy_spec.rb
+++ b/spec/requests/api/dependency_proxy_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::DependencyProxy, api: true do
let_it_be(:user) { create(:user) }
- let_it_be(:blob) { create(:dependency_proxy_blob ) }
+ let_it_be(:blob) { create(:dependency_proxy_blob) }
let_it_be(:group, reload: true) { blob.group }
before do
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index 24e0e5d3180..8124080abea 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -201,7 +201,7 @@ RSpec.describe API::Deployments do
}
)
- expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
it 'links any merged merge requests to the deployment', :sidekiq_inline do
@@ -325,7 +325,7 @@ RSpec.describe API::Deployments do
context 'as non member' do
it 'returns a 404 status code' do
post(
- api( "/projects/#{project.id}/deployments", non_member),
+ api("/projects/#{project.id}/deployments", non_member),
params: {
environment: 'production',
sha: '123',
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index d0334cf6dd2..85dafef569d 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -193,7 +193,7 @@ RSpec.describe API::Features, stub_feature_flags: false do
'state' => 'conditional',
'gates' => [
{ 'key' => 'boolean', 'value' => false },
- { 'key' => 'actors', 'value' => ["#{actor.class}:#{actor.id}"] }
+ { 'key' => 'actors', 'value' => [actor.flipper_id] }
],
'definition' => known_feature_flag_definition_hash
)
@@ -269,6 +269,20 @@ RSpec.describe API::Features, stub_feature_flags: false do
end
end
+ context 'when enabling for a repository by path' do
+ context 'when the repository exists' do
+ it_behaves_like 'enables the flag for the actor', :repository do
+ let_it_be(:actor) { create(:project).repository }
+ end
+ end
+
+ context 'when the repository does not exist' do
+ it_behaves_like 'does not enable the flag', :repository do
+ let(:actor_path) { 'not/a/repository' }
+ end
+ end
+ end
+
context 'with multiple users' do
let_it_be(:users) { create_list(:user, 3) }
@@ -361,6 +375,29 @@ RSpec.describe API::Features, stub_feature_flags: false do
end
end
+ context 'with multiple repository' do
+ let_it_be(:projects) { create_list(:project, 3) }
+
+ it_behaves_like 'creates an enabled feature for the specified entries' do
+ let(:gate_params) { { repository: projects.map { |p| p.repository.full_path }.join(',') } }
+ let(:expected_gate_params) { projects.map { |p| p.repository.flipper_id } }
+ end
+
+ context 'when empty value exists between comma' do
+ it_behaves_like 'creates an enabled feature for the specified entries' do
+ let(:gate_params) { { repository: "#{projects.first.repository.full_path},,,," } }
+ let(:expected_gate_params) { projects.first.repository.flipper_id }
+ end
+ end
+
+ context 'when one of the projects does not exist' do
+ it_behaves_like 'does not enable the flag', :project do
+ let(:actor_path) { "#{projects.first.repository.full_path},inexistent-entry" }
+ let(:expected_inexistent_path) { "inexistent-entry" }
+ end
+ end
+ end
+
it 'creates a feature with the given percentage of time if passed an integer' do
post api("/features/#{feature_name}", admin), params: { value: '50' }
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index e95a626b4aa..d4d3aace204 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe API::Files do
let_it_be(:inherited_reporter) { create(:user) }
let_it_be(:inherited_developer) { create(:user) }
- let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+ let!(:project) { create(:project, :repository, namespace: user.namespace) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
let(:file_path) { 'files%2Fruby%2Fpopen%2Erb' }
let(:file_name) { 'popen.rb' }
@@ -935,7 +935,7 @@ RSpec.describe API::Files do
end
context 'and the repo is empty' do
- let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
+ let!(:project) { create(:project_empty_repo, namespace: user.namespace) }
it_behaves_like 'creates a new file in the project repo' do
let(:current_user) { user }
@@ -1253,4 +1253,35 @@ RSpec.describe API::Files do
expect(json_response['content']).to eq(put_params[:content])
end
end
+
+ describe 'POST /projects/:id/repository/files with text encoding' do
+ let(:file_path) { 'test%2Etext' }
+ let(:put_params) do
+ {
+ branch: 'master',
+ content: 'test',
+ commit_message: 'Text file',
+ encoding: 'text'
+ }
+ end
+
+ let(:get_params) do
+ {
+ ref: 'master'
+ }
+ end
+
+ before do
+ post api(route(file_path), user), params: put_params
+ end
+
+ it 'returns base64-encoded text file' do
+ get api(route(file_path), user), params: get_params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ expect(json_response['file_name']).to eq(CGI.unescape(file_path))
+ expect(Base64.decode64(json_response['content'])).to eq("test")
+ end
+ end
end
diff --git a/spec/requests/api/go_proxy_spec.rb b/spec/requests/api/go_proxy_spec.rb
index 7c44fddc303..5498ed6df13 100644
--- a/spec/requests/api/go_proxy_spec.rb
+++ b/spec/requests/api/go_proxy_spec.rb
@@ -16,12 +16,12 @@ RSpec.describe API::GoProxy do
let_it_be(:modules) do
commits = [
- create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } ),
- create(:go_module_commit, :module, project: project, tag: 'v1.0.1' ),
- create(:go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' ),
- create(:go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' ),
- create(:go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" } ),
- create(:go_module_commit, :module, project: project, name: 'v2' ),
+ create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' }),
+ create(:go_module_commit, :module, project: project, tag: 'v1.0.1'),
+ create(:go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg'),
+ create(:go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod'),
+ create(:go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" }),
+ create(:go_module_commit, :module, project: project, name: 'v2'),
create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" })
]
@@ -288,10 +288,10 @@ RSpec.describe API::GoProxy do
let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" }
let_it_be(:modules) do
- create(:go_module_commit, :files, project: project, files: { 'a.go' => "package\a" } )
+ create(:go_module_commit, :files, project: project, files: { 'a.go' => "package\a" })
create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'go.mod' => "module not/a/real/module\n" })
- create(:go_module_commit, :files, project: project, files: { 'v2/a.go' => "package a\n" } )
- create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/go.mod' => "module #{base}\n" } )
+ create(:go_module_commit, :files, project: project, files: { 'v2/a.go' => "package a\n" })
+ create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/go.mod' => "module #{base}\n" })
end
describe 'GET /projects/:id/packages/go/*module_name/@v/list' do
@@ -406,6 +406,19 @@ RSpec.describe API::GoProxy do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ context 'with access to package registry for everyone' do
+ let_it_be(:user) { nil }
+
+ before do
+ project.reload.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it_behaves_like 'a module version list resource', 'v1.0.1', 'v1.0.2', 'v1.0.3'
+ it_behaves_like 'a module version information resource', 'v1.0.1'
+ it_behaves_like 'a module file resource', 'v1.0.1'
+ it_behaves_like 'a module archive resource', 'v1.0.1', ['README.md', 'go.mod', 'a.go']
+ end
end
context 'with a public project' do
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
index 484ddc3469b..9bed720c815 100644
--- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'get board lists' do
let_it_be(:user) { create(:user) }
let_it_be(:unauth_user) { create(:user) }
- let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
+ let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:project_label) { create(:label, project: project, name: 'Development') }
let_it_be(:project_label2) { create(:label, project: project, name: 'Testing') }
@@ -21,6 +21,7 @@ RSpec.describe 'get board lists' do
let(:board_data) { graphql_data[board_parent_type]['boards']['nodes'][0] }
let(:lists_data) { board_data['lists']['nodes'][0] }
let(:issues_data) { lists_data['issues']['nodes'] }
+ let(:issue_params) { { filters: { label_name: label2.title, confidential: confidential }, first: 3 } }
def query(list_params = params)
graphql_query_for(
@@ -31,7 +32,7 @@ RSpec.describe 'get board lists' do
nodes {
lists {
nodes {
- issues(filters: {labelName: "#{label2.title}", confidential: #{confidential}}, first: 3) {
+ issues(#{attributes_to_graphql(issue_params)}) {
count
nodes {
#{all_graphql_fields_for('issues'.classify)}
@@ -77,18 +78,23 @@ RSpec.describe 'get board lists' do
end
context 'when user can read the board' do
- before do
+ before_all do
board_parent.add_reporter(user)
- post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user)
end
+ subject { post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) }
+
it 'can access the issues', :aggregate_failures do
+ subject
+
# ties for relative positions are broken by id in ascending order by default
expect(issue_titles).to eq([issue2.title, issue1.title, issue3.title])
expect(issue_relative_positions).not_to include(nil)
end
it 'does not set the relative positions of the issues not being returned', :aggregate_failures do
+ subject
+
expect(issue_id).not_to include(issue6.id)
expect(issue3.relative_position).to be_nil
end
@@ -97,10 +103,36 @@ RSpec.describe 'get board lists' do
let(:confidential) { true }
it 'returns matching issue' do
+ subject
+
expect(issue_titles).to match_array([issue7.title])
expect(issue_relative_positions).not_to include(nil)
end
end
+
+ context 'when filtering by a unioned argument' do
+ let(:another_user) { create(:user) }
+ let(:issue_params) { { filters: { or: { assignee_usernames: [user.username, another_user.username] } } } }
+
+ it 'returns correctly filtered issues' do
+ issue1.assignee_ids = user.id
+ issue2.assignee_ids = another_user.id
+
+ subject
+
+ expect(issue_id).to contain_exactly(issue1.to_gid.to_s, issue2.to_gid.to_s)
+ end
+
+ context 'when feature flag is disabled' do
+ it 'returns an error' do
+ stub_feature_flags(or_issuable_queries: false)
+
+ subject
+
+ expect_graphql_errors_to_include("'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled.")
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
index 6fe2e41cf35..ad7df5c9344 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe 'get board lists' do
end
shared_examples 'group and project board lists query' do
- let!(:board) { create(:board, resource_parent: board_parent) }
+ let_it_be(:board) { create(:board, resource_parent: board_parent) }
context 'when the user does not have access to the board' do
it 'returns nil' do
@@ -107,16 +107,20 @@ RSpec.describe 'get board lists' do
end
context 'when querying for a single list' do
+ let_it_be(:label_list) { create(:list, board: board, label: label, position: 10) }
+ let_it_be(:issues) do
+ [
+ create(:issue, project: project, labels: [label, label2]),
+ create(:issue, project: project, labels: [label, label2], confidential: true),
+ create(:issue, project: project, labels: [label])
+ ]
+ end
+
before do
board_parent.add_reporter(user)
end
it 'returns the correct list with issue count for matching issue filters' do
- label_list = create(:list, board: board, label: label, position: 10)
- create(:issue, project: project, labels: [label, label2])
- create(:issue, project: project, labels: [label, label2], confidential: true)
- create(:issue, project: project, labels: [label])
-
post_graphql(
query(
id: global_id_of(label_list),
@@ -131,21 +135,56 @@ RSpec.describe 'get board lists' do
expect(list_node['issuesCount']).to eq 1
end
end
+
+ context 'when filtering by a unioned argument' do
+ let_it_be(:another_user) { create(:user) }
+
+ it 'returns correctly filtered issues' do
+ issues[0].assignee_ids = user.id
+ issues[1].assignee_ids = another_user.id
+
+ post_graphql(
+ query(
+ id: global_id_of(label_list),
+ issueFilters: { or: { assignee_usernames: [user.username, another_user.username] } }
+ ), current_user: user
+ )
+
+ expect(lists_data[0]['node']['issuesCount']).to eq 2
+ end
+
+ context 'when feature flag is disabled' do
+ it 'returns an error' do
+ stub_feature_flags(or_issuable_queries: false)
+
+ post_graphql(
+ query(
+ id: global_id_of(label_list),
+ issueFilters: { or: { assignee_usernames: [user.username, another_user.username] } }
+ ), current_user: user
+ )
+
+ expect_graphql_errors_to_include(
+ "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled."
+ )
+ end
+ end
+ end
end
end
describe 'for a project' do
- let(:board_parent) { project }
- let(:label) { project_label }
- let(:label2) { project_label2 }
+ let_it_be(:board_parent) { project }
+ let_it_be(:label) { project_label }
+ let_it_be(:label2) { project_label2 }
it_behaves_like 'group and project board lists query'
end
describe 'for a group' do
- let(:board_parent) { group }
- let(:label) { group_label }
- let(:label2) { group_label2 }
+ let_it_be(:board_parent) { group }
+ let_it_be(:label) { group_label }
+ let_it_be(:label2) { group_label2 }
before do
allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false)
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 47e3221c567..a161c5c98ed 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -109,7 +109,7 @@ RSpec.describe 'Query.project.pipeline' do
'name' => 'docker 1 2',
'needs' => { 'nodes' => [] },
'previousStageJobsOrNeeds' => { 'nodes' => [
- a_hash_including( 'name' => 'my test job' )
+ a_hash_including('name' => 'my test job')
] }
),
a_hash_including(
@@ -129,7 +129,7 @@ RSpec.describe 'Query.project.pipeline' do
'name' => 'rspec 2 2',
'needs' => { 'nodes' => [a_hash_including('name' => 'my test job')] },
'previousStageJobsOrNeeds' => { 'nodes' => [
- a_hash_including('name' => 'my test job' )
+ a_hash_including('name' => 'my test job')
] }
)
)
diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb
index f471a152603..948704e8770 100644
--- a/spec/requests/api/graphql/ci/pipelines_spec.rb
+++ b/spec/requests/api/graphql/ci/pipelines_spec.rb
@@ -419,7 +419,7 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
end
before do
- create(:ci_sources_pipeline, source_pipeline: upstream_pipeline, pipeline: pipeline )
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline, pipeline: pipeline)
post_graphql(query, current_user: user)
end
@@ -441,10 +441,10 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
pipeline_2 = create(:ci_pipeline, project: project, user: user)
upstream_pipeline_2 = create(:ci_pipeline, project: upstream_project, user: user)
- create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_2, pipeline: pipeline_2 )
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_2, pipeline: pipeline_2)
pipeline_3 = create(:ci_pipeline, project: project, user: user)
upstream_pipeline_3 = create(:ci_pipeline, project: upstream_project, user: user)
- create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_3, pipeline: pipeline_3 )
+ create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_3, pipeline: pipeline_3)
expect do
post_graphql(query, current_user: second_user)
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index bd90753f9ad..94c0a3c41bd 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -104,7 +104,8 @@ RSpec.describe 'Query.runner(id)' do
'userPermissions' => {
'readRunner' => true,
'updateRunner' => true,
- 'deleteRunner' => true
+ 'deleteRunner' => true,
+ 'assignRunner' => true
}
)
expect(runner_data['tagList']).to match_array runner.tag_list
diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb
index d6b0673e4f8..35090e2a89f 100644
--- a/spec/requests/api/graphql/group/work_item_types_spec.rb
+++ b/spec/requests/api/graphql/group/work_item_types_spec.rb
@@ -57,15 +57,4 @@ RSpec.describe 'getting a list of work item types for a group' do
expect(graphql_data).to eq('group' => nil)
end
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- post_graphql(query, current_user: current_user)
- end
-
- it 'returns null' do
- expect(graphql_data.dig('group', 'workItemTypes')).to be_nil
- end
- end
end
diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb
new file mode 100644
index 00000000000..8838ad78f72
--- /dev/null
+++ b/spec/requests/api/graphql/issues_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting an issue list at root level' do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:group1) { create(:group).tap { |group| group.add_developer(developer) } }
+ let_it_be(:group2) { create(:group).tap { |group| group.add_developer(developer) } }
+ let_it_be(:project_a) { create(:project, :repository, :public, group: group1) }
+ let_it_be(:project_b) { create(:project, :repository, :private, group: group1) }
+ let_it_be(:project_c) { create(:project, :repository, :public, group: group2) }
+ let_it_be(:project_d) { create(:project, :repository, :private, group: group2) }
+ let_it_be(:early_milestone) { create(:milestone, project: project_d, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, project: project_c, due_date: 30.days.from_now) }
+ let_it_be(:priority1) { create(:label, project: project_c, priority: 1) }
+ let_it_be(:priority2) { create(:label, project: project_d, priority: 5) }
+ let_it_be(:priority3) { create(:label, project: project_a, priority: 10) }
+
+ let_it_be(:issue_a) { create(:issue, project: project_a, labels: [priority3]) }
+ let_it_be(:issue_b) { create(:issue, :with_alert, project: project_b, discussion_locked: true) }
+ let_it_be(:issue_c) do
+ create(
+ :issue,
+ project: project_c,
+ title: 'title matching issue plus',
+ labels: [priority1],
+ milestone: late_milestone
+ )
+ end
+
+ let_it_be(:issue_d) { create(:issue, :with_alert, project: project_d, discussion_locked: true, labels: [priority2]) }
+ let_it_be(:issue_e) { create(:issue, project: project_d, milestone: early_milestone) }
+
+ let(:issue_filter_params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('issues'.classify)}
+ }
+ QUERY
+ end
+
+ before_all do
+ group2.add_reporter(reporter)
+ end
+
+ context 'when the root_level_issues_query feature flag is disabled' do
+ before do
+ stub_feature_flags(root_level_issues_query: false)
+ end
+
+ it 'the field returns null' do
+ post_graphql(query, current_user: developer)
+
+ expect(graphql_data).to eq('issues' => nil)
+ end
+ end
+
+ it_behaves_like 'graphql issue list request spec' do
+ subject(:post_query) { post_graphql(query, current_user: current_user) }
+
+ let(:current_user) { developer }
+ let(:another_user) { reporter }
+ let(:issues_data) { graphql_data['issues']['nodes'] }
+ let(:issue_ids) { graphql_dig_at(issues_data, :id) }
+
+ # filters
+ let(:expected_negated_assignee_issues) { [issue_b, issue_c, issue_d, issue_e] }
+ let(:expected_unioned_assignee_issues) { [issue_a, issue_c] }
+ let(:voted_issues) { [issue_a, issue_c] }
+ let(:no_award_issues) { [issue_b, issue_d, issue_e] }
+ let(:locked_discussion_issues) { [issue_b, issue_d] }
+ let(:unlocked_discussion_issues) { [issue_a, issue_c, issue_e] }
+ let(:search_title_term) { 'matching issue' }
+ let(:title_search_issue) { issue_c }
+
+ # sorting
+ let(:data_path) { [:issues] }
+ let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
+ let(:expected_priority_sorted_asc) { [issue_e, issue_c, issue_d, issue_a, issue_b] }
+ let(:expected_priority_sorted_desc) { [issue_c, issue_e, issue_a, issue_d, issue_b] }
+
+ before_all do
+ issue_a.assignee_ids = developer.id
+ issue_c.assignee_ids = reporter.id
+
+ create(:award_emoji, :upvote, user: developer, awardable: issue_a)
+ create(:award_emoji, :upvote, user: developer, awardable: issue_c)
+
+ # severity sorting
+ create(:issuable_severity, issue: issue_a, severity: :unknown)
+ create(:issuable_severity, issue: issue_b, severity: :low)
+ create(:issuable_severity, issue: issue_d, severity: :critical)
+ create(:issuable_severity, issue: issue_e, severity: :high)
+ end
+
+ def pagination_query(params)
+ graphql_query_for(
+ :issues,
+ params,
+ "#{page_info} nodes { id }"
+ )
+ end
+ end
+
+ def query(params = issue_filter_params)
+ graphql_query_for(
+ :issues,
+ params,
+ fields
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb
index 840bd7c018c..435e1b5b596 100644
--- a/spec/requests/api/graphql/metadata_query_spec.rb
+++ b/spec/requests/api/graphql/metadata_query_spec.rb
@@ -17,7 +17,8 @@ RSpec.describe 'getting project information' do
'enabled' => Gitlab::Kas.enabled?,
'version' => expected_kas_version,
'externalUrl' => expected_kas_external_url
- }
+ },
+ 'enterprise' => Gitlab.ee?
}
}
end
diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
index 7cd39f93ae7..e81621209fb 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'Removing an AwardEmoji' do
end
def create_award_emoji(user)
- create(:award_emoji, name: emoji_name, awardable: awardable, user: user )
+ create(:award_emoji, name: emoji_name, awardable: awardable, user: user)
end
shared_examples 'a mutation that does not destroy an AwardEmoji' do
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 7ddffa1ab0a..b151da72b55 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'Toggling an AwardEmoji' do
end
def create_award_emoji(user)
- create(:award_emoji, name: emoji_name, awardable: awardable, user: user )
+ create(:award_emoji, name: emoji_name, awardable: awardable, user: user)
end
context 'when the user has permission' do
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb
new file mode 100644
index 00000000000..8dfbf20d00b
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_schedule_take_ownership_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineScheduleTakeOwnership' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: owner) }
+
+ let(:mutation) do
+ graphql_mutation(
+ :pipeline_schedule_take_ownership,
+ { id: pipeline_schedule_id },
+ <<-QL
+ errors
+ QL
+ )
+ end
+
+ let(:pipeline_schedule_id) { pipeline_schedule.to_global_id.to_s }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'returns an error if the user is not allowed to take ownership of the schedule' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'takes ownership of the schedule' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to be_nil
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
index 6818ba33e74..54e63df96a6 100644
--- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe 'RunnersRegistrationTokenReset' do
include_context('when unauthorized', 'group')
include_context 'when authorized', 'group' do
- let_it_be(:user) { create_default(:group_member, :owner, user: create(:user), group: group ).user }
+ let_it_be(:user) { create_default(:group_member, :owner, user: create(:user), group: group).user }
def get_token
group.reload.runners_token
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
index c4121cfed42..5a27d39ecbc 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -33,11 +33,11 @@ RSpec.describe 'Destroying a container repository' do
end
shared_examples 'destroying the container repository' do
- it 'destroy the container repository' do
+ it 'marks the container repository as delete_scheduled' do
expect(::Packages::CreateEventService)
.to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
expect(DeleteContainerRepositoryWorker)
- .to receive(:perform_async).with(user.id, container_repository.id)
+ .not_to receive(:perform_async)
expect { subject }.to change { ::Packages::Event.count }.by(1)
@@ -80,6 +80,25 @@ RSpec.describe 'Destroying a container repository' do
it_behaves_like params[:shared_examples_name]
end
+
+ context 'with container_registry_delete_repository_with_cron_worker disabled' do
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
+ end
+
+ it 'enqueues a removal job' do
+ expect(::Packages::CreateEventService)
+ .to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original
+ expect(DeleteContainerRepositoryWorker)
+ .to receive(:perform_async).with(user.id, container_repository.id)
+
+ expect { subject }.to change { ::Packages::Event.count }.by(1)
+
+ expect(container_repository_mutation_response).to match_schema('graphql/container_repository')
+ expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED')
+ end
+ end
end
context 'with invalid id' do
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb
index 923e12a3c06..fc3b666dd3d 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/create_spec.rb
@@ -10,8 +10,16 @@ RSpec.describe 'Creating an incident timeline event' do
let_it_be(:incident) { create(:incident, project: project) }
let_it_be(:event_occurred_at) { Time.current }
let_it_be(:note) { 'demo note' }
+ let_it_be(:tag1) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 1') }
+ let_it_be(:tag2) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 2') }
+
+ let(:input) do
+ { incident_id: incident.to_global_id.to_s,
+ note: note,
+ occurred_at: event_occurred_at,
+ timeline_event_tag_names: [tag1.name] }
+ end
- let(:input) { { incident_id: incident.to_global_id.to_s, note: note, occurred_at: event_occurred_at } }
let(:mutation) do
graphql_mutation(:timeline_event_create, input) do
<<~QL
@@ -22,6 +30,7 @@ RSpec.describe 'Creating an incident timeline event' do
author { id username }
incident { id title }
note
+ timelineEventTags { nodes { name } }
editable
action
occurredAt
@@ -57,4 +66,25 @@ RSpec.describe 'Creating an incident timeline event' do
'occurredAt' => event_occurred_at.iso8601
)
end
+
+ context 'when note is more than 280 characters long' do
+ let_it_be(:note) { 'n' * 281 }
+
+ it_behaves_like 'timeline event mutation responds with validation error',
+ error_message: 'Timeline text is too long (maximum is 280 characters)'
+ end
+
+ context 'when timeline event tags are passed' do
+ it 'creates incident timeline event with tags', :aggregate_failures do
+ post_graphql_mutation(mutation, current_user: user)
+
+ timeline_event_response = mutation_response['timelineEvent']
+ tag_names = timeline_event_response['timelineEventTags']['nodes']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(timeline_event_response).to include(
+ 'timelineEventTags' => { 'nodes' => tag_names }
+ )
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
index 85eaec90f47..62eeecb3fb7 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/promote_from_note_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'Promote an incident timeline event from a comment' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:incident) { create(:incident, project: project) }
- let_it_be(:comment) { create(:note, project: project, noteable: incident) }
+ let_it_be(:comment) { create(:note, project: project, noteable: incident, note: 'a' * 281) }
let(:input) { { note_id: comment.to_global_id.to_s } }
let(:mutation) do
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb
index 1c4439cec6f..542d51b990f 100644
--- a/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event/update_spec.rb
@@ -13,11 +13,12 @@ RSpec.describe 'Updating an incident timeline event' do
end
let(:occurred_at) { 1.minute.ago.iso8601 }
+ let(:note) { 'Updated note' }
let(:variables) do
{
id: timeline_event.to_global_id.to_s,
- note: 'Updated note',
+ note: note,
occurred_at: occurred_at
}
end
@@ -70,11 +71,18 @@ RSpec.describe 'Updating an incident timeline event' do
'id' => incident.to_global_id.to_s,
'title' => incident.title
},
- 'note' => 'Updated note',
+ 'note' => note,
'noteHtml' => timeline_event.note_html,
'occurredAt' => occurred_at,
'createdAt' => timeline_event.created_at.iso8601,
'updatedAt' => timeline_event.updated_at.iso8601
)
end
+
+ context 'when note is more than 280 characters long' do
+ let(:note) { 'n' * 281 }
+
+ it_behaves_like 'timeline event mutation responds with validation error',
+ error_message: 'Timeline text is too long (maximum is 280 characters)'
+ end
end
diff --git a/spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb b/spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb
new file mode 100644
index 00000000000..7476499d9da
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/incident_management/timeline_event_tag/create_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating a timeline event tag' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:name) { 'Test tag 1' }
+
+ let(:input) { { project_path: project.full_path, name: name } }
+ let(:mutation) do
+ graphql_mutation(:timeline_event_tag_create, input) do
+ <<~QL
+ clientMutationId
+ errors
+ timelineEventTag {
+ id
+ name
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:timeline_event_tag_create) }
+
+ context 'when user has permissions to create timeline event tag' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates timeline event tag', :aggregate_failures do
+ post_graphql_mutation(mutation, current_user: user)
+
+ timeline_event_tag_response = mutation_response['timelineEventTag']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(timeline_event_tag_response).to include(
+ 'name' => name
+ )
+ end
+ end
+
+ context 'when user does not have permissions to create timeline event tag' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'raises error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(mutation_response).to be_nil
+ expect_graphql_errors_to_include(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index 9345735afe4..a489b7424e8 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -58,34 +58,15 @@ RSpec.describe 'Create an issue' do
input['type'] = 'TASK'
end
- context 'when work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
+ it 'creates an issue with TASK type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(Issue, :count).by(1)
- it 'creates an issue with the default ISSUE type' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.to change(Issue, :count).by(1)
+ created_issue = Issue.last
- created_issue = Issue.last
-
- expect(created_issue.work_item_type.base_type).to eq('issue')
- expect(created_issue.issue_type).to eq('issue')
- end
- end
-
- context 'when work_items feature flag is enabled' do
- it 'creates an issue with TASK type' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.to change(Issue, :count).by(1)
-
- created_issue = Issue.last
-
- expect(created_issue.work_item_type.base_type).to eq('task')
- expect(created_issue.issue_type).to eq('task')
- end
+ expect(created_issue.work_item_type.base_type).to eq('task')
+ expect(created_issue.issue_type).to eq('task')
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
index e7f4917ddde..c6a980b5cef 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
@@ -71,19 +71,5 @@ RSpec.describe "Create a work item from a task in a work item's description" do
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::CreateFromTask }
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does nothing and returns and error' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.to not_change(WorkItem, :count)
-
- expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
- end
- end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index 8233821053f..be3917316c3 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -154,17 +154,65 @@ RSpec.describe 'Create a work item' do
end
end
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
+ context 'with milestone widget input' do
+ let(:widgets_response) { mutation_response['workItem']['widgets'] }
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetMilestone {
+ milestone {
+ id
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
end
- it 'does not create the work item and returns an error' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.to not_change(WorkItem, :count)
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
- expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
+ context 'when setting milestone on work item creation' do
+ let_it_be(:project_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, project: project) }
+
+ let(:input) do
+ {
+ title: 'some WI',
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
+ milestoneWidget: { 'milestoneId' => milestone.to_global_id.to_s }
+ }
+ end
+
+ shared_examples "work item's milestone is set" do
+ it "sets the work item's milestone" do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(WorkItem, :count).by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include(
+ {
+ 'type' => 'MILESTONE',
+ 'milestone' => { 'id' => milestone.to_global_id.to_s }
+ }
+ )
+ end
+ end
+
+ context 'when assigning a project milestone' do
+ it_behaves_like "work item's milestone is set" do
+ let(:milestone) { project_milestone }
+ end
+ end
+
+ context 'when assigning a group milestone' do
+ it_behaves_like "work item's milestone is set" do
+ let(:milestone) { group_milestone }
+ end
+ end
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/delete_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
index 14c8b757a57..0a84225a7ab 100644
--- a/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/delete_spec.rb
@@ -31,19 +31,5 @@ RSpec.describe 'Delete a work item' do
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['project']).to include('id' => work_item.project.to_global_id.to_s)
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does not delete the work item' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.to not_change(WorkItem, :count)
-
- expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
- end
- end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
index e576d0ee7ef..c44939c8d54 100644
--- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb
@@ -75,19 +75,5 @@ RSpec.describe "Delete a task in a work item's description" do
expect(mutation_response['errors']).to contain_exactly('Stale work item. Check lock version')
end
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does nothing and returns and error' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- end.to not_change(WorkItem, :count)
-
- expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
- end
- end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 6b0129c457f..96736457f26 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -5,8 +5,11 @@ require 'spec_helper'
RSpec.describe 'Update a work item' do
include GraphqlHelpers
- let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+ let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
+ let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
let(:work_item_event) { 'CLOSE' }
@@ -543,6 +546,91 @@ RSpec.describe 'Update a work item' do
end
end
+ context 'when updating milestone' do
+ let_it_be(:project_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, project: project) }
+
+ let(:input) { { 'milestoneWidget' => { 'milestoneId' => new_milestone&.to_global_id&.to_s } } }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetMilestone {
+ milestone {
+ id
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ shared_examples "work item's milestone is updated" do
+ it "updates the work item's milestone" do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ work_item.reload
+ end.to change(work_item, :milestone).from(old_milestone).to(new_milestone)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ shared_examples "work item's milestone is not updated" do
+ it "ignores the update request" do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ work_item.reload
+ end.to not_change(work_item, :milestone)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'when user cannot set work item metadata' do
+ let(:current_user) { guest }
+ let(:old_milestone) { nil }
+
+ it_behaves_like "work item's milestone is not updated" do
+ let(:new_milestone) { project_milestone }
+ end
+ end
+
+ context 'when user can set work item metadata' do
+ let(:current_user) { reporter }
+
+ context 'when assigning a project milestone' do
+ it_behaves_like "work item's milestone is updated" do
+ let(:old_milestone) { nil }
+ let(:new_milestone) { project_milestone }
+ end
+ end
+
+ context 'when assigning a group milestone' do
+ it_behaves_like "work item's milestone is updated" do
+ let(:old_milestone) { nil }
+ let(:new_milestone) { group_milestone }
+ end
+ end
+
+ context "when unsetting the work item's milestone" do
+ it_behaves_like "work item's milestone is updated" do
+ let(:old_milestone) { group_milestone }
+ let(:new_milestone) { nil }
+
+ before do
+ work_item.update!(milestone: old_milestone)
+ end
+ end
+ end
+ end
+ end
+
context 'when unsupported widget input is sent' do
let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
@@ -556,20 +644,5 @@ RSpec.describe 'Update a work item' do
it_behaves_like 'a mutation that returns top-level errors',
errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"]
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does not update the work item and returns and error' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- work_item.reload
- end.to not_change(work_item, :title)
-
- expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
- end
- end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
index 32468a46ace..55285be5a5d 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
@@ -74,22 +74,6 @@ RSpec.describe 'Update a work item task' do
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::UpdateTask }
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'does not update the task item and returns and error' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- work_item.reload
- task.reload
- end.to not_change(task, :title)
-
- expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
- end
- end
end
context 'when user does not have permissions to update a work item' do
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index e9f82d66775..02a3206f587 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -206,5 +206,25 @@ RSpec.describe 'package details' do
expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
end
+
+ context 'web_path' do
+ before do
+ subject
+ end
+
+ it 'returns web_path correctly' do
+ expect(graphql_data_at(:package, :_links, :web_path)).to eq("/#{project.full_path}/-/packages/#{composer_package.id}")
+ end
+
+ context 'with terraform module' do
+ let_it_be(:terraform_package) { create(:terraform_module_package, project: project) }
+
+ let(:package_global_id) { global_id_of(terraform_package) }
+
+ it 'returns web_path correctly' do
+ expect(graphql_data_at(:package, :_links, :web_path)).to eq("/#{project.full_path}/-/infrastructure_registry/#{terraform_package.id}")
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
index cb5006ec8e4..a80f683ea93 100644
--- a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
+++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb
@@ -3,107 +3,5 @@
require 'spec_helper'
RSpec.describe 'getting merge access levels for a branch protection' do
- include GraphqlHelpers
-
- let_it_be(:current_user) { create(:user) }
-
- let(:merge_access_level_data) { merge_access_levels_data[0] }
-
- let(:merge_access_levels_data) do
- graphql_data_at('project',
- 'branchRules',
- 'nodes',
- 0,
- 'branchProtection',
- 'mergeAccessLevels',
- 'nodes')
- end
-
- let(:project) { protected_branch.project }
-
- let(:merge_access_levels_count) { protected_branch.merge_access_levels.size }
-
- let(:variables) { { path: project.full_path } }
-
- let(:fields) { all_graphql_fields_for('MergeAccessLevel') }
-
- let(:query) do
- <<~GQL
- query($path: ID!) {
- project(fullPath: $path) {
- branchRules(first: 1) {
- nodes {
- branchProtection {
- mergeAccessLevels {
- nodes {
- #{fields}
- }
- }
- }
- }
- }
- }
- }
- GQL
- end
-
- context 'when the user does not have read_protected_branch abilities' do
- let_it_be(:protected_branch) { create(:protected_branch) }
-
- before do
- project.add_guest(current_user)
- post_graphql(query, current_user: current_user, variables: variables)
- end
-
- it_behaves_like 'a working graphql query'
-
- it { expect(merge_access_levels_data).not_to be_present }
- end
-
- shared_examples 'merge access request' do
- let(:merge_access) { protected_branch.merge_access_levels.first }
-
- before do
- project.add_maintainer(current_user)
- post_graphql(query, current_user: current_user, variables: variables)
- end
-
- it_behaves_like 'a working graphql query'
-
- it 'returns all merge access levels' do
- expect(merge_access_levels_data.size).to eq(merge_access_levels_count)
- end
-
- it 'includes access_level' do
- expect(merge_access_level_data['accessLevel'])
- .to eq(merge_access.access_level)
- end
-
- it 'includes access_level_description' do
- expect(merge_access_level_data['accessLevelDescription'])
- .to eq(merge_access.humanize)
- end
- end
-
- context 'when the user does have read_protected_branch abilities' do
- let(:merge_access) { protected_branch.merge_access_levels.first }
-
- context 'when no one has access' do
- let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_merge) }
-
- it_behaves_like 'merge access request'
- end
-
- context 'when developers have access' do
- let_it_be(:protected_branch) { create(:protected_branch, :developers_can_merge) }
-
- it_behaves_like 'merge access request'
- end
-
- context 'when maintainers have access' do
- let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_merge) }
-
- it_behaves_like 'merge access request'
- end
- end
+ include_examples 'perform graphql requests for AccessLevel type objects', :merge
end
diff --git a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
index 59f9c7d61cb..cfdaf1096c3 100644
--- a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
+++ b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb
@@ -3,107 +3,5 @@
require 'spec_helper'
RSpec.describe 'getting push access levels for a branch protection' do
- include GraphqlHelpers
-
- let_it_be(:current_user) { create(:user) }
-
- let(:push_access_level_data) { push_access_levels_data[0] }
-
- let(:push_access_levels_data) do
- graphql_data_at('project',
- 'branchRules',
- 'nodes',
- 0,
- 'branchProtection',
- 'pushAccessLevels',
- 'nodes')
- end
-
- let(:project) { protected_branch.project }
-
- let(:push_access_levels_count) { protected_branch.push_access_levels.size }
-
- let(:variables) { { path: project.full_path } }
-
- let(:fields) { all_graphql_fields_for('PushAccessLevel'.classify) }
-
- let(:query) do
- <<~GQL
- query($path: ID!) {
- project(fullPath: $path) {
- branchRules(first: 1) {
- nodes {
- branchProtection {
- pushAccessLevels {
- nodes {
- #{fields}
- }
- }
- }
- }
- }
- }
- }
- GQL
- end
-
- context 'when the user does not have read_protected_branch abilities' do
- let_it_be(:protected_branch) { create(:protected_branch) }
-
- before do
- project.add_guest(current_user)
- post_graphql(query, current_user: current_user, variables: variables)
- end
-
- it_behaves_like 'a working graphql query'
-
- it { expect(push_access_levels_data).not_to be_present }
- end
-
- shared_examples 'push access request' do
- let(:push_access) { protected_branch.push_access_levels.first }
-
- before do
- project.add_maintainer(current_user)
- post_graphql(query, current_user: current_user, variables: variables)
- end
-
- it_behaves_like 'a working graphql query'
-
- it 'returns all push access levels' do
- expect(push_access_levels_data.size).to eq(push_access_levels_count)
- end
-
- it 'includes access_level' do
- expect(push_access_level_data['accessLevel'])
- .to eq(push_access.access_level)
- end
-
- it 'includes access_level_description' do
- expect(push_access_level_data['accessLevelDescription'])
- .to eq(push_access.humanize)
- end
- end
-
- context 'when the user does have read_protected_branch abilities' do
- let(:push_access) { protected_branch.push_access_levels.first }
-
- context 'when no one has access' do
- let_it_be(:protected_branch) { create(:protected_branch, :no_one_can_push) }
-
- it_behaves_like 'push access request'
- end
-
- context 'when developers have access' do
- let_it_be(:protected_branch) { create(:protected_branch, :developers_can_push) }
-
- it_behaves_like 'push access request'
- end
-
- context 'when maintainers have access' do
- let_it_be(:protected_branch) { create(:protected_branch, :maintainers_can_push) }
-
- it_behaves_like 'push access request'
- end
- end
+ include_examples 'perform graphql requests for AccessLevel type objects', :push
end
diff --git a/spec/requests/api/graphql/project/branch_rules_spec.rb b/spec/requests/api/graphql/project/branch_rules_spec.rb
index 1aaf0e9edc7..ed866305445 100644
--- a/spec/requests/api/graphql/project/branch_rules_spec.rb
+++ b/spec/requests/api/graphql/project/branch_rules_spec.rb
@@ -7,10 +7,9 @@ RSpec.describe 'getting list of branch rules for a project' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:branch_name_a) { 'branch_name_a' }
- let_it_be(:branch_name_b) { 'wildcard-*' }
+ let_it_be(:branch_name_a) { TestEnv::BRANCH_SHA.each_key.first }
+ let_it_be(:branch_name_b) { 'diff-*' }
let_it_be(:branch_rules) { [branch_rule_a, branch_rule_b] }
-
let_it_be(:branch_rule_a) do
create(:protected_branch, project: project, name: branch_name_a)
end
@@ -21,7 +20,6 @@ RSpec.describe 'getting list of branch rules for a project' do
let(:branch_rules_data) { graphql_data_at('project', 'branchRules', 'edges') }
let(:variables) { { path: project.full_path } }
- # fields must use let as the all_graphql_fields_for also configures some spies
let(:fields) { all_graphql_fields_for('BranchRule') }
let(:query) do
<<~GQL
@@ -60,49 +58,124 @@ RSpec.describe 'getting list of branch rules for a project' do
context 'when the user does have read_protected_branch abilities' do
before do
project.add_maintainer(current_user)
- post_graphql(query, current_user: current_user, variables: variables)
end
- it_behaves_like 'a working graphql query'
+ describe 'queries' do
+ before do
+ # rubocop:disable RSpec/AnyInstanceOf
+ allow_any_instance_of(User).to receive(:update_tracked_fields!)
+ allow_any_instance_of(Users::ActivityService).to receive(:execute)
+ # rubocop:enable RSpec/AnyInstanceOf
+ allow_next_instance_of(Resolvers::ProjectResolver) do |resolver|
+ allow(resolver).to receive(:resolve)
+ .with(full_path: project.full_path)
+ .and_return(project)
+ end
+ allow(project.repository).to receive(:branch_names).and_call_original
+ allow(project.repository.gitaly_ref_client).to receive(:branch_names).and_call_original
+ end
+
+ it 'matching_branches_count avoids N+1 queries' do
+ query = <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules {
+ nodes {
+ matchingBranchesCount
+ }
+ }
+ }
+ }
+ GQL
+
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ # Verify the response includes the field
+ expect_n_matching_branches_count_fields(2)
+
+ create(:protected_branch, project: project)
+ create(:protected_branch, name: '*', project: project)
+
+ expect do
+ post_graphql(query, current_user: current_user, variables: variables)
+ end.not_to exceed_all_query_limit(control)
+
+ expect(project.repository).to have_received(:branch_names).at_least(2).times
+ expect(project.repository.gitaly_ref_client).to have_received(:branch_names).once
- it 'returns branch rules data' do
- expect(branch_rules_data.dig(0, 'node', 'name')).to be_present
- expect(branch_rules_data.dig(0, 'node', 'isDefault')).to be(true).or be(false)
- expect(branch_rules_data.dig(0, 'node', 'branchProtection')).to be_present
- expect(branch_rules_data.dig(0, 'node', 'createdAt')).to be_present
- expect(branch_rules_data.dig(0, 'node', 'updatedAt')).to be_present
-
- expect(branch_rules_data.dig(1, 'node', 'name')).to be_present
- expect(branch_rules_data.dig(1, 'node', 'isDefault')).to be(true).or be(false)
- expect(branch_rules_data.dig(1, 'node', 'branchProtection')).to be_present
- expect(branch_rules_data.dig(1, 'node', 'createdAt')).to be_present
- expect(branch_rules_data.dig(1, 'node', 'updatedAt')).to be_present
+ expect_n_matching_branches_count_fields(4)
+ end
+
+ def expect_n_matching_branches_count_fields(count)
+ branch_rule_nodes = graphql_data_at('project', 'branchRules', 'nodes')
+ expect(branch_rule_nodes.count).to eq(count)
+ branch_rule_nodes.each do |node|
+ expect(node['matchingBranchesCount']).to be_present
+ end
+ end
end
- context 'when limiting the number of results' do
- let(:branch_rule_limit) { 1 }
- let(:variables) { { path: project.full_path, n: branch_rule_limit } }
- let(:next_variables) do
- { path: project.full_path, n: branch_rule_limit, cursor: last_cursor }
+ describe 'response' do
+ before do
+ post_graphql(query, current_user: current_user, variables: variables)
end
it_behaves_like 'a working graphql query'
- it 'returns pagination information' do
- expect(branch_rules_data.size).to eq(branch_rule_limit)
- expect(has_next_page).to be_truthy
- expect(has_prev_page).to be_falsey
- post_graphql(query, current_user: current_user, variables: next_variables)
- expect(branch_rules_data.size).to eq(branch_rule_limit)
- expect(has_next_page).to be_falsey
- expect(has_prev_page).to be_truthy
+ it 'includes all fields', :aggregate_failures do
+ # Responses will be sorted alphabetically. Branch names for this spec
+ # come from an external constant so we check which is first
+ br_a_idx = branch_name_a < branch_name_b ? 0 : 1
+ br_b_idx = 1 - br_a_idx
+
+ branch_rule_a_data = branch_rules_data.dig(br_a_idx, 'node')
+ branch_rule_b_data = branch_rules_data.dig(br_b_idx, 'node')
+
+ expect(branch_rule_a_data['name']).to eq(branch_name_a)
+ expect(branch_rule_a_data['isDefault']).to be(true).or be(false)
+ expect(branch_rule_a_data['branchProtection']).to be_present
+ expect(branch_rule_a_data['matchingBranchesCount']).to eq(1)
+ expect(branch_rule_a_data['createdAt']).to be_present
+ expect(branch_rule_a_data['updatedAt']).to be_present
+
+ wildcard_count = TestEnv::BRANCH_SHA.keys.count do |branch_name|
+ branch_name.starts_with?('diff-')
+ end
+ expect(branch_rule_b_data['name']).to eq(branch_name_b)
+ expect(branch_rule_b_data['isDefault']).to be(true).or be(false)
+ expect(branch_rule_b_data['branchProtection']).to be_present
+ expect(branch_rule_b_data['matchingBranchesCount']).to eq(wildcard_count)
+ expect(branch_rule_b_data['createdAt']).to be_present
+ expect(branch_rule_b_data['updatedAt']).to be_present
end
- context 'when no limit is provided' do
- let(:branch_rule_limit) { nil }
+ context 'when limiting the number of results' do
+ let(:branch_rule_limit) { 1 }
+ let(:variables) { { path: project.full_path, n: branch_rule_limit } }
+ let(:next_variables) do
+ { path: project.full_path, n: branch_rule_limit, cursor: last_cursor }
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns pagination information' do
+ expect(branch_rules_data.size).to eq(branch_rule_limit)
+ expect(has_next_page).to be_truthy
+ expect(has_prev_page).to be_falsey
+ post_graphql(query, current_user: current_user, variables: next_variables)
+ expect(branch_rules_data.size).to eq(branch_rule_limit)
+ expect(has_next_page).to be_falsey
+ expect(has_prev_page).to be_truthy
+ end
+
+ context 'when no limit is provided' do
+ let(:branch_rule_limit) { nil }
- it 'returns all branch_rules' do
- expect(branch_rules_data.size).to eq(branch_rules.size)
+ it 'returns all branch_rules' do
+ expect(branch_rules_data.size).to eq(branch_rules.size)
+ end
end
end
end
diff --git a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
index bcbb1f11d43..544d2d7bd95 100644
--- a/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
+++ b/spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb
@@ -48,6 +48,7 @@ RSpec.describe 'getting incident timeline events' do
note
noteHtml
promotedFromNote { id body }
+ timelineEventTags { nodes { name } }
editable
action
occurredAt
@@ -100,6 +101,7 @@ RSpec.describe 'getting incident timeline events' do
'id' => promoted_from_note.to_global_id.to_s,
'body' => promoted_from_note.note
},
+ 'timelineEventTags' => { 'nodes' => [] },
'editable' => true,
'action' => timeline_event.action,
'occurredAt' => timeline_event.occurred_at.iso8601,
@@ -108,6 +110,47 @@ RSpec.describe 'getting incident timeline events' do
)
end
+ context 'when timelineEvent tags are linked' do
+ let_it_be(:tag1) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 1') }
+ let_it_be(:tag2) { create(:incident_management_timeline_event_tag, project: project, name: 'Tag 2') }
+ let_it_be(:timeline_event_tag_link) do
+ create(:incident_management_timeline_event_tag_link,
+ timeline_event: timeline_event,
+ timeline_event_tag: tag1)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the set tags' do
+ expect(timeline_events.first['timelineEventTags']['nodes'].first['name']).to eq(tag1.name)
+ end
+
+ context 'when different timeline events are loaded' do
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: current_user)
+ end
+
+ new_event = create(:incident_management_timeline_event,
+ incident: incident,
+ project: project,
+ updated_by_user: updated_by_user,
+ promoted_from_note: promoted_from_note,
+ note: "Referencing #{issue.to_reference(full: true)} - Full URL #{issue_url}"
+ )
+
+ create(:incident_management_timeline_event_tag_link,
+ timeline_event: new_event,
+ timeline_event_tag: tag2
+ )
+
+ expect(incident.incident_management_timeline_events.length).to eq(3)
+ expect(post_graphql(query, current_user: current_user)).not_to exceed_query_limit(control)
+ expect(timeline_events.count).to eq(3)
+ end
+ end
+ end
+
context 'when filtering by id' do
let(:params) { { incident_id: incident.to_global_id.to_s, id: timeline_event.to_global_id.to_s } }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 3b8beb4f798..214165cb171 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -8,84 +8,74 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, :public, group: group) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
- let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
- let_it_be(:issues, reload: true) { [issue_a, issue_b] }
+ let_it_be(:another_user) { create(:user).tap { |u| group.add_reporter(u) } }
+ let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
+ let_it_be(:priority1) { create(:label, project: project, priority: 1) }
+ let_it_be(:priority2) { create(:label, project: project, priority: 5) }
+ let_it_be(:priority3) { create(:label, project: project, priority: 10) }
+
+ let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true, labels: [priority3]) }
+ let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project, title: 'title matching issue i') }
+ let_it_be(:issue_c) { create(:issue, project: project, labels: [priority1], milestone: late_milestone) }
+ let_it_be(:issue_d) { create(:issue, project: project, labels: [priority2]) }
+ let_it_be(:issue_e) { create(:issue, project: project, milestone: early_milestone) }
+ let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
let(:issue_a_gid) { issue_a.to_global_id.to_s }
let(:issue_b_gid) { issue_b.to_global_id.to_s }
- let(:issues_data) { graphql_data['project']['issues']['edges'] }
+ let(:issues_data) { graphql_data['project']['issues']['nodes'] }
let(:issue_filter_params) { {} }
let(:fields) do
<<~QUERY
- edges {
- node {
- #{all_graphql_fields_for('issues'.classify)}
- }
+ nodes {
+ #{all_graphql_fields_for('issues'.classify)}
}
QUERY
end
- it_behaves_like 'a working graphql query' do
- before do
- post_graphql(query, current_user: current_user)
- end
- end
-
- it 'includes a web_url' do
- post_graphql(query, current_user: current_user)
-
- expect(issues_data[0]['node']['webUrl']).to be_present
- end
-
- it 'includes discussion locked' do
- post_graphql(query, current_user: current_user)
-
- expect(issues_data[0]['node']['discussionLocked']).to eq(false)
- expect(issues_data[1]['node']['discussionLocked']).to eq(true)
- end
-
- context 'when both assignee_username filters are provided' do
- let(:issue_filter_params) { { assignee_username: current_user.username, assignee_usernames: [current_user.username] } }
-
- it 'returns a mutually exclusive param error' do
- post_graphql(query, current_user: current_user)
-
- expect_graphql_errors_to_include('only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
- end
- end
-
- context 'filtering by my_reaction_emoji' do
- using RSpec::Parameterized::TableSyntax
-
- let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) }
-
- where(:value, :gids) do
- 'thumbsup' | lazy { [issue_a_gid] }
- 'ANY' | lazy { [issue_a_gid] }
- 'any' | lazy { [issue_a_gid] }
- 'AnY' | lazy { [issue_a_gid] }
- 'NONE' | lazy { [issue_b_gid] }
- 'thumbsdown' | lazy { [] }
+ # All new specs should be added to the shared example if the change also
+ # affects the `issues` query at the root level of the API.
+ # Shared example also used in spec/requests/api/graphql/issues_spec.rb
+ it_behaves_like 'graphql issue list request spec' do
+ subject(:post_query) { post_graphql(query, current_user: current_user) }
+
+ # filters
+ let(:expected_negated_assignee_issues) { [issue_b, issue_c, issue_d, issue_e] }
+ let(:expected_unioned_assignee_issues) { [issue_a, issue_b] }
+ let(:voted_issues) { [issue_a] }
+ let(:no_award_issues) { [issue_b, issue_c, issue_d, issue_e] }
+ let(:locked_discussion_issues) { [issue_a] }
+ let(:unlocked_discussion_issues) { [issue_b, issue_c, issue_d, issue_e] }
+ let(:search_title_term) { 'matching issue' }
+ let(:title_search_issue) { issue_b }
+
+ # sorting
+ let(:data_path) { [:project, :issues] }
+ let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] }
+ let(:expected_priority_sorted_asc) { [issue_e, issue_c, issue_d, issue_a, issue_b] }
+ let(:expected_priority_sorted_desc) { [issue_c, issue_e, issue_a, issue_d, issue_b] }
+
+ before_all do
+ issue_a.assignee_ids = current_user.id
+ issue_b.assignee_ids = another_user.id
+
+ create(:award_emoji, :upvote, user: current_user, awardable: issue_a)
+
+ # severity sorting
+ create(:issuable_severity, issue: issue_a, severity: :unknown)
+ create(:issuable_severity, issue: issue_b, severity: :low)
+ create(:issuable_severity, issue: issue_d, severity: :critical)
+ create(:issuable_severity, issue: issue_e, severity: :high)
end
- with_them do
- let(:issue_filter_params) { { my_reaction_emoji: value } }
-
- it 'returns correctly filtered issues' do
- post_graphql(query, current_user: current_user)
-
- expect(issues_ids).to eq(gids)
- end
- end
- end
-
- context 'when filtering by search' do
- it_behaves_like 'query with a search term' do
- let(:issuable_data) { issues_data }
- let(:user) { current_user }
- let_it_be(:issuable) { create(:issue, project: project, description: 'bar') }
+ def pagination_query(params)
+ graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:issues, params, "#{page_info} nodes { id }")
+ )
end
end
@@ -155,10 +145,10 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns issues without confidential issues' do
post_graphql(query, current_user: current_user)
- expect(issues_data.size).to eq(2)
+ expect(issues_data.size).to eq(5)
issues_data.each do |issue|
- expect(issue.dig('node', 'confidential')).to eq(false)
+ expect(issue['confidential']).to eq(false)
end
end
@@ -178,7 +168,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
- expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
+ expect(issue_ids).to match_array(issues.map { |i| i.to_gid.to_s })
end
end
end
@@ -191,13 +181,13 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns issues with confidential issues' do
post_graphql(query, current_user: current_user)
- expect(issues_data.size).to eq(3)
+ expect(issues_data.size).to eq(6)
confidentials = issues_data.map do |issue|
- issue.dig('node', 'confidential')
+ issue['confidential']
end
- expect(confidentials).to eq([true, false, false])
+ expect(confidentials).to contain_exactly(true, false, false, false, false, false)
end
context 'filtering for confidential issues' do
@@ -206,7 +196,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
- expect(issues_ids).to contain_exactly(confidential_issue_gid)
+ expect(issue_ids).to contain_exactly(confidential_issue_gid)
end
end
@@ -216,7 +206,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
- expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
+ expect(issue_ids).to match_array([issue_a, issue_b, issue_c, issue_d, issue_e].map { |i| i.to_gid.to_s })
end
end
end
@@ -238,37 +228,7 @@ RSpec.describe 'getting an issue list for a project' do
data.map { |issue| issue['iid'].to_i }
end
- context 'when sorting by severity' do
- let_it_be(:severty_issue1) { create(:issue, project: sort_project) }
- let_it_be(:severty_issue2) { create(:issue, project: sort_project) }
- let_it_be(:severty_issue3) { create(:issue, project: sort_project) }
- let_it_be(:severty_issue4) { create(:issue, project: sort_project) }
- let_it_be(:severty_issue5) { create(:issue, project: sort_project) }
-
- before(:all) do
- create(:issuable_severity, issue: severty_issue1, severity: :unknown)
- create(:issuable_severity, issue: severty_issue2, severity: :low)
- create(:issuable_severity, issue: severty_issue4, severity: :critical)
- create(:issuable_severity, issue: severty_issue5, severity: :high)
- end
-
- context 'when ascending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :SEVERITY_ASC }
- let(:first_param) { 2 }
- let(:all_records) { [severty_issue3.iid, severty_issue1.iid, severty_issue2.iid, severty_issue5.iid, severty_issue4.iid] }
- end
- end
-
- context 'when descending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :SEVERITY_DESC }
- let(:first_param) { 2 }
- let(:all_records) { [severty_issue4.iid, severty_issue5.iid, severty_issue2.iid, severty_issue1.iid, severty_issue3.iid] }
- end
- end
- end
-
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
context 'when sorting by due date' do
let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
@@ -314,41 +274,6 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'when sorting by priority' do
- let_it_be(:on_project) { { project: sort_project } }
- let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) }
- let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) }
- let_it_be(:priority_1) { create(:label, **on_project, priority: 1) }
- let_it_be(:priority_2) { create(:label, **on_project, priority: 5) }
- let_it_be(:priority_issue1) { create(:issue, **on_project, labels: [priority_1], milestone: late_milestone) }
- let_it_be(:priority_issue2) { create(:issue, **on_project, labels: [priority_2]) }
- let_it_be(:priority_issue3) { create(:issue, **on_project, milestone: early_milestone) }
- let_it_be(:priority_issue4) { create(:issue, **on_project) }
-
- context 'when ascending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :PRIORITY_ASC }
- let(:first_param) { 2 }
- let(:all_records) do
- [
- priority_issue3.iid, priority_issue1.iid,
- priority_issue2.iid, priority_issue4.iid
- ]
- end
- end
- end
-
- context 'when descending' do
- it_behaves_like 'sorted paginated query' do
- let(:sort_param) { :PRIORITY_DESC }
- let(:first_param) { 2 }
- let(:all_records) do
- [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid]
- end
- end
- end
- end
-
context 'when sorting by label priority' do
let_it_be(:label1) { create(:label, project: sort_project, priority: 1) }
let_it_be(:label2) { create(:label, project: sort_project, priority: 5) }
@@ -374,6 +299,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
end
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
context 'when sorting by milestone due date' do
let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
@@ -403,14 +329,17 @@ RSpec.describe 'getting an issue list for a project' do
context 'when fetching alert management alert' do
let(:fields) do
<<~QUERY
- edges {
- node {
+ nodes {
iid
alertManagementAlert {
title
}
+ alertManagementAlerts {
+ nodes {
+ title
+ }
+ }
}
- }
QUERY
end
@@ -430,11 +359,22 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns the alert data' do
post_graphql(query, current_user: current_user)
- alert_titles = issues_data.map { |issue| issue.dig('node', 'alertManagementAlert', 'title') }
+ alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlert', 'title') }
expected_titles = issues.map { |issue| issue.alert_management_alert&.title }
expect(alert_titles).to contain_exactly(*expected_titles)
end
+
+ it 'returns the alerts data' do
+ post_graphql(query, current_user: current_user)
+
+ alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlerts', 'nodes') }
+ expected_titles = issues.map do |issue|
+ issue.alert_management_alerts.map { |alert| { 'title' => alert.title } }
+ end
+
+ expect(alert_titles).to contain_exactly(*expected_titles)
+ end
end
context 'when fetching customer_relations_contacts' do
@@ -469,13 +409,11 @@ RSpec.describe 'getting an issue list for a project' do
context 'when fetching labels' do
let(:fields) do
<<~QUERY
- edges {
- node {
- id
- labels {
- nodes {
- id
- }
+ nodes {
+ id
+ labels {
+ nodes {
+ id
}
}
}
@@ -491,8 +429,8 @@ RSpec.describe 'getting an issue list for a project' do
end
def response_label_ids(response_data)
- response_data.map do |edge|
- edge['node']['labels']['nodes'].map { |u| u['id'] }
+ response_data.map do |node|
+ node['labels']['nodes'].map { |u| u['id'] }
end.flatten
end
@@ -502,7 +440,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'avoids N+1 queries', :aggregate_failures do
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- expect(issues_data.count).to eq(2)
+ expect(issues_data.count).to eq(5)
expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues))
new_issues = issues + [create(:issue, project: project, labels: [create(:label, project: project)])]
@@ -510,8 +448,8 @@ RSpec.describe 'getting an issue list for a project' do
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
# graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
# so we have to parse the body ourselves the second time
- issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges']
- expect(issues_data.count).to eq(3)
+ issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes']
+ expect(issues_data.count).to eq(6)
expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues))
end
end
@@ -519,13 +457,11 @@ RSpec.describe 'getting an issue list for a project' do
context 'when fetching assignees' do
let(:fields) do
<<~QUERY
- edges {
- node {
- id
- assignees {
- nodes {
- id
- }
+ nodes {
+ id
+ assignees {
+ nodes {
+ id
}
}
}
@@ -541,8 +477,8 @@ RSpec.describe 'getting an issue list for a project' do
end
def response_assignee_ids(response_data)
- response_data.map do |edge|
- edge['node']['assignees']['nodes'].map { |node| node['id'] }
+ response_data.map do |node|
+ node['assignees']['nodes'].map { |node| node['id'] }
end.flatten
end
@@ -552,7 +488,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'avoids N+1 queries', :aggregate_failures do
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- expect(issues_data.count).to eq(2)
+ expect(issues_data.count).to eq(5)
expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues))
new_issues = issues + [create(:issue, project: project, assignees: [create(:user)])]
@@ -560,8 +496,8 @@ RSpec.describe 'getting an issue list for a project' do
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
# graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
# so we have to parse the body ourselves the second time
- issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges']
- expect(issues_data.count).to eq(3)
+ issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['nodes']
+ expect(issues_data.count).to eq(6)
expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
end
end
@@ -572,11 +508,9 @@ RSpec.describe 'getting an issue list for a project' do
let(:statuses) { issue_data.to_h { |issue| [issue['iid'], issue['escalationStatus']] } }
let(:fields) do
<<~QUERY
- edges {
- node {
- id
- escalationStatus
- }
+ nodes {
+ id
+ escalationStatus
}
QUERY
end
@@ -588,9 +522,9 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns the escalation status values' do
post_graphql(query, current_user: current_user)
- statuses = issues_data.map { |issue| issue.dig('node', 'escalationStatus') }
+ statuses = issues_data.map { |issue| issue['escalationStatus'] }
- expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil)
+ expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil, nil, nil, nil)
end
it 'avoids N+1 queries', :aggregate_failures do
@@ -726,8 +660,8 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- def issues_ids
- graphql_dig_at(issues_data, :node, :id)
+ def issue_ids
+ graphql_dig_at(issues_data, :id)
end
def query(params = issue_filter_params)
diff --git a/spec/requests/api/graphql/project/languages_spec.rb b/spec/requests/api/graphql/project/languages_spec.rb
new file mode 100644
index 00000000000..6ef500cde41
--- /dev/null
+++ b/spec/requests/api/graphql/project/languages_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.languages' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ languages {
+ name
+ share
+ color
+ }
+ }
+ }
+ )
+ end
+
+ let_it_be(:test_languages) do
+ [{ value: 66.69, label: "Ruby", color: "#701516", highlight: "#701516" },
+ { value: 22.98, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
+ { value: 7.91, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
+ { value: 2.42, label: "CoffeeScript", color: "#244776", highlight: "#244776" }]
+ end
+
+ let_it_be(:expected_languages) do
+ test_languages.map { |lang| { 'name' => lang[:label], 'share' => lang[:value], 'color' => lang[:color] } }
+ end
+
+ before do
+ allow(project.repository).to receive(:languages).and_return(test_languages)
+ end
+
+ context "when the languages haven't been detected yet" do
+ it 'returns expected languages on second request as detection is done asynchronously', :sidekiq_inline do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :languages)).to eq([])
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :languages)).to eq(expected_languages)
+ end
+ end
+
+ context 'when the languages were detected before' do
+ before do
+ Projects::DetectRepositoryLanguagesService.new(project, project.first_owner).execute
+ end
+
+ it 'returns repository languages' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :languages)).to eq(expected_languages)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb
index 25e878a5b1a..e63e0d3dd04 100644
--- a/spec/requests/api/graphql/project/tree/tree_spec.rb
+++ b/spec/requests/api/graphql/project/tree/tree_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe 'getting a tree in a project' do
include GraphqlHelpers
- let(:project) { create(:project, :repository) }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:current_user) { project.first_owner }
let(:path) { "" }
let(:ref) { "master" }
@@ -82,6 +83,89 @@ RSpec.describe 'getting a tree in a project' do
end
end
+ context 'when the ref points to a gpg-signed commit with a user' do
+ let_it_be(:name) { GpgHelpers::User1.names.first }
+ let_it_be(:email) { GpgHelpers::User1.emails.first }
+ let_it_be(:current_user) { create(:user, name: name, email: email).tap { |user| project.add_owner(user) } }
+ let_it_be(:gpg_key) { create(:gpg_key, user: current_user, key: GpgHelpers::User1.public_key) }
+
+ let(:ref) { GpgHelpers::SIGNED_AND_AUTHORED_SHA }
+ let(:fields) do
+ <<~QUERY
+ tree(path:"#{path}", ref:"#{ref}") {
+ lastCommit {
+ signature {
+ ... on GpgSignature {
+ #{all_graphql_fields_for('GpgSignature'.classify, max_depth: 2)}
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns the expected signature data' do
+ signature = graphql_data['project']['repository']['tree']['lastCommit']['signature']
+ expect(signature['commitSha']).to eq(ref)
+ expect(signature['user']['id']).to eq("gid://gitlab/User/#{current_user.id}")
+ expect(signature['gpgKeyUserName']).to eq(name)
+ expect(signature['gpgKeyUserEmail']).to eq(email)
+ expect(signature['verificationStatus']).to eq('VERIFIED')
+ expect(signature['project']['id']).to eq("gid://gitlab/Project/#{project.id}")
+ end
+ end
+
+ context 'when the ref points to a X.509-signed commit' do
+ let_it_be(:email) { X509Helpers::User1.certificate_email }
+ let_it_be(:current_user) { create(:user, email: email).tap { |user| project.add_owner(user) } }
+
+ let(:ref) { X509Helpers::User1.commit }
+ let(:fields) do
+ <<~QUERY
+ tree(path:"#{path}", ref:"#{ref}") {
+ lastCommit {
+ signature {
+ ... on X509Signature {
+ #{all_graphql_fields_for('X509Signature'.classify, max_depth: 2)}
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ store = OpenSSL::X509::Store.new
+ store.add_cert(OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert))
+ allow(OpenSSL::X509::Store).to receive(:new).and_return(store)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns the expected signature data' do
+ signature = graphql_data['project']['repository']['tree']['lastCommit']['signature']
+ expect(signature['commitSha']).to eq(ref)
+ expect(signature['verificationStatus']).to eq('VERIFIED')
+ expect(signature['project']['id']).to eq("gid://gitlab/Project/#{project.id}")
+ end
+
+ it 'returns expected certificate data' do
+ signature = graphql_data['project']['repository']['tree']['lastCommit']['signature']
+ certificate = signature['x509Certificate']
+ expect(certificate['certificateStatus']).to eq('good')
+ expect(certificate['email']).to eq(X509Helpers::User1.certificate_email)
+ expect(certificate['id']).to be_present
+ expect(certificate['serialNumber']).to eq(X509Helpers::User1.certificate_serial.to_s)
+ expect(certificate['subject']).to eq(X509Helpers::User1.certificate_subject)
+ expect(certificate['subjectKeyIdentifier']).to eq(X509Helpers::User1.certificate_subject_key_identifier)
+ expect(certificate['createdAt']).to be_present
+ expect(certificate['updatedAt']).to be_present
+ end
+ end
+
context 'when current user is nil' do
it 'returns empty project' do
post_graphql(query, current_user: nil)
diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb
index 157961c3f66..3d30baab816 100644
--- a/spec/requests/api/graphql/project/work_item_types_spec.rb
+++ b/spec/requests/api/graphql/project/work_item_types_spec.rb
@@ -57,15 +57,4 @@ RSpec.describe 'getting a list of work item types for a project' do
expect(graphql_data).to eq('project' => nil)
end
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- post_graphql(query, current_user: current_user)
- end
-
- it 'returns null' do
- expect(graphql_data.dig('project', 'workItemTypes')).to be_nil
- end
- end
end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index e82f6ad24a2..6d20799c9ec 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -10,6 +10,8 @@ RSpec.describe 'getting a work item list for a project' do
let_it_be(:current_user) { create(:user) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
+ let_it_be(:milestone1) { create(:milestone, project: project) }
+ let_it_be(:milestone2) { create(:milestone, project: project) }
let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1', labels: [label1]) }
let_it_be(:item2) do
@@ -19,7 +21,8 @@ RSpec.describe 'getting a work item list for a project' do
title: 'item2',
last_edited_by: current_user,
last_edited_at: 1.day.ago,
- labels: [label2]
+ labels: [label2],
+ milestone: milestone1
)
end
@@ -55,7 +58,8 @@ RSpec.describe 'getting a work item list for a project' do
:last_edited_by_user,
last_edited_at: 1.week.ago,
project: project,
- labels: [label1, label2]
+ labels: [label1, label2],
+ milestone: milestone2
)
expect_graphql_errors_to_be_empty
@@ -94,6 +98,11 @@ RSpec.describe 'getting a work item list for a project' do
labels { nodes { id } }
allowsScopedLabels
}
+ ... on WorkItemWidgetMilestone {
+ milestone {
+ id
+ }
+ }
}
}
GRAPHQL
@@ -121,18 +130,6 @@ RSpec.describe 'getting a work item list for a project' do
end
end
- context 'when work_items flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'returns an empty list' do
- post_graphql(query)
-
- expect(items_data).to eq([])
- end
- end
-
it 'returns only items visible to user' do
post_graphql(query, current_user: current_user)
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 2105e479ed2..a55de6adfb2 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -298,6 +298,40 @@ RSpec.describe 'Query.work_item(id)' do
)
end
end
+
+ describe 'milestone widget' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+
+ let(:work_item) { create(:work_item, project: project, milestone: milestone) }
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetMilestone {
+ milestone {
+ id
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'MILESTONE',
+ 'milestone' => {
+ 'id' => work_item.milestone.to_gid.to_s
+ }
+ )
+ )
+ )
+ end
+ end
end
context 'when an Issue Global ID is provided' do
@@ -323,16 +357,4 @@ RSpec.describe 'Query.work_item(id)' do
)
end
end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'returns nil' do
- post_graphql(query)
-
- expect(work_item_data).to be_nil
- end
- end
end
diff --git a/spec/requests/api/group_boards_spec.rb b/spec/requests/api/group_boards_spec.rb
index 6ce8b766807..cc110aa4017 100644
--- a/spec/requests/api/group_boards_spec.rb
+++ b/spec/requests/api/group_boards_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe API::GroupBoards do
board_parent.add_owner(user)
end
- let_it_be(:project) { create(:project, :public, namespace: board_parent ) }
+ let_it_be(:project) { create(:project, :public, namespace: board_parent) }
let_it_be(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: board_parent)
diff --git a/spec/requests/api/group_container_repositories_spec.rb b/spec/requests/api/group_container_repositories_spec.rb
index 413c37eaed9..82daab0e5e8 100644
--- a/spec/requests/api/group_container_repositories_spec.rb
+++ b/spec/requests/api/group_container_repositories_spec.rb
@@ -57,5 +57,13 @@ RSpec.describe API::GroupContainerRepositories do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with URL-encoded path of the group' do
+ let(:url) { "/groups/#{group.full_path}/registry/repositories" }
+
+ it_behaves_like 'rejected container repository access', :guest, :forbidden
+ it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'returns repositories for allowed users', :reporter
+ end
end
end
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 4fed7dd7624..a07a8ae4292 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe API::GroupVariables do
it 'creates variable' do
expect do
- post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true }
+ post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true, raw: true }
end.to change { group.variables.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -100,6 +100,16 @@ RSpec.describe API::GroupVariables do
expect(json_response['masked']).to be_truthy
expect(json_response['variable_type']).to eq('env_var')
expect(json_response['environment_scope']).to eq('*')
+ expect(json_response['raw']).to be_truthy
+ end
+
+ it 'masks the new value when logging' do
+ masked_params = { 'key' => 'VAR_KEY', 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
+
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+
+ post api("/groups/#{group.id}/variables", user),
+ params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
end
it 'creates variable with optional attributes' do
@@ -112,6 +122,7 @@ RSpec.describe API::GroupVariables do
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
expect(json_response['masked']).to be_falsey
+ expect(json_response['raw']).to be_falsey
expect(json_response['variable_type']).to eq('file')
expect(json_response['environment_scope']).to eq('*')
end
@@ -152,7 +163,7 @@ RSpec.describe API::GroupVariables do
initial_variable = group.variables.reload.first
value_before = initial_variable.value
- put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, masked: true }
+ put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true, masked: true, raw: true }
updated_variable = group.variables.reload.first
@@ -162,6 +173,16 @@ RSpec.describe API::GroupVariables do
expect(updated_variable).to be_protected
expect(json_response['variable_type']).to eq('file')
expect(json_response['masked']).to be_truthy
+ expect(json_response['raw']).to be_truthy
+ end
+
+ it 'masks the new value when logging' do
+ masked_params = { 'value' => '[FILTERED]', 'protected' => 'true', 'masked' => 'true' }
+
+ expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
+
+ put api("/groups/#{group.id}/variables/#{variable.key}", user),
+ params: { value: 'SENSITIVE', protected: true, masked: true }
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 02d29601ceb..ce6140d8da8 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -91,11 +91,8 @@ RSpec.describe API::Groups do
.to satisfy_one { |group| group['name'] == group1.name }
end
- it 'avoids N+1 queries' do
- # Establish baseline
- get api("/groups", admin)
-
- control = ActiveRecord::QueryRecorder.new do
+ it 'avoids N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/groups", admin)
end
@@ -103,7 +100,7 @@ RSpec.describe API::Groups do
expect do
get api("/groups", admin)
- end.not_to exceed_query_limit(control)
+ end.not_to exceed_all_query_limit(control)
end
context 'when statistics are requested' do
@@ -1230,10 +1227,7 @@ RSpec.describe API::Groups do
group1.reload
end
- it "only looks up root ancestor once and returns projects including those in subgroups" do
- expect(Namespace).to receive(:find_by).with(id: group1.id.to_s).once.and_call_original # For the group sent in the API call
- expect(Namespace).to receive(:joins).with(start_with('INNER JOIN (SELECT id, traversal_ids[1]')).once.and_call_original # All-in-one root_ancestor query
-
+ it "returns projects including those in subgroups" do
get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1241,6 +1235,18 @@ RSpec.describe API::Groups do
expect(json_response).to be_an(Array)
expect(json_response.length).to eq(6)
end
+
+ it 'avoids N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
+ end
+
+ create_list(:project, 2, :public, namespace: group1)
+
+ expect do
+ get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
+ end.not_to exceed_all_query_limit(control.count)
+ end
end
context 'when include_ancestor_groups is true' do
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 015a09d41ab..4f95295c14d 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -89,6 +89,18 @@ RSpec.describe API::ImportGithub do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
+
+ context 'when unauthenticated user' do
+ it 'returns 403 response' do
+ post api("/import/github"), params: {
+ target_namespace: user.namespace_path,
+ personal_access_token: token,
+ repo_id: non_existing_record_id
+ }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
describe "POST /import/github/cancel" do
@@ -127,5 +139,15 @@ RSpec.describe API::ImportGithub do
expect(json_response['message']).to eq('The import cannot be canceled because it is finished')
end
end
+
+ context 'when unauthenticated user' do
+ it 'returns 403 response' do
+ post api("/import/github/cancel"), params: {
+ project_id: project.id
+ }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
end
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 67d8a18dfd8..3c6604cf409 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -69,48 +69,6 @@ RSpec.describe API::Internal::Kubernetes do
context 'is authenticated for an agent' do
let!(:agent_token) { create(:cluster_agent_token) }
- # Todo: Remove gitops_sync_count and k8s_api_proxy_request_count in the next milestone
- # https://gitlab.com/gitlab-org/gitlab/-/issues/369489
- # We're only keeping it for backwards compatibility until KAS is released
- # using `counts:` instead
- context 'deprecated events' do
- it 'returns no_content for valid events' do
- send_request(params: { gitops_sync_count: 10, k8s_api_proxy_request_count: 5 })
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
-
- it 'returns no_content for counts of zero' do
- send_request(params: { gitops_sync_count: 0, k8s_api_proxy_request_count: 0 })
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
-
- it 'returns 400 for non number' do
- send_request(params: { gitops_sync_count: 'string', k8s_api_proxy_request_count: 1 })
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it 'returns 400 for negative number' do
- send_request(params: { gitops_sync_count: -1, k8s_api_proxy_request_count: 1 })
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it 'tracks events' do
- counters = { gitops_sync_count: 10, k8s_api_proxy_request_count: 5 }
- expected_counters = {
- kubernetes_agent_gitops_sync: counters[:gitops_sync_count],
- kubernetes_agent_k8s_api_proxy_request: counters[:k8s_api_proxy_request_count]
- }
-
- send_request(params: counters)
-
- expect(Gitlab::UsageDataCounters::KubernetesAgentCounter.totals).to eq(expected_counters)
- end
- end
-
it 'returns no_content for valid events' do
counters = { gitops_sync: 10, k8s_api_proxy_request: 5 }
unique_counters = { agent_users_using_ci_tunnel: [10] }
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index a795b49c44e..c07d2e11363 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe API::Invitations do
let_it_be(:maintainer) { create(:user, username: 'maintainer_user') }
+ let_it_be(:maintainer2) { create(:user, username: 'user-with-maintainer-role') }
let_it_be(:developer) { create(:user) }
let_it_be(:access_requester) { create(:user) }
let_it_be(:stranger) { create(:user) }
@@ -31,8 +32,8 @@ RSpec.describe API::Invitations do
api("/#{source.model_name.plural}/#{source.id}/invitations", user)
end
- def invite_member_by_email(source, source_type, email, created_by)
- create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
+ def invite_member_by_email(source, source_type, email, created_by, access_level: :developer)
+ create(:"#{source_type}_member", access_level, invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
end
shared_examples 'POST /:source_type/:id/invitations' do |source_type|
@@ -44,15 +45,42 @@ RSpec.describe API::Invitations do
end
end
- context 'when authenticated as a non-member or member with insufficient rights' do
- %i[access_requester stranger developer].each do |type|
- context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
+ context 'when authenticated as a non-member or member with insufficient membership management rights' do
+ context 'when the user does not have rights to manage members' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ post invitations_url(source, public_send(type)),
+ params: { email: email, access_level: Member::MAINTAINER }
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the user has the rights to manage members but tries to manage members with a higher access level' do
+ let(:maintainer) { maintainer2 }
- post invitations_url(source, user), params: { email: email, access_level: Member::MAINTAINER }
+ before do
+ source.add_maintainer(maintainer)
+ end
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'when an invitee is added as OWNER' do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ post invitations_url(source, maintainer),
+ params: { email: email, access_level: Member::OWNER }
+ end
+ end
+ end
+
+ context 'when an access_requester is added as OWNER' do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ post invitations_url(source, maintainer),
+ params: { user_id: access_requester.email, access_level: Member::OWNER }
+ end
end
end
end
@@ -348,6 +376,14 @@ RSpec.describe API::Invitations do
it 'returns 400 when the email list is not a valid format' do
post invitations_url(source, maintainer),
+ params: { email: %w[email1@example.com not-an-email], access_level: Member::MAINTAINER }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('email contains an invalid email address')
+ end
+
+ it 'returns 400 when the comma-separated email list is not a valid format' do
+ 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)
@@ -495,14 +531,12 @@ RSpec.describe API::Invitations do
end
end
- %i[developer access_requester stranger].each do |type|
- context "when authenticated as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
-
- get invitations_url(source, user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ get invitations_url(source, public_send(type))
+ end
end
end
end
@@ -573,14 +607,14 @@ RSpec.describe API::Invitations do
end
context 'when authenticated as a non-member or member with insufficient rights' do
- %i[access_requester stranger].each do |type|
- context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
-
- delete invite_api(source, user, invite.invite_email)
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'when the user does not have rights to manage members' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ delete invite_api(source, public_send(type), invite.invite_email)
+ end
+ end
end
end
end
@@ -604,6 +638,23 @@ RSpec.describe API::Invitations do
expect(response).to have_gitlab_http_status(:no_content)
end.to change { source.members.count }.by(-1)
end
+
+ context 'when MAINTAINER tries to remove invitation of an OWNER' do
+ let_it_be(:maintainer) { maintainer2 }
+ let!(:owner_invite) do
+ invite_member_by_email(source, source_type, 'owner@owner.com', developer, access_level: :owner)
+ end
+
+ before do
+ source.add_maintainer(maintainer)
+ end
+
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ delete invite_api(source, maintainer, owner_invite.invite_email)
+ end
+ end
+ end
end
it 'returns 404 if member does not exist' do
@@ -651,14 +702,15 @@ RSpec.describe API::Invitations do
end
context 'when authenticated as a non-member or member with insufficient rights' do
- %i[access_requester stranger].each do |type|
- context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
-
- put update_api(source, user, invite.invite_email), params: { access_level: Member::MAINTAINER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'when the user does not have rights to manage members' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ put update_api(source, public_send(type), invite.invite_email),
+ params: { access_level: Member::MAINTAINER }
+ end
+ end
end
end
end
@@ -673,6 +725,21 @@ RSpec.describe API::Invitations do
expect(json_response['access_level']).to eq(Member::MAINTAINER)
expect(invite.reload.access_level).to eq(Member::MAINTAINER)
end
+
+ context 'MAINTAINER tries to update access level to OWNER' do
+ let_it_be(:maintainer) { maintainer2 }
+
+ before do
+ source.add_maintainer(maintainer)
+ end
+
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ put update_api(source, maintainer, invite.invite_email),
+ params: { access_level: Member::OWNER }
+ end
+ end
+ end
end
it 'returns 409 if member does not exist' do
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index f5c73846173..0e20b2133db 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -92,7 +92,7 @@ RSpec.describe API::Issues do
describe 'GET /issues/:id' do
context 'when unauthorized' do
it 'returns unauthorized' do
- get api("/issues/#{issue.id}" )
+ get api("/issues/#{issue.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -101,7 +101,7 @@ RSpec.describe API::Issues do
context 'when authorized' do
context 'as a normal user' do
it 'returns forbidden' do
- get api("/issues/#{issue.id}", user )
+ get api("/issues/#{issue.id}", user)
expect(response).to have_gitlab_http_status(:forbidden)
end
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 3883eb01391..deaf7be96ab 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -473,8 +473,8 @@ RSpec.describe API::Issues do
end
describe '/projects/:id/issues/:issue_iid/move' do
- let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
- let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
+ let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace) }
it 'moves an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index c1217292d5c..97ab90c9776 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -234,7 +234,7 @@ RSpec.describe API::Labels do
before do
create(:labeled_issue, project: project, labels: [group_label], author: user)
create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
- create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
+ create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project)
end
it 'includes counts in the response' do
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
index 3e702b05bc9..6239ac4e749 100644
--- a/spec/requests/api/markdown_spec.rb
+++ b/spec/requests/api/markdown_spec.rb
@@ -193,7 +193,7 @@ RSpec.describe API::Markdown do
end
let(:issue) { create(:issue, project: public_project, title: 'Team only title') }
- let(:text) { "#{issue.to_reference}" }
+ let(:text) { issue.to_reference.to_s }
let(:params) { { text: text, gfm: true, project: public_project.full_path } }
shared_examples 'user without proper access' do
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index d771c1e2dcc..ac8c4aacdf2 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -720,6 +720,16 @@ RSpec.describe API::MavenPackages do
expect(response).to have_gitlab_http_status(:not_found)
end
+ context 'with access to package registry for everyone' do
+ subject { download_file(file_name: package_file.file_name) }
+
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it_behaves_like 'successfully returning the file'
+ end
+
it_behaves_like 'downloads with a job token'
it_behaves_like 'downloads with a deploy token'
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 9df9c75b020..69be574f38a 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -270,39 +270,42 @@ RSpec.describe API::Members do
end
end
- context 'when authenticated as a non-member or member with insufficient rights' do
- %i[access_requester stranger developer].each do |type|
- context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
- post api("/#{source_type.pluralize}/#{source.id}/members", user),
- params: { user_id: access_requester.id, access_level: Member::MAINTAINER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'when authenticated as a non-member or member with insufficient membership management rights' do
+ context 'when the user does not have rights to manage members' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/members", public_send(type)),
+ params: { user_id: access_requester.id, access_level: Member::MAINTAINER }
+ end
+ end
end
end
+ end
- context 'adding a member of higher access level' do
- before do
- # the other 'maintainer' is in fact an owner of the group!
- source.add_maintainer(maintainer2)
- end
+ context 'when the user has the rights to manage members but tries to manage members with a higher access level' do
+ # the other 'maintainer' is in fact an owner of the group!
+ let(:maintainer) { maintainer2 }
- context 'when an access requester' do
- it 'is not successful' do
- post api("/#{source_type.pluralize}/#{source.id}/members", maintainer2),
- params: { user_id: access_requester.id, access_level: Member::OWNER }
+ before do
+ source.add_maintainer(maintainer)
+ end
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'when an access requester is added as OWNER' do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: { user_id: access_requester.id, access_level: Member::OWNER }
end
end
+ end
- context 'when a totally new user' do
- it 'is not successful' do
- post api("/#{source_type.pluralize}/#{source.id}/members", maintainer2),
+ context 'when a totally new user is added as OWNER' do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
params: { user_id: stranger.id, access_level: Member::OWNER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
@@ -561,27 +564,31 @@ RSpec.describe API::Members do
context 'when authenticated as a non-member or member with insufficient rights' do
%i[access_requester stranger developer].each do |type|
context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
- put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
- params: { access_level: Member::MAINTAINER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", public_send(type)),
+ params: { access_level: Member::MAINTAINER }
+ end
end
end
end
context 'as a maintainer updating a member to one with higher access level than themselves' do
+ # the other 'maintainer' is in fact an owner of the group!
+ let(:maintainer) { maintainer2 }
+
before do
# the other 'maintainer' is in fact an owner of the group!
source.add_maintainer(maintainer2)
end
- it 'returns 403' do
- put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer2),
- params: { access_level: Member::OWNER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'updating a member to OWNER' do
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
+ params: { access_level: Member::OWNER }
+ end
+ end
end
end
end
@@ -600,18 +607,19 @@ RSpec.describe API::Members do
context 'when updating a member with higher access level' do
let(:owner) { create(:user) }
+ # the other 'maintainer' is in fact an owner of the group!
+ let(:maintainer) { maintainer2 }
before do
source.add_owner(owner)
- # the other 'maintainer' is in fact an owner of the group!
- source.add_maintainer(maintainer2)
+ source.add_maintainer(maintainer)
end
- it 'returns 403' do
- put api("/#{source_type.pluralize}/#{source.id}/members/#{owner.id}", maintainer2),
- params: { access_level: Member::DEVELOPER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{owner.id}", maintainer),
+ params: { access_level: Member::OWNER }
+ end
end
end
end
@@ -676,11 +684,10 @@ RSpec.describe API::Members do
context 'when authenticated as a non-member or member with insufficient rights' do
%i[access_requester stranger].each do |type|
context "as a #{type}" do
- it 'returns 403' do
- user = public_send(type)
- delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", public_send(type))
+ end
end
end
end
@@ -709,18 +716,18 @@ RSpec.describe API::Members do
context 'when attempting to delete a member with higher access level' do
let(:owner) { create(:user) }
+ # the other 'maintainer' is in fact an owner of the group!
+ let(:maintainer) { maintainer2 }
before do
source.add_owner(owner)
- # the other 'maintainer' is in fact an owner of the group!
- source.add_maintainer(maintainer2)
+ source.add_maintainer(maintainer)
end
- it 'returns 403' do
- delete api("/#{source_type.pluralize}/#{source.id}/members/#{owner.id}", maintainer2),
- params: { access_level: Member::DEVELOPER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{owner.id}", maintainer)
+ end
end
end
@@ -799,11 +806,11 @@ RSpec.describe API::Members do
end
context 'adding owner to project' do
- it 'returns 403' do
- post api("/projects/#{project.id}/members", maintainer),
- params: { user_id: stranger.id, access_level: Member::OWNER }
-
- expect(response).to have_gitlab_http_status(:forbidden)
+ it_behaves_like 'a 403 response when user does not have rights to manage members of a specific access level' do
+ let(:route) do
+ post api("/projects/#{project.id}/members", maintainer),
+ params: { user_id: access_requester.id, access_level: Member::OWNER }
+ end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index d593e369d27..eea223485ce 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -3022,7 +3022,7 @@ RSpec.describe API::MergeRequests do
describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
context 'updates force_remove_source_branch properly' do
it 'sets to false' do
- merge_request.update!(merge_params: { 'force_remove_source_branch' => true } )
+ merge_request.update!(merge_params: { 'force_remove_source_branch' => true })
expect(merge_request.force_remove_source_branch?).to be_truthy
@@ -3034,7 +3034,7 @@ RSpec.describe API::MergeRequests do
end
it 'sets to true' do
- merge_request.update!(merge_params: { 'force_remove_source_branch' => false } )
+ merge_request.update!(merge_params: { 'force_remove_source_branch' => false })
expect(merge_request.force_remove_source_branch?).to be false
@@ -3279,7 +3279,7 @@ RSpec.describe API::MergeRequests do
end
it 'when removing labels, only removes those specified' do
- put api_base, params: { remove_labels: "#{label.title}" }
+ put api_base, params: { remove_labels: label.title.to_s }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label2.title])
diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb
index 5e64ac7d481..a09596f167d 100644
--- a/spec/requests/api/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe API::Metrics::Dashboard::Annotations do
{
'starting_at' => starting_at.to_time,
'ending_at' => ending_at.to_time,
- "#{source_type}" => source,
+ source_type.to_s => source,
'dashboard_path' => dashboard_unescaped,
'description' => params[:description]
}
diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb
index 09e9359c0b3..9448f009742 100644
--- a/spec/requests/api/ml/mlflow_spec.rb
+++ b/spec/requests/api/ml/mlflow_spec.rb
@@ -253,7 +253,7 @@ RSpec.describe API::Ml::Mlflow do
it 'creates the experiment', :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include('experiment_id' )
+ expect(json_response).to include('experiment_id')
end
describe 'Error States' do
@@ -295,7 +295,6 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => params[:experiment_id],
'user_id' => current_user.id.to_s,
'start_time' => params[:start_time],
- 'artifact_uri' => 'not_implemented',
'status' => "RUNNING",
'lifecycle_stage' => "active"
}
@@ -339,7 +338,7 @@ RSpec.describe API::Ml::Mlflow do
'experiment_id' => candidate.experiment.iid.to_s,
'user_id' => candidate.user.id.to_s,
'start_time' => candidate.start_time,
- 'artifact_uri' => 'not_implemented',
+ 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.iid}/-/",
'status' => "RUNNING",
'lifecycle_stage' => "active"
}
@@ -378,7 +377,7 @@ RSpec.describe API::Ml::Mlflow do
'user_id' => candidate.user.id.to_s,
'start_time' => candidate.start_time,
'end_time' => params[:end_time],
- 'artifact_uri' => 'not_implemented',
+ 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.iid}/-/",
'status' => 'FAILED',
'lifecycle_stage' => 'active'
}
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index bdcd6e7278d..373327787a2 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -5,16 +5,29 @@ require 'spec_helper'
RSpec.describe API::NpmProjectPackages do
include_context 'npm api setup'
- describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do
- it_behaves_like 'handling get metadata requests', scope: :project do
- let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") }
+ shared_examples 'accept get request on private project with access to package registry for everyone' do
+ subject { get(url) }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
end
+
+ it_behaves_like 'returning response status', :ok
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do
+ let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") }
+
+ it_behaves_like 'handling get metadata requests', scope: :project
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
- it_behaves_like 'handling get dist tags requests', scope: :project do
- let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") }
- end
+ let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") }
+
+ it_behaves_like 'handling get dist tags requests', scope: :project
+ it_behaves_like 'accept get request on private project with access to package registry for everyone'
end
describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
@@ -108,6 +121,14 @@ RSpec.describe API::NpmProjectPackages do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
+
+ context 'with access to package registry for everyone' do
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ it_behaves_like 'successfully downloads the file'
+ end
end
context 'internal project' do
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index 7d44ff533aa..4a94bf90205 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe API::Pages do
end
it 'removes the pages' do
- delete api("/projects/#{project.id}/pages", admin )
+ delete api("/projects/#{project.id}/pages", admin)
expect(project.reload.pages_metadatum.deployed?).to be(false)
end
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 31c4e8803e3..1fa2ad6ebfa 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -18,9 +18,10 @@ RSpec.describe API::PersonalAccessTokens do
it "status, count and result as expected" do
subject
- if status == :bad_request
+ case status
+ when :bad_request
expect(json_response).to eq(result)
- elsif status == :ok
+ when :ok
expect(map_id(json_response)).to a_collection_containing_exactly(*result)
end
@@ -87,7 +88,7 @@ RSpec.describe API::PersonalAccessTokens do
end
context 'filter with created parameter' do
- let_it_be(:token1) { create(:personal_access_token, created_at: DateTime.new(2022, 01, 01, 12, 30, 25) ) }
+ let_it_be(:token1) { create(:personal_access_token, created_at: DateTime.new(2022, 01, 01, 12, 30, 25)) }
context 'test created_before' do
where(:created_at, :status, :result_count, :result) do
@@ -121,7 +122,7 @@ RSpec.describe API::PersonalAccessTokens do
end
context 'filter with last_used parameter' do
- let_it_be(:token1) { create(:personal_access_token, last_used_at: DateTime.new(2022, 01, 01, 12, 30, 25) ) }
+ let_it_be(:token1) { create(:personal_access_token, last_used_at: DateTime.new(2022, 01, 01, 12, 30, 25)) }
let_it_be(:never_used_token) { create(:personal_access_token) }
context 'test last_used_before' do
@@ -209,13 +210,13 @@ RSpec.describe API::PersonalAccessTokens do
expect(response).to have_gitlab_http_status(status)
- expect(json_response.map { |pat| pat['id'] } ).to include(*result) if status == :ok
+ expect(json_response.map { |pat| pat['id'] }).to include(*result) if status == :ok
end
end
end
context 'filter last_used_before and last_used_after combined is valid' do
- let_it_be(:token1) { create(:personal_access_token, last_used_at: DateTime.new(2022, 01, 02) ) }
+ let_it_be(:token1) { create(:personal_access_token, last_used_at: DateTime.new(2022, 01, 02)) }
where(:last_used_before, :last_used_after, :status, :result) do
'2022-01-02' | '2022-01-02' | :ok | lazy { [token1.id] }
@@ -232,7 +233,7 @@ RSpec.describe API::PersonalAccessTokens do
expect(response).to have_gitlab_http_status(status)
- expect(json_response.map { |pat| pat['id'] } ).to include(*result) if status == :ok
+ expect(json_response.map { |pat| pat['id'] }).to include(*result) if status == :ok
end
end
end
@@ -304,7 +305,7 @@ RSpec.describe API::PersonalAccessTokens do
# Here it is only tested whether PATs to which the user has no access right are excluded from the filter function.
context 'filter with created parameter' do
let_it_be(:token1) do
- create(:personal_access_token, created_at: DateTime.new(2022, 01, 02, 12, 30, 25), user: current_user )
+ create(:personal_access_token, created_at: DateTime.new(2022, 01, 02, 12, 30, 25), user: current_user)
end
let_it_be(:token2) { create(:personal_access_token, created_at: DateTime.new(2022, 01, 02, 12, 30, 25)) }
@@ -332,7 +333,7 @@ RSpec.describe API::PersonalAccessTokens do
create(:personal_access_token, last_used_at: DateTime.new(2022, 01, 01, 12, 30, 25), user: current_user)
end
- let_it_be(:token2) { create(:personal_access_token, last_used_at: DateTime.new(2022, 01, 01, 12, 30, 25) ) }
+ let_it_be(:token2) { create(:personal_access_token, last_used_at: DateTime.new(2022, 01, 01, 12, 30, 25)) }
let_it_be(:never_used_token) { create(:personal_access_token) }
let_it_be(:status) { :ok }
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 0b4a96896d6..2ff4cd72f1e 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -162,6 +162,7 @@ project_setting:
- show_diff_preview_in_email
- suggested_reviewers_enabled
- jitsu_key
+ - mirror_branch_regex
build_service_desk_setting: # service_desk_setting
unexposed_attributes:
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index 506e60d19a6..52ec06d76a9 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -138,14 +138,26 @@ RSpec.describe API::ProjectContainerRepositories do
context 'for maintainer' do
let(:api_user) { maintainer }
- it 'schedules removal of repository' do
- expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
- .with(maintainer.id, root_repository.id)
-
- subject
+ it 'marks the repository as delete_scheduled' do
+ expect(DeleteContainerRepositoryWorker).not_to receive(:perform_async)
+ expect { subject }.to change { root_repository.reload.status }.from(nil).to('delete_scheduled')
expect(response).to have_gitlab_http_status(:accepted)
end
+
+ context 'with container_registry_delete_repository_with_cron_worker disabled' do
+ before do
+ stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
+ end
+
+ it 'schedules removal of repository' do
+ expect(DeleteContainerRepositoryWorker).to receive(:perform_async)
+ .with(maintainer.id, root_repository.id)
+ expect { subject }.to change { root_repository.reload.status }.from(nil).to('delete_scheduled')
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
end
end
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 401db766589..05fe55b06a1 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
it 'executes a limited number of queries' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
- expect(control_count).to be <= 110
+ expect(control_count).to be <= 111
end
it 'schedules an import using a namespace' do
@@ -215,7 +215,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to eq('Name has already been taken')
+ expect(json_response['message']).to eq('Project namespace name has already been taken')
end
context 'when param overwrite is true' do
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index b23fb86a9de..8294ca143d3 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::ProjectMilestones do
let_it_be(:user) { create(:user) }
- let_it_be_with_reload(:project) { create(:project, namespace: user.namespace ) }
+ let_it_be_with_reload(:project) { create(:project, namespace: user.namespace) }
let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
let_it_be(:route) { "/projects/#{project.id}/milestones" }
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 6e2dd6e76a9..1d255f7c1d8 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -89,7 +89,7 @@ RSpec.describe API::ProjectSnippets do
expect(json_response['title']).to eq(snippet.title)
expect(json_response['description']).to eq(snippet.description)
expect(json_response['file_name']).to eq(snippet.file_name_on_repo)
- expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) } )
+ expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) })
expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo)
expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 38f7d6e3eba..3831e8e1dfe 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -169,10 +169,8 @@ RSpec.describe API::Projects do
shared_examples_for 'projects response without N + 1 queries' do |threshold|
let(:additional_project) { create(:project, :public) }
- it 'avoids N + 1 queries' do
- get api('/projects', current_user)
-
- control = ActiveRecord::QueryRecorder.new do
+ it 'avoids N + 1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api('/projects', current_user)
end
@@ -180,7 +178,7 @@ RSpec.describe API::Projects do
expect do
get api('/projects', current_user)
- end.not_to exceed_query_limit(control).with_threshold(threshold)
+ end.not_to exceed_all_query_limit(control).with_threshold(threshold)
end
end
@@ -209,16 +207,28 @@ RSpec.describe API::Projects do
let(:current_user) { user }
end
- it 'includes container_registry_access_level', :aggregate_failures do
- project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
+ shared_examples 'includes container_registry_access_level', :aggregate_failures do
+ it do
+ project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
- get api('/projects', user)
- project_response = json_response.find { |p| p['id'] == project.id }
+ 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_access_level']).to eq('disabled')
- expect(project_response['container_registry_enabled']).to eq(false)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(project_response['container_registry_access_level']).to eq('disabled')
+ expect(project_response['container_registry_enabled']).to eq(false)
+ end
+ end
+
+ include_examples 'includes container_registry_access_level'
+
+ context 'when projects_preloader_fix is disabled' do
+ before do
+ stub_feature_flags(projects_preloader_fix: false)
+ end
+
+ include_examples 'includes container_registry_access_level'
end
it 'includes releases_access_level', :aggregate_failures do
@@ -1055,7 +1065,7 @@ RSpec.describe API::Projects do
let_it_be(:admin) { create(:admin) }
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
get api('/projects', admin)
base_project = create(:project, :public, namespace: admin.namespace)
@@ -1063,7 +1073,7 @@ RSpec.describe API::Projects do
fork_project1 = fork_project(base_project, admin, namespace: create(:user).namespace)
fork_project2 = fork_project(fork_project1, admin, namespace: create(:user).namespace)
- control = ActiveRecord::QueryRecorder.new do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api('/projects', admin)
end
@@ -3428,18 +3438,6 @@ RSpec.describe API::Projects do
end
context 'when authenticated as project owner' do
- it 'updates name' do
- project_param = { name: 'bar' }
-
- 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 visibility_level' do
project_param = { visibility: 'public' }
@@ -3797,10 +3795,16 @@ RSpec.describe API::Projects do
expect(json_response['message']['path']).to eq(['has already been taken'])
end
- it 'does not update name' do
+ it 'updates name' do
project_param = { name: 'bar' }
- put api("/projects/#{project3.id}", user4), params: project_param
- expect(response).to have_gitlab_http_status(:forbidden)
+
+ 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 'does not update visibility_level' do
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 9f10eb1bb9f..b46859a0e70 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -29,8 +29,7 @@ RSpec.describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
-
+ expect(response).to match_response_schema('protected_branches')
protected_branch_names = json_response.map { |x| x['name'] }
expect(protected_branch_names).to match_array(expected_branch_names)
end
@@ -71,6 +70,7 @@ RSpec.describe API::ProtectedBranches do
get api(route, user)
expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
@@ -130,6 +130,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
@@ -140,6 +141,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name, push_access_level: 30 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::DEVELOPER)
@@ -150,6 +152,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name, merge_access_level: 30 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
@@ -160,6 +163,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name, push_access_level: 30, merge_access_level: 30 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::DEVELOPER)
@@ -170,6 +174,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name, push_access_level: 0 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::NO_ACCESS)
@@ -180,6 +185,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name, merge_access_level: 0 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
@@ -190,6 +196,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name, push_access_level: 0, merge_access_level: 0 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(false)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::NO_ACCESS)
@@ -200,6 +207,7 @@ RSpec.describe API::ProtectedBranches do
post post_endpoint, params: { name: branch_name, allow_force_push: true }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_branch')
expect(json_response['name']).to eq(branch_name)
expect(json_response['allow_force_push']).to eq(true)
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
@@ -246,6 +254,49 @@ RSpec.describe API::ProtectedBranches do
end
end
+ describe 'PATCH /projects/:id/protected_branches/:name' do
+ let(:route) { "/projects/#{project.id}/protected_branches/#{branch_name}" }
+
+ context 'when authenticated as a maintainer' do
+ let(:user) { maintainer }
+
+ it "updates a single branch" do
+ expect do
+ patch api(route, user), params: { allow_force_push: true }
+ end.to change { protected_branch.reload.allow_force_push }.from(false).to(true)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when returned protected branch is invalid' do
+ let(:user) { maintainer }
+
+ before do
+ allow_next_found_instance_of(ProtectedBranch) do |instance|
+ allow(instance).to receive(:valid?).and_return(false)
+ end
+ end
+
+ it "returns a 422" do
+ expect do
+ patch api(route, user), params: { allow_force_push: true }
+ end.not_to change { protected_branch.reload.allow_force_push }
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'when authenticated as a guest' do
+ let(:user) { guest }
+
+ it "returns a 403 error" do
+ patch api(route, user), params: { allow_force_push: true }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
describe "DELETE /projects/:id/protected_branches/unprotect/:branch" do
let(:user) { maintainer }
let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb
index 84b7df86f31..f1db39ac204 100644
--- a/spec/requests/api/protected_tags_spec.rb
+++ b/spec/requests/api/protected_tags_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe API::ProtectedTags do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+ expect(response).to match_response_schema('protected_tags')
protected_tag_names = json_response.map { |x| x['name'] }
expected_tags_names = project.protected_tags.map { |x| x['name'] }
@@ -57,6 +57,7 @@ RSpec.describe API::ProtectedTags do
get api(route, user)
expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('protected_tag')
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(::Gitlab::Access::MAINTAINER)
end
@@ -108,6 +109,7 @@ RSpec.describe API::ProtectedTags do
post api("/projects/#{project.id}/protected_tags", user), params: { name: tag_name }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_tag')
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
@@ -117,6 +119,7 @@ RSpec.describe API::ProtectedTags do
params: { name: tag_name, create_access_level: 30 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_tag')
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::DEVELOPER)
end
@@ -126,6 +129,7 @@ RSpec.describe API::ProtectedTags do
params: { name: tag_name, create_access_level: 0 }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_tag')
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::NO_ACCESS)
end
@@ -142,6 +146,7 @@ RSpec.describe API::ProtectedTags do
post api("/projects/#{project2.id}/protected_tags", user), params: { name: protected_name }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_tag')
expect(json_response['name']).to eq(protected_name)
end
@@ -152,6 +157,7 @@ RSpec.describe API::ProtectedTags do
post api("/projects/#{project.id}/protected_tags", user), params: { name: tag_name }
expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('protected_tag')
expect(json_response['name']).to eq(tag_name)
expect(json_response['create_access_levels'][0]['access_level']).to eq(Gitlab::Access::MAINTAINER)
end
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 6c130bb4963..12091158a02 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -69,6 +69,7 @@ RSpec.describe API::PypiPackages do
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'
+ it_behaves_like 'allow access for everyone with public package_registry_access_level'
context 'with project path as id' do
let(:url) { "/projects/#{CGI.escape(project.full_path)}/packages/pypi/simple" }
@@ -130,6 +131,7 @@ RSpec.describe API::PypiPackages do
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'
+ it_behaves_like 'allow access for everyone with public package_registry_access_level'
context 'with project path as id' do
let(:url) { "/projects/#{CGI.escape(project.full_path)}/packages/pypi/simple/#{package.name}" }
@@ -377,6 +379,7 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'pypi file download endpoint'
it_behaves_like 'rejects PyPI access with unknown project id'
+ it_behaves_like 'allow access for everyone with public package_registry_access_level'
end
end
end
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index 57b2e005929..38166c5ce97 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -81,24 +81,20 @@ RSpec.describe API::Release::Links do
end
context 'when project is public' do
- let(:project) { create(:project, :repository, :public) }
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
it 'allows the request' do
get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member)
expect(response).to have_gitlab_http_status(:ok)
end
- end
-
- context 'when project is public and the repository is private' do
- let(:project) { create(:project, :repository, :public, :repository_private) }
-
- it_behaves_like '403 response' do
- let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) }
- end
- context 'when the release does not exists' do
- let!(:release) {}
+ context 'and the releases are private' do
+ before do
+ project.project_feature.update!(releases_access_level: ProjectFeature::PRIVATE)
+ end
it_behaves_like '403 response' do
let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) }
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index d9a12e7e148..24efac3128d 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -243,65 +243,33 @@ RSpec.describe API::ResourceAccessTokens do
end
context "when the user has valid permissions" do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it "deletes the #{source_type} access token from the #{source_type}" do
- delete_token
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(
- Users::GhostUserMigration.where(user: project_bot,
- initiator_user: user)
- ).to be_exists
- end
-
- context "when using #{source_type} access token to DELETE other #{source_type} access token" do
- let_it_be(:other_project_bot) { create(:user, :project_bot) }
- let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
- let_it_be(:token_id) { other_token.id }
-
- before do
- resource.add_maintainer(other_project_bot)
- end
-
- it "deletes the #{source_type} access token from the #{source_type}" do
- delete_token
+ it "deletes the #{source_type} access token from the #{source_type}" do
+ delete_token
- expect(response).to have_gitlab_http_status(:no_content)
- expect(
- Users::GhostUserMigration.where(user: other_project_bot,
- initiator_user: user)
- ).to be_exists
- end
- end
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(
+ Users::GhostUserMigration.where(user: project_bot,
+ initiator_user: user)
+ ).to be_exists
end
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
+ context "when using #{source_type} access token to DELETE other #{source_type} access token" do
+ let_it_be(:other_project_bot) { create(:user, :project_bot) }
+ let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
+ let_it_be(:token_id) { other_token.id }
+
before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
+ resource.add_maintainer(other_project_bot)
end
it "deletes the #{source_type} access token from the #{source_type}" do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
- expect(User.exists?(project_bot.id)).to be_falsy
- end
-
- context "when using #{source_type} access token to DELETE other #{source_type} access token" do
- let_it_be(:other_project_bot) { create(:user, :project_bot) }
- let_it_be(:other_token) { create(:personal_access_token, user: other_project_bot) }
- let_it_be(:token_id) { other_token.id }
-
- before do
- resource.add_maintainer(other_project_bot)
- end
-
- it "deletes the #{source_type} access token from the #{source_type}" do
- delete_token
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(User.exists?(other_project_bot.id)).to be_falsy
- end
+ expect(
+ Users::GhostUserMigration.where(user: other_project_bot,
+ initiator_user: user)
+ ).to be_exists
end
end
@@ -416,6 +384,41 @@ RSpec.describe API::ResourceAccessTokens do
expect(response.body).to include("scopes is missing")
end
end
+
+ context "when using invalid 'scopes'" do
+ let_it_be(:params) do
+ {
+ name: "test",
+ scopes: ["test"],
+ expires_at: 5.days.from_now
+ }
+ end
+
+ it "does not create a #{source_type} access token with invalid 'scopes'", :aggregate_failures do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include("scopes does not have a valid value")
+ end
+ end
+
+ context "when using invalid 'access_level'" do
+ let_it_be(:params) do
+ {
+ name: "test",
+ scopes: ["api"],
+ expires_at: 5.days.from_now,
+ access_level: Gitlab::Access::NO_ACCESS
+ }
+ end
+
+ it "does not create a #{source_type} access token with invalid 'access_level'", :aggregate_failures do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include("access_level does not have a valid value")
+ end
+ end
end
context "when trying to create a token in a different #{source_type}" do
diff --git a/spec/requests/api/rpm_project_packages_spec.rb b/spec/requests/api/rpm_project_packages_spec.rb
index 6a646c26fd2..68511795c94 100644
--- a/spec/requests/api/rpm_project_packages_spec.rb
+++ b/spec/requests/api/rpm_project_packages_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe API::RpmProjectPackages do
using RSpec::Parameterized::TableSyntax
- let_it_be_with_reload(:project) { create(:project, :public) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be_with_reload(:project) { create(:project, :public, group: group) }
let_it_be(:user) { create(:user) }
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) }
@@ -36,7 +37,7 @@ RSpec.describe API::RpmProjectPackages do
it_behaves_like 'returning response status', status
end
- shared_examples 'a deploy token for RPM requests' do
+ shared_examples 'a deploy token for RPM requests' do |success_status = :not_found|
context 'with deploy token headers' do
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
@@ -45,7 +46,7 @@ RSpec.describe API::RpmProjectPackages do
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) }
context 'when token is valid' do
- it_behaves_like 'returning response status', :not_found
+ it_behaves_like 'returning response status', success_status
end
context 'when token is invalid' do
@@ -56,7 +57,7 @@ RSpec.describe API::RpmProjectPackages do
end
end
- shared_examples 'a job token for RPM requests' do
+ shared_examples 'a job token for RPM requests' do |success_status = :not_found|
context 'with job token headers' do
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
@@ -66,7 +67,7 @@ RSpec.describe API::RpmProjectPackages do
end
context 'with valid token' do
- it_behaves_like 'returning response status', :not_found
+ it_behaves_like 'returning response status', success_status
end
context 'with invalid token' do
@@ -83,10 +84,10 @@ RSpec.describe API::RpmProjectPackages do
end
end
- shared_examples 'a user token for RPM requests' do
+ shared_examples 'a user token for RPM requests' do |success_status = :not_found|
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | success_status
'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
@@ -95,7 +96,7 @@ RSpec.describe API::RpmProjectPackages do
'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
+ 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | success_status
'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
@@ -123,26 +124,31 @@ RSpec.describe API::RpmProjectPackages do
end
describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do
- let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } }
+ let(:repository_file) { create(:rpm_repository_file, project: project) }
+ let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{repository_file.file_name}" }
subject { get api(url), headers: headers }
- it_behaves_like 'a job token for RPM requests'
- it_behaves_like 'a deploy token for RPM requests'
- it_behaves_like 'a user token for RPM requests'
+ it_behaves_like 'a job token for RPM requests', :success
+ it_behaves_like 'a deploy token for RPM requests', :success
+ it_behaves_like 'a user token for RPM requests', :success
end
describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: group } }
let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
subject { get api(url), headers: headers }
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
it_behaves_like 'a job token for RPM requests'
it_behaves_like 'a deploy token for RPM requests'
it_behaves_like 'a user token for RPM requests'
end
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, user: user } }
let(:url) { "/projects/#{project.id}/packages/rpm" }
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/rpm/hello-0.0.1-1.fc29.x86_64.rpm') }
@@ -150,25 +156,25 @@ RSpec.describe API::RpmProjectPackages do
context 'with user token' do
context 'with valid project' do
- where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
- 'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden
- 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found
- 'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found
- 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
- 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status, :tracked) do
+ 'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found | true
+ 'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden | false
+ 'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found | false
+ 'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found | false
+ 'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized | false
+ 'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found | true
+ 'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden | false
+ 'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found | false
+ 'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found | false
+ 'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized | false
+ 'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized | false
end
with_them do
@@ -180,6 +186,8 @@ RSpec.describe API::RpmProjectPackages do
project.send("add_#{user_role}", user) if member && user_role != :anonymous
end
+ tracking_example = params[:tracked] ? 'a package tracking event' : 'not a package tracking event'
+ it_behaves_like tracking_example, described_class.name, 'push_package'
it_behaves_like params[:shared_examples_name], params[:expected_status]
end
end
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index f0408d94137..a7d461781b8 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -325,9 +325,10 @@ RSpec.describe API::RubygemPackages do
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
+ case token_type
+ when :deploy_token
deploy_token
- elsif token_type == :job_token
+ when :job_token
job.user
else
user
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 05f38aff6ab..60acf6b71dd 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -29,6 +29,19 @@ RSpec.describe API::Search do
end
end
+ shared_examples 'apdex recorded' do |scope:, level:, search: ''|
+ it 'increments the custom search sli apdex' do
+ expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with(
+ elapsed: a_kind_of(Numeric),
+ search_scope: scope,
+ search_type: 'basic',
+ search_level: level
+ )
+
+ get api(endpoint, user), params: { scope: scope, search: search }
+ end
+ end
+
shared_examples 'orderable by created_at' do |scope:|
it 'allows ordering results by created_at asc' do
get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'asc' }
@@ -172,6 +185,8 @@ RSpec.describe API::Search do
it_behaves_like 'pagination', scope: :projects
it_behaves_like 'ping counters', scope: :projects
+
+ it_behaves_like 'apdex recorded', scope: 'projects', level: 'global'
end
context 'for issues scope' do
@@ -186,6 +201,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
+ it_behaves_like 'apdex recorded', scope: 'issues', level: 'global'
+
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
@@ -248,6 +265,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
+ it_behaves_like 'apdex recorded', scope: 'merge_requests', level: 'global'
+
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
@@ -293,6 +312,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :milestones
+ it_behaves_like 'apdex recorded', scope: 'milestones', level: 'global'
+
describe 'pagination' do
before do
create(:milestone, project: project, title: 'another milestone')
@@ -330,6 +351,8 @@ RSpec.describe API::Search do
it_behaves_like 'pagination', scope: :users
it_behaves_like 'ping counters', scope: :users
+
+ it_behaves_like 'apdex recorded', scope: 'users', level: 'global'
end
context 'for snippet_titles scope' do
@@ -343,6 +366,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :snippet_titles
+ it_behaves_like 'apdex recorded', scope: 'snippet_titles', level: 'global'
+
describe 'pagination' do
before do
create(:snippet, :public, title: 'another snippet', content: 'snippet content')
@@ -352,17 +377,6 @@ RSpec.describe API::Search do
end
end
- it 'increments the custom search sli apdex' do
- expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_apdex).with(
- elapsed: a_kind_of(Numeric),
- search_scope: 'issues',
- search_type: 'basic',
- search_level: 'global'
- )
-
- get api(endpoint, user), params: { scope: 'issues', search: 'john doe' }
- end
-
it 'increments the custom search sli error rate with error false if no error occurred' do
expect(Gitlab::Metrics::GlobalSearchSlis).to receive(:record_error_rate).with(
error: false,
@@ -466,6 +480,8 @@ RSpec.describe API::Search do
it_behaves_like 'pagination', scope: :projects
it_behaves_like 'ping counters', scope: :projects
+
+ it_behaves_like 'apdex recorded', scope: 'projects', level: 'group'
end
context 'for issues scope' do
@@ -479,6 +495,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :issues
+ it_behaves_like 'apdex recorded', scope: 'issues', level: 'group'
+
it_behaves_like 'issues orderable by created_at'
describe 'pagination' do
@@ -501,6 +519,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :merge_requests
+ it_behaves_like 'apdex recorded', scope: 'merge_requests', level: 'group'
+
it_behaves_like 'merge_requests orderable by created_at'
describe 'pagination' do
@@ -523,6 +543,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :milestones
+ it_behaves_like 'apdex recorded', scope: 'milestones', level: 'group'
+
describe 'pagination' do
before do
create(:milestone, project: project, title: 'another milestone')
@@ -556,6 +578,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :users
+ it_behaves_like 'apdex recorded', scope: 'users', level: 'group'
+
describe 'pagination' do
before do
create(:group_member, :developer, group: group)
@@ -645,6 +669,8 @@ RSpec.describe API::Search do
it_behaves_like 'issues orderable by created_at'
+ it_behaves_like 'apdex recorded', scope: 'issues', level: 'project'
+
describe 'pagination' do
before do
create(:issue, project: project, title: 'another issue')
@@ -677,6 +703,8 @@ RSpec.describe API::Search do
it_behaves_like 'merge_requests orderable by created_at'
+ it_behaves_like 'apdex recorded', scope: 'merge_requests', level: 'project'
+
describe 'pagination' do
before do
create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
@@ -700,6 +728,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :milestones
+ it_behaves_like 'apdex recorded', scope: 'milestones', level: 'project'
+
describe 'pagination' do
before do
create(:milestone, project: project, title: 'another milestone')
@@ -737,6 +767,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :users
+ it_behaves_like 'apdex recorded', scope: 'users', level: 'project'
+
describe 'pagination' do
before do
create(:project_member, :developer, project: project)
@@ -757,6 +789,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :notes
+ it_behaves_like 'apdex recorded', scope: 'notes', level: 'project'
+
describe 'pagination' do
before do
mr = create(:merge_request, source_project: project, target_branch: 'another_branch')
@@ -780,6 +814,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :wiki_blobs
+ it_behaves_like 'apdex recorded', scope: 'wiki_blobs', level: 'project'
+
describe 'pagination' do
before do
create(:wiki_page, wiki: wiki, title: 'home 2', content: 'Another page')
@@ -802,6 +838,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :commits
+ it_behaves_like 'apdex recorded', scope: 'commits', level: 'project'
+
describe 'pipeline visibility' do
shared_examples 'pipeline information visible' do
it 'contains status and last_pipeline' do
@@ -899,6 +937,8 @@ RSpec.describe API::Search do
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
+
+ it_behaves_like 'apdex recorded', scope: 'commits', level: 'project'
end
context 'for blobs scope' do
@@ -914,6 +954,8 @@ RSpec.describe API::Search do
it_behaves_like 'ping counters', scope: :blobs
+ it_behaves_like 'apdex recorded', scope: 'blobs', level: 'project'
+
context 'filters' do
it 'by filename' do
get api(endpoint, user), params: { scope: 'blobs', search: 'mon filename:PROCESS.md' }
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 031bcb612f4..9408d1cc248 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.map { |snippet| snippet['id'] } ).to contain_exactly(
+ expect(json_response.map { |snippet| snippet['id'] }).to contain_exactly(
public_snippet.id,
internal_snippet.id,
private_snippet.id)
@@ -75,7 +75,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
it 'returns snippets available for user in given time range' do
get api(path, personal_access_token: user_token)
- expect(json_response.map { |snippet| snippet['id'] } ).to contain_exactly(
+ expect(json_response.map { |snippet| snippet['id'] }).to contain_exactly(
private_snippet_in_time_range1.id,
private_snippet_in_time_range2.id)
end
@@ -99,10 +99,10 @@ RSpec.describe API::Snippets, factory_default: :keep do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.map { |snippet| snippet['id'] } ).to contain_exactly(
+ expect(json_response.map { |snippet| snippet['id'] }).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
- expect(json_response.map { |snippet| snippet['web_url'] } ).to contain_exactly(
+ expect(json_response.map { |snippet| snippet['web_url'] }).to contain_exactly(
"http://localhost/-/snippets/#{public_snippet.id}",
"http://localhost/-/snippets/#{public_snippet_other.id}")
expect(json_response[0]['files'].first).to eq snippet_blob_file(public_snippet_other.blobs.first)
@@ -126,7 +126,7 @@ RSpec.describe API::Snippets, factory_default: :keep do
it 'returns public snippets available to user in given time range' do
get api(path, personal_access_token: user_token)
- expect(json_response.map { |snippet| snippet['id'] } ).to contain_exactly(
+ expect(json_response.map { |snippet| snippet['id'] }).to contain_exactly(
public_snippet_in_time_range.id)
end
end
diff --git a/spec/requests/api/submodules_spec.rb b/spec/requests/api/submodules_spec.rb
index 6b141d6d036..9840476ca27 100644
--- a/spec/requests/api/submodules_spec.rb
+++ b/spec/requests/api/submodules_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::Submodules do
let(:user) { create(:user) }
- let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+ let!(:project) { create(:project, :repository, namespace: user.namespace) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
let(:submodule) { 'six' }
let(:commit_sha) { 'e25eda1fece24ac7a03624ed1320f82396f35bd8' }
diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb
index 2393a268693..dfc5d169af6 100644
--- a/spec/requests/api/suggestions_spec.rb
+++ b/spec/requests/api/suggestions_spec.rb
@@ -91,7 +91,7 @@ RSpec.describe API::Suggestions do
end
context 'when suggestion is not found' do
- let(:url) { "/suggestions/foo-123/apply" }
+ let(:url) { "/suggestions/9999/apply" }
it 'renders a not found error and returns json content' do
project.add_maintainer(user)
@@ -103,6 +103,19 @@ RSpec.describe API::Suggestions do
end
end
+ context 'when suggestion ID is not valid' do
+ let(:url) { "/suggestions/foo-123/apply" }
+
+ it 'renders a not found error and returns json content' do
+ project.add_maintainer(user)
+
+ put api(url, user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq({ 'error' => 'id is invalid' })
+ end
+ end
+
context 'when unauthorized' do
it 'renders a forbidden error and returns json content' do
project.add_reporter(user)
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index c635d73efe3..3f2ca2a0938 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe API::Tags do
context 'searching' do
it 'only returns searched tags' do
- get api("#{route}", user), params: { search: 'v1.1.0' }
+ get api(route.to_s, user), params: { search: 'v1.1.0' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index dff44a45de4..ae61017f5bb 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -585,9 +585,10 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
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
+ case token_type
+ when :deploy_token
deploy_token
- elsif token_type == :job_token
+ when :job_token
job.user
else
user
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index e8458db4a4d..38b08b4e214 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -46,26 +46,19 @@ RSpec.describe API::Terraform::State, :snowplow do
let(:expected_value) { instance_of(Integer) }
end
- it 'tracks Snowplow event' do
- request
-
- expect_snowplow_event(
- category: described_class.to_s,
- action: 'p_terraform_state_api_unique_users',
- namespace: project.namespace.reload,
- user: current_user
- )
- end
-
- context 'when route_hll_to_snowplow_phase2 FF is disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'does not track Snowplow event' do
- request
-
- expect_no_snowplow_event
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ subject(:api_request) { request }
+
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'terraform_state_api_request' }
+ let(:label) { 'redis_hll_counters.terraform.p_terraform_state_api_unique_users_monthly' }
+ let(:namespace) { project.namespace.reload }
+ let(:user) { current_user }
+ let(:context) do
+ payload = Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: 'p_terraform_state_api_unique_users').to_context
+ [Gitlab::Json.dump(payload)]
end
end
end
@@ -318,7 +311,7 @@ RSpec.describe API::Terraform::State, :snowplow do
Version: '0.1',
Operation: 'OperationTypePlan',
Info: '',
- Who: "#{current_user.username}",
+ Who: current_user.username.to_s,
Created: Time.now.utc.iso8601(6),
Path: ''
}
@@ -365,7 +358,7 @@ RSpec.describe API::Terraform::State, :snowplow do
Version: '0.1',
Operation: 'OperationTypePlan',
Info: '',
- Who: "#{current_user.username}",
+ Who: current_user.username.to_s,
Created: Time.now.utc.iso8601(6),
Path: ''
}
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 0fcb6412a2d..7a626ee4d29 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -231,7 +231,7 @@ RSpec.describe API::Todos do
create(:on_commit_todo, project: new_todo.project, author: author_1, user: john_doe, target: merge_request_3)
create(:todo, project: new_todo.project, author: author_2, user: john_doe, target: merge_request_3)
- expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(4)
+ expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control1).with_threshold(5)
control2 = ActiveRecord::QueryRecorder.new { get api('/todos', john_doe) }
create_issue_todo_for(john_doe)
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 51c567309b7..4d382f91023 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -202,18 +202,6 @@ RSpec.describe API::Unleash do
3.times { get api(features_url), params: params, headers: headers }
end
-
- context 'when cache_unleash_client_api is disabled' do
- before do
- stub_feature_flags(cache_unleash_client_api: false)
- end
-
- it 'serializes feature flags every time' do
- expect(::API::Entities::UnleashFeature).to receive(:represent).exactly(5).times
-
- 5.times { get api(features_url), params: params, headers: headers }
- end
- end
end
context 'with version 2 feature flags' do
diff --git a/spec/requests/api/user_counts_spec.rb b/spec/requests/api/user_counts_spec.rb
index ab2aa87d1b7..369ae49de08 100644
--- a/spec/requests/api/user_counts_spec.rb
+++ b/spec/requests/api/user_counts_spec.rb
@@ -26,24 +26,22 @@ RSpec.describe API::UserCounts do
expect(json_response['assigned_issues']).to eq(1)
end
- context 'merge requests' do
- it 'returns assigned MR counts for current user' do
- get api('/user_counts', user)
+ it 'returns assigned MR counts for current user' do
+ get api('/user_counts', user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a Hash
- expect(json_response['merge_requests']).to eq(1)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a Hash
+ expect(json_response['merge_requests']).to eq(1)
+ end
- it 'updates the mr count when a new mr is assigned' do
- create(:merge_request, source_project: project, author: user, assignees: [user])
+ it 'updates the mr count when a new mr is assigned' do
+ create(:merge_request, source_project: project, author: user, assignees: [user])
- get api('/user_counts', user)
+ get api('/user_counts', user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a Hash
- expect(json_response['merge_requests']).to eq(2)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a Hash
+ expect(json_response['merge_requests']).to eq(2)
end
it 'returns pending todo counts for current_user' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 1b0a27e78e3..6688a998a1a 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1291,6 +1291,20 @@ RSpec.describe API::Users do
.to eq([Gitlab::PathRegex.namespace_format_message])
end
+ it 'tracks weak password errors' do
+ attributes = attributes_for(:user).merge({ password: "password" })
+ post api('/users', admin), params: attributes
+
+ expect(json_response['message']['password'])
+ .to eq(['must not contain commonly used combinations of words and letters'])
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'API::Users',
+ method: 'create'
+ )
+ end
+
it "is not available for non admin users" do
post api("/users", user), params: attributes_for(:user)
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1492,6 +1506,21 @@ RSpec.describe API::Users do
.not_to have_enqueued_mail(DeviseMailer, :password_change)
end
end
+
+ context 'with a weak password' do
+ it 'tracks weak password errors' do
+ update_password(user, admin, "password")
+
+ expect(json_response['message']['password'])
+ .to eq(['must not contain commonly used combinations of words and letters'])
+ expect_snowplow_event(
+ category: 'Gitlab::Tracking::Helpers::WeakPasswordErrorEvent',
+ action: 'track_weak_password_error',
+ controller: 'API::Users',
+ method: 'update'
+ )
+ end
+ end
end
it "updates user with new bio" do
@@ -2535,32 +2564,12 @@ RSpec.describe API::Users do
describe "DELETE /users/:id" do
let_it_be(:issue) { create(:issue, author: user) }
- context 'user deletion' do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it "deletes user", :sidekiq_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(Users::GhostUserMigration.where(user: user,
- initiator_user: admin)).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it "deletes user", :sidekiq_inline do
- namespace_id = user.namespace.id
-
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ it "deletes user", :sidekiq_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
- expect(response).to have_gitlab_http_status(:no_content)
- expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
- expect { Namespace.find(namespace_id) }.to raise_error ActiveRecord::RecordNotFound
- end
- end
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)).to be_exists
end
context "sole owner of a group" do
@@ -2624,55 +2633,26 @@ RSpec.describe API::Users do
expect(response).to have_gitlab_http_status(:not_found)
end
- context 'hard delete' do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- context "hard delete disabled" do
- it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(issue.reload).to be_persisted
- expect(Users::GhostUserMigration.where(user: user,
- initiator_user: admin,
- hard_delete: false)).to be_exists
- end
- end
-
- context "hard delete enabled" do
- it "removes contributions", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+ context "hard delete disabled" do
+ it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
- expect(response).to have_gitlab_http_status(:no_content)
- expect(Users::GhostUserMigration.where(user: user,
- initiator_user: admin,
- hard_delete: true)).to be_exists
- end
- end
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(issue.reload).to be_persisted
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: false)).to be_exists
end
+ end
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- context "hard delete disabled" do
- it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(issue.reload).to be_persisted
- expect(issue.author.ghost?).to be_truthy
- end
- end
-
- context "hard delete enabled" do
- it "removes contributions", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+ context "hard delete enabled" do
+ it "removes contributions", :sidekiq_might_not_need_inline do
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
- expect(response).to have_gitlab_http_status(:no_content)
- expect(Issue.exists?(issue.id)).to be_falsy
- end
- end
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(Users::GhostUserMigration.where(user: user,
+ initiator_user: admin,
+ hard_delete: true)).to be_exists
end
end
end
@@ -4395,6 +4375,74 @@ RSpec.describe API::Users do
end
end
+ describe 'GET /users/:id/associations_count' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let(:associations) do
+ {
+ groups_count: 1,
+ projects_count: 1,
+ issues_count: 2,
+ merge_requests_count: 1
+ }.as_json
+ end
+
+ before :all do
+ group.add_member(user, Gitlab::Access::OWNER)
+ project.add_member(user, Gitlab::Access::OWNER)
+ create(:merge_request, source_project: project, source_branch: "my-personal-branch-1", author: user)
+ create_list(:issue, 2, project: project, author: user)
+ end
+
+ context 'as an unauthorized user' do
+ it 'returns 401 unauthorized' do
+ get api("/users/#{user.id}/associations_count", nil)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'as a non-admin user' do
+ context 'with a different user id' do
+ it 'returns 403 Forbidden' do
+ get api("/users/#{omniauth_user.id}/associations_count", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'with the current user id' do
+ it 'returns valid JSON response' do
+ get api("/users/#{user.id}/associations_count", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a Hash
+ expect(json_response).to match(associations)
+ end
+ end
+ end
+
+ context 'as an admin user' do
+ context 'with invalid user id' do
+ it 'returns 404 User Not Found' do
+ get api("/users/#{non_existing_record_id}/associations_count", admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with valid user id' do
+ it 'returns valid JSON response' do
+ get api("/users/#{user.id}/associations_count", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a Hash
+ expect(json_response).to match(associations)
+ end
+ end
+ end
+ end
+
it_behaves_like 'custom attributes endpoints', 'users' do
let(:attributable) { user }
let(:other_attributable) { admin }
diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb
index 9be013d4385..a08231fe939 100644
--- a/spec/requests/groups/observability_controller_spec.rb
+++ b/spec/requests/groups/observability_controller_spec.rb
@@ -8,23 +8,16 @@ RSpec.describe Groups::ObservabilityController do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
- subject do
- get group_observability_index_path(group)
- response
- end
+ let(:observability_url) { Gitlab::Observability.observability_url }
+ let(:expected_observability_path) { "/" }
- describe 'GET #index' do
- context 'when user is not authenticated' do
- it 'returns 404' do
- expect(subject).to have_gitlab_http_status(:not_found)
- end
+ shared_examples 'observability route request' do
+ subject do
+ get path
+ response
end
- context 'when observability url is missing' do
- before do
- allow(described_class).to receive(:observability_url).and_return("")
- end
-
+ context 'when user is not authenticated' do
it 'returns 404' do
expect(subject).to have_gitlab_http_status(:not_found)
end
@@ -46,6 +39,16 @@ RSpec.describe Groups::ObservabilityController do
group.add_developer(user)
end
+ context 'when observability url is missing' do
+ before do
+ allow(Gitlab::Observability).to receive(:observability_url).and_return("")
+ end
+
+ it 'returns 404' do
+ expect(subject).to have_gitlab_http_status(:not_found)
+ end
+ end
+
it 'returns 200' do
expect(subject).to have_gitlab_http_status(:ok)
end
@@ -55,135 +58,112 @@ RSpec.describe Groups::ObservabilityController do
expect(subject).to render_template("layouts/fullscreen")
expect(subject).not_to render_template('layouts/nav/breadcrumbs')
expect(subject).to render_template("nav/sidebar/_group")
+ expect(subject).to render_template("groups/observability/observability")
end
- describe 'iframe' do
- subject do
- get group_observability_index_path(group)
- Nokogiri::HTML.parse(response.body).at_css('iframe#observability-ui-iframe')
- end
-
- it 'sets the iframe src to the proper URL' do
- expect(subject.attributes['src'].value).to eq("https://observe.gitlab.com/-/#{group.id}")
- end
-
- it 'when the env is staging, sets the iframe src to the proper URL' do
- stub_config_setting(url: Gitlab::Saas.staging_com_url)
- expect(subject.attributes['src'].value).to eq("https://staging.observe.gitlab.com/-/#{group.id}")
- end
-
- it 'overrides the iframe src url if specified by OVERRIDE_OBSERVABILITY_URL env' do
- stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test')
-
- expect(subject.attributes['src'].value).to eq("http://foo.test/-/#{group.id}")
- end
+ it 'renders the js-observability-app element correctly' do
+ element = Nokogiri::HTML.parse(subject.body).at_css('#js-observability-app')
+ expect(element.attributes['data-observability-iframe-src'].value).to eq(expected_observability_path)
end
+ end
+ end
- describe 'CSP' do
- before do
- setup_existing_csp_for_controller(described_class, csp)
- end
+ describe 'GET #dashboards' do
+ let(:path) { group_observability_dashboards_path(group) }
+ let(:expected_observability_path) { "#{observability_url}/#{group.id}/" }
- subject do
- get group_observability_index_path(group)
- response.headers['Content-Security-Policy']
- end
+ it_behaves_like 'observability route request'
+ end
- context 'when there is no CSP config' do
- let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
+ describe 'GET #manage' do
+ let(:path) { group_observability_manage_path(group) }
+ let(:expected_observability_path) { "#{observability_url}/#{group.id}/dashboards" }
- it 'does not add any csp header' do
- expect(subject).to be_blank
- end
- end
+ it_behaves_like 'observability route request'
+ end
- context 'when frame-src exists in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src 'https://something.test'
- end
- end
+ describe 'GET #explore' do
+ let(:path) { group_observability_explore_path(group) }
+ let(:expected_observability_path) { "#{observability_url}/#{group.id}/explore" }
- it 'appends the proper url to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test https://observe.gitlab.com 'self'")
- end
+ it_behaves_like 'observability route request'
+ end
- it 'appends the proper url to frame-src CSP directives when Gilab.staging?' do
- stub_config_setting(url: Gitlab::Saas.staging_com_url)
+ describe 'CSP' do
+ before do
+ setup_csp_for_controller(described_class, csp)
+ end
- expect(subject).to include(
- "frame-src https://something.test https://staging.observe.gitlab.com 'self'")
- end
+ subject do
+ get group_observability_dashboards_path(group)
+ response.headers['Content-Security-Policy']
+ end
- it 'appends the proper url to frame-src CSP directives when OVERRIDE_OBSERVABILITY_URL is specified' do
- stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test')
+ context 'when there is no CSP config' do
+ let(:csp) { ActionDispatch::ContentSecurityPolicy.new }
- expect(subject).to include(
- "frame-src https://something.test http://foo.test 'self'")
- end
- end
+ it 'does not add any csp header' do
+ expect(subject).to be_blank
+ end
+ end
- context 'when self is already present in the policy' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.frame_src "'self'"
- end
- end
-
- it 'does not append self again' do
- expect(subject).to include(
- "frame-src 'self' https://observe.gitlab.com;")
- end
+ context 'when frame-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src 'https://something.test'
end
+ end
- context 'when default-src exists in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.default_src 'https://something.test'
- end
- end
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} 'self'")
+ end
+ end
- it 'does not change default-src' do
- expect(subject).to include(
- "default-src https://something.test;")
- end
+ context 'when self is already present in the policy' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.frame_src "'self'"
+ end
+ end
- it 'appends the proper url to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test https://observe.gitlab.com 'self'")
- end
+ it 'does not append self again' do
+ expect(subject).to include(
+ "frame-src 'self' #{observability_url};")
+ end
+ end
- it 'appends the proper url to frame-src CSP directives when Gilab.staging?' do
- stub_config_setting(url: Gitlab::Saas.staging_com_url)
+ context 'when default-src exists in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something.test'
+ end
+ end
- expect(subject).to include(
- "frame-src https://something.test https://staging.observe.gitlab.com 'self'")
- end
+ it 'does not change default-src' do
+ expect(subject).to include(
+ "default-src https://something.test;")
+ end
- it 'appends the proper url to frame-src CSP directives when OVERRIDE_OBSERVABILITY_URL is specified' do
- stub_env('OVERRIDE_OBSERVABILITY_URL', 'http://foo.test')
+ it 'appends the proper url to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} 'self'")
+ end
+ end
- expect(subject).to include(
- "frame-src https://something.test http://foo.test 'self'")
- end
+ context 'when frame-src and default-src exist in the CSP config' do
+ let(:csp) do
+ ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src 'https://something_default.test'
+ p.frame_src 'https://something.test'
end
+ end
- context 'when frame-src and default-src exist in the CSP config' do
- let(:csp) do
- ActionDispatch::ContentSecurityPolicy.new do |p|
- p.default_src 'https://something_default.test'
- p.frame_src 'https://something.test'
- end
- end
-
- it 'appends to frame-src CSP directives' do
- expect(subject).to include(
- "frame-src https://something.test https://observe.gitlab.com 'self'")
- expect(subject).to include(
- "default-src https://something_default.test")
- end
- end
+ it 'appends to frame-src CSP directives' do
+ expect(subject).to include(
+ "frame-src https://something.test #{observability_url} 'self'")
+ expect(subject).to include(
+ "default-src https://something_default.test")
end
end
end
diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb
index cf728b3935f..6b150e0acb6 100644
--- a/spec/requests/groups/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe Groups::Settings::AccessTokensController do
let_it_be(:user) { create(:user) }
let_it_be(:resource) { create(:group) }
- let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:access_token_user) { create(:user, :project_bot) }
before_all do
resource.add_owner(user)
- resource.add_maintainer(bot_user)
+ resource.add_maintainer(access_token_user)
end
before do
@@ -27,13 +27,24 @@ RSpec.describe Groups::Settings::AccessTokensController do
end
describe 'GET /:namespace/-/settings/access_tokens' do
- subject do
+ let(:get_access_tokens) do
get group_settings_access_tokens_path(resource)
response
end
+ let(:get_access_tokens_json) do
+ get group_settings_access_tokens_path(resource), params: { format: :json }
+ response
+ end
+
+ subject(:get_access_tokens_with_page) do
+ get group_settings_access_tokens_path(resource), params: { page: 1 }
+ response
+ end
+
it_behaves_like 'feature unavailable'
it_behaves_like 'GET resource access tokens available'
+ it_behaves_like 'GET access tokens are paginated and ordered'
end
describe 'POST /:namespace/-/settings/access_tokens' do
@@ -77,7 +88,7 @@ RSpec.describe Groups::Settings::AccessTokensController do
end
describe 'PUT /:namespace/-/settings/access_tokens/:id', :sidekiq_inline do
- let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
+ let(:resource_access_token) { create(:personal_access_token, user: access_token_user) }
subject do
put revoke_group_settings_access_token_path(resource, resource_access_token)
@@ -89,17 +100,17 @@ RSpec.describe Groups::Settings::AccessTokensController do
end
describe '#index' do
- let_it_be(:resource_access_tokens) { create_list(:personal_access_token, 3, user: bot_user) }
+ let_it_be(:resource_access_tokens) { create_list(:personal_access_token, 3, user: access_token_user) }
before do
get group_settings_access_tokens_path(resource)
end
it 'includes details of the active group access tokens' do
- active_resource_access_tokens =
+ active_access_tokens =
::GroupAccessTokenSerializer.new.represent(resource_access_tokens.reverse, group: resource)
- expect(assigns(:active_resource_access_tokens).to_json).to eq(active_resource_access_tokens.to_json)
+ expect(assigns(:active_access_tokens).to_json).to eq(active_access_tokens.to_json)
end
end
end
diff --git a/spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb b/spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb
new file mode 100644
index 00000000000..d441a8575d0
--- /dev/null
+++ b/spec/requests/jira_connect/cors_preflight_checks_controller_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe JiraConnect::CorsPreflightChecksController do
+ shared_examples 'allows cross-origin requests on self managed' do
+ it 'renders not found' do
+ options path
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.headers['Access-Control-Allow-Origin']).to be_nil
+ end
+
+ context 'with jira_connect_proxy_url setting' do
+ before do
+ stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
+
+ options path, headers: { 'Origin' => 'http://notgitlab.com' }
+ end
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'responds with access-control-allow headers', :aggregate_failures do
+ expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq allowed_methods
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
+ end
+
+ context 'when on GitLab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'renders not found' do
+ options path
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.headers['Access-Control-Allow-Origin']).to be_nil
+ end
+ end
+ end
+ end
+
+ describe 'OPTIONS /-/jira_connect/oauth_application_id' do
+ let(:allowed_methods) { 'GET, OPTIONS' }
+ let(:path) { '/-/jira_connect/oauth_application_id' }
+
+ it_behaves_like 'allows cross-origin requests on self managed'
+ end
+
+ describe 'OPTIONS /-/jira_connect/subscriptions/:id' do
+ let(:allowed_methods) { 'DELETE, OPTIONS' }
+ let(:path) { '/-/jira_connect/subscriptions/123' }
+
+ it_behaves_like 'allows cross-origin requests on self managed'
+ end
+end
diff --git a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
index b0c2eaec4e2..1d772e973ff 100644
--- a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
+++ b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
@@ -3,42 +3,12 @@
require 'spec_helper'
RSpec.describe JiraConnect::OauthApplicationIdsController do
- describe 'OPTIONS /-/jira_connect/oauth_application_id' do
- before do
- stub_application_setting(jira_connect_application_key: '123456')
-
- options '/-/jira_connect/oauth_application_id', headers: { 'Origin' => 'http://notgitlab.com' }
- end
-
- it 'returns 200' do
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'allows cross-origin requests', :aggregate_failures do
- expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
- expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
- expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
- end
-
- context 'on GitLab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- it 'renders not found' do
- options '/-/jira_connect/oauth_application_id'
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.headers['Access-Control-Allow-Origin']).not_to eq '*'
- end
- end
- end
-
describe 'GET /-/jira_connect/oauth_application_id' do
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
before do
stub_application_setting(jira_connect_application_key: '123456')
+ stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
end
it 'renders the jira connect application id' do
@@ -51,7 +21,7 @@ RSpec.describe JiraConnect::OauthApplicationIdsController do
it 'allows cross-origin requests', :aggregate_failures do
get '/-/jira_connect/oauth_application_id', headers: cors_request_headers
- expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
+ expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
expect(response.headers['Access-Control-Allow-Methods']).to eq 'GET, OPTIONS'
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
end
diff --git a/spec/requests/jira_connect/subscriptions_controller_spec.rb b/spec/requests/jira_connect/subscriptions_controller_spec.rb
index f407ea09250..b5f3ab916a4 100644
--- a/spec/requests/jira_connect/subscriptions_controller_spec.rb
+++ b/spec/requests/jira_connect/subscriptions_controller_spec.rb
@@ -5,36 +5,70 @@ require 'spec_helper'
RSpec.describe JiraConnect::SubscriptionsController do
describe 'GET /-/jira_connect/subscriptions' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'http://self-managed-gitlab.com') }
-
let(:qsh) do
Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test')
end
let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
+ let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
+ let(:path) { '/-/jira_connect/subscriptions' }
+ let(:params) { { jwt: jwt } }
+
+ before do
+ stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
+ end
subject(:content_security_policy) do
- get '/-/jira_connect/subscriptions', params: { jwt: jwt }
+ get path, params: params
response.headers['Content-Security-Policy']
end
it { is_expected.to include('http://self-managed-gitlab.com/-/jira_connect/') }
it { is_expected.to include('http://self-managed-gitlab.com/api/') }
+ it { is_expected.to include('http://self-managed-gitlab.com/oauth/') }
context 'with no self-managed instance configured' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: '') }
it { is_expected.not_to include('http://self-managed-gitlab.com/-/jira_connect/') }
it { is_expected.not_to include('http://self-managed-gitlab.com/api/') }
+ it { is_expected.not_to include('http://self-managed-gitlab.com/oauth/') }
end
- context 'with jira_connect_oauth_self_managed feature disabled' do
+ context 'with jira_connect_oauth_self_managed_setting feature disabled' do
before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
+ stub_feature_flags(jira_connect_oauth_self_managed_setting: false)
end
it { is_expected.not_to include('http://self-managed-gitlab.com/-/jira_connect/') }
it { is_expected.not_to include('http://self-managed-gitlab.com/api/') }
+ it { is_expected.not_to include('http://self-managed-gitlab.com/oauth/') }
+ end
+ end
+
+ describe 'DELETE /-/jira_connect/subscriptions/:id' do
+ let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'http://self-managed-gitlab.com') }
+ let_it_be(:subscription) { create(:jira_connect_subscription, installation: installation) }
+
+ let(:qsh) do
+ Atlassian::Jwt.create_query_string_hash('https://gitlab.test/subscriptions', 'GET', 'https://gitlab.test')
+ end
+
+ let(:jwt) { Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret) }
+ let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
+ let(:params) { { jwt: jwt, format: :json } }
+
+ before do
+ stub_application_setting(jira_connect_proxy_url: 'https://gitlab.com')
+ end
+
+ it 'allows cross-origin requests', :aggregate_failures do
+ delete "/-/jira_connect/subscriptions/#{subscription.id}", params: params, headers: cors_request_headers
+
+ expect(response.headers['Access-Control-Allow-Origin']).to eq 'https://gitlab.com'
+ expect(response.headers['Access-Control-Allow-Methods']).to eq 'DELETE, OPTIONS'
+ expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
end
end
end
diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb
index e4cb28cc42b..507489d92cf 100644
--- a/spec/requests/oauth/tokens_controller_spec.rb
+++ b/spec/requests/oauth/tokens_controller_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Oauth::TokensController do
let(:other_headers) { {} }
let(:headers) { cors_request_headers.merge(other_headers) }
let(:allowed_methods) { 'POST, OPTIONS' }
+ let(:authorization_methods) { %w[Authorization X-CSRF-Token X-Requested-With] }
shared_examples 'cross-origin POST request' do
it 'allows cross-origin requests' do
@@ -25,7 +26,7 @@ RSpec.describe Oauth::TokensController 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 allowed_methods
- expect(response.headers['Access-Control-Allow-Headers']).to eq 'Authorization'
+ expect(response.headers['Access-Control-Allow-Headers']).to eq authorization_methods
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
end
end
@@ -39,7 +40,7 @@ RSpec.describe Oauth::TokensController do
end
describe 'OPTIONS /oauth/token' do
- let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } }
+ let(:other_headers) { { 'Access-Control-Request-Headers' => authorization_methods, 'Access-Control-Request-Method' => 'POST' } }
before do
options '/oauth/token', headers: headers
@@ -63,7 +64,7 @@ RSpec.describe Oauth::TokensController do
end
describe 'OPTIONS /oauth/revoke' do
- let(:other_headers) { { 'Access-Control-Request-Headers' => 'Authorization', 'Access-Control-Request-Method' => 'POST' } }
+ let(:other_headers) { { 'Access-Control-Request-Headers' => authorization_methods, 'Access-Control-Request-Method' => 'POST' } }
before do
options '/oauth/revoke', headers: headers
diff --git a/spec/requests/product_analytics/collector_app_attack_spec.rb b/spec/requests/product_analytics/collector_app_attack_spec.rb
deleted file mode 100644
index 6f86e39c295..00000000000
--- a/spec/requests/product_analytics/collector_app_attack_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'ProductAnalytics::CollectorApp throttle' do
- include RackAttackSpecHelpers
-
- include_context 'rack attack cache store'
-
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
-
- before do
- allow(ProductAnalyticsEvent).to receive(:create).and_return(true)
- end
-
- context 'per application id' do
- let(:params) do
- {
- aid: project1.id,
- eid: SecureRandom.uuid
- }
- end
-
- it 'throttles the endpoint' do
- # Allow requests under the rate limit.
- 100.times do
- expect_ok { get '/-/collector/i', params: params }
- end
-
- # Ensure its not related to ip address
- random_next_ip
-
- # Reject request over the limit
- expect_rejection { get '/-/collector/i', params: params }
-
- # But allows request for different aid
- expect_ok { get '/-/collector/i', params: params.merge(aid: project2.id) }
- end
- end
-end
diff --git a/spec/requests/product_analytics/collector_app_spec.rb b/spec/requests/product_analytics/collector_app_spec.rb
deleted file mode 100644
index 0d55d167a6f..00000000000
--- a/spec/requests/product_analytics/collector_app_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'ProductAnalytics::CollectorApp' do
- let_it_be(:project) { create(:project) }
-
- let(:params) { {} }
- let(:raw_event) { Gitlab::Json.parse(fixture_file('product_analytics/event.json')) }
-
- subject { get '/-/collector/i', params: params }
-
- RSpec.shared_examples 'not found' do
- it 'repond with 404' do
- expect { subject }.not_to change { ProductAnalyticsEvent.count }
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'correct event params' do
- let(:params) { raw_event.merge(aid: project.id) }
-
- it 'repond with 200' do
- expect { subject }.to change { ProductAnalyticsEvent.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- context 'feature disabled' do
- before do
- stub_feature_flags(product_analytics: false)
- end
-
- it_behaves_like 'not found'
- end
- end
-
- context 'empty event params' do
- it_behaves_like 'not found'
- end
-
- context 'invalid project id in params' do
- let(:params) do
- {
- aid: '-1',
- p: 'web',
- tna: 'sp',
- tv: 'js-2.14.0',
- eid: SecureRandom.uuid,
- duid: SecureRandom.uuid,
- sid: SecureRandom.uuid
- }
- end
-
- it_behaves_like 'not found'
- end
-end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 65540f86d34..370febf82ff 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'value stream analytics events' do
let(:project) { create(:project, :repository, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
- describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do
+ describe 'GET /:namespace/:project/value_stream_analytics/events/issues', :sidekiq_inline do
let(:first_issue_iid) { project.issues.sort_by_attribute(:created_desc).pick(:iid).to_s }
let(:first_mr_iid) { project.merge_requests.sort_by_attribute(:created_desc).pick(:iid).to_s }
@@ -65,7 +65,7 @@ RSpec.describe 'value stream analytics events' do
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
- it 'lists the staging events', :sidekiq_inline do
+ it 'lists the staging events' do
get project_cycle_analytics_staging_path(project, format: :json)
expect(json_response['events']).not_to be_empty
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
index ad6a3912e0b..c777e8c1f69 100644
--- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -83,7 +83,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
it 'renders template' do
- get "#{project_google_cloud_deployments_path(project)}"
+ get project_google_cloud_deployments_path(project).to_s
expect(response).to render_template(:index)
@@ -98,7 +98,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
describe 'Authorized GET project/-/google_cloud/deployments/cloud_run', :snowplow do
- let_it_be(:url) { "#{project_google_cloud_deployments_cloud_run_path(project)}" }
+ let_it_be(:url) { project_google_cloud_deployments_cloud_run_path(project).to_s }
before do
sign_in(user_maintainer)
@@ -188,7 +188,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
describe 'Authorized GET project/-/google_cloud/deployments/cloud_storage', :snowplow do
- let_it_be(:url) { "#{project_google_cloud_deployments_cloud_storage_path(project)}" }
+ let_it_be(:url) { project_google_cloud_deployments_cloud_storage_path(project).to_s }
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
index 133c6f9153d..d91e5a4f068 100644
--- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
let_it_be(:project) { create(:project, :public) }
describe 'GET index', :snowplow do
- let_it_be(:url) { "#{project_google_cloud_service_accounts_path(project)}" }
+ let_it_be(:url) { project_google_cloud_service_accounts_path(project).to_s }
let_it_be(:user_guest) { create(:user) }
let_it_be(:user_developer) { create(:user) }
diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb
index 81fd1adb1fd..e5f40625cfa 100644
--- a/spec/requests/projects/issue_links_controller_spec.rb
+++ b/spec/requests/projects/issue_links_controller_spec.rb
@@ -28,12 +28,28 @@ RSpec.describe Projects::IssueLinksController do
context 'when linked issue is a task' do
let(:issue_b) { create :issue, :task, project: project }
- it 'returns a work item path for the linked task' do
+ context 'when the use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
+ it 'returns a work item path for the linked task' do
+ get namespace_project_issue_links_path(issue_links_params)
+
+ expect(json_response.count).to eq(1)
+ expect(json_response.first).to include(
+ 'path' => project_work_items_path(issue_b.project, issue_b.id),
+ 'type' => 'TASK'
+ )
+ end
+ end
+
+ it 'returns a work item path for the linked task using the iid in the path' do
get namespace_project_issue_links_path(issue_links_params)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
- 'path' => project_work_items_path(issue_b.project, issue_b.id),
+ 'path' => project_work_items_path(issue_b.project, issue_b.iid, iid_path: true),
'type' => 'TASK'
)
end
diff --git a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
index c859e91e21a..ec65e8cf11e 100644
--- a/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/context_commit_diffs_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
commit: nil,
diff_view: :inline,
merge_ref_head_diff: nil,
- allow_tree_conflicts: true,
+ merge_conflicts_in_diff: true,
pagination_data: {
total_pages: nil
}.merge(pagination_data)
diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb
index 9f0b9a9cb1b..12990b54617 100644
--- a/spec/requests/projects/merge_requests/diffs_spec.rb
+++ b/spec/requests/projects/merge_requests/diffs_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe 'Merge Requests Diffs' do
commit: nil,
diff_view: :inline,
merge_ref_head_diff: nil,
- allow_tree_conflicts: true,
+ merge_conflicts_in_diff: true,
pagination_data: {
total_pages: nil
}.merge(pagination_data)
@@ -80,6 +80,20 @@ RSpec.describe 'Merge Requests Diffs' do
expect(response).to have_gitlab_http_status(:not_modified)
end
+ context 'with check_etags_diffs_batch_before_write_cache flag turned off' do
+ before do
+ stub_feature_flags(check_etags_diffs_batch_before_write_cache: false)
+ end
+
+ it 'does not serialize diffs' do
+ expect(PaginatedDiffSerializer).not_to receive(:new)
+
+ go(headers: headers, page: 0, per_page: 5)
+
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
+
context 'with the different user' do
let(:another_user) { create(:user) }
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
@@ -114,7 +128,7 @@ RSpec.describe 'Merge Requests Diffs' do
context 'with disabled display_merge_conflicts_in_diff feature' do
let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch }
- let(:expected_options) { collection_arguments(total_pages: 20).merge(allow_tree_conflicts: false) }
+ let(:expected_options) { collection_arguments(total_pages: 20).merge(merge_conflicts_in_diff: false) }
before do
stub_feature_flags(display_merge_conflicts_in_diff: false)
diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb
new file mode 100644
index 00000000000..67a2fe47dc8
--- /dev/null
+++ b/spec/requests/projects/ml/experiments_controller_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Ml::ExperimentsController do
+ let_it_be(:project_with_feature) { create(:project, :repository) }
+ let_it_be(:user) { project_with_feature.first_owner }
+ let_it_be(:project_without_feature) do
+ create(:project, :repository).tap { |p| p.add_developer(user) }
+ end
+
+ let_it_be(:experiment) do
+ create(:ml_experiments, project: project_with_feature, user: user).tap do |e|
+ create(:ml_candidates, experiment: e, user: user)
+ end
+ end
+
+ let(:params) { basic_params }
+ let(:ff_value) { true }
+ let(:threshold) { 4 }
+ let(:project) { project_with_feature }
+ let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } }
+
+ before do
+ stub_feature_flags(ml_experiment_tracking: false)
+ stub_feature_flags(ml_experiment_tracking: project_with_feature) if ff_value
+
+ sign_in(user)
+ end
+
+ shared_examples '404 if feature flag disabled' do
+ context 'when :ml_experiment_tracking disabled' do
+ let(:ff_value) { false }
+
+ it 'is 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET index' do
+ before do
+ list_experiments
+ end
+
+ it 'renders the template' do
+ expect(response).to render_template('projects/ml/experiments/index')
+ end
+
+ it 'does not perform N+1 sql queries' do
+ control_count = ActiveRecord::QueryRecorder.new { list_experiments }
+
+ create_list(:ml_experiments, 2, project: project, user: user)
+
+ expect { list_experiments }.not_to exceed_all_query_limit(control_count).with_threshold(threshold)
+ end
+
+ context 'when :ml_experiment_tracking is disabled for the project' do
+ let(:project) { project_without_feature }
+
+ it 'responds with a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ it_behaves_like '404 if feature flag disabled'
+ end
+
+ describe 'GET show' do
+ let(:params) { basic_params.merge(id: experiment.iid) }
+
+ before do
+ show_experiment
+ end
+
+ it 'renders the template' do
+ expect(response).to render_template('projects/ml/experiments/show')
+ end
+
+ it 'does not perform N+1 sql queries' do
+ control_count = ActiveRecord::QueryRecorder.new { show_experiment }
+
+ create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment)
+
+ expect { show_experiment }.not_to exceed_all_query_limit(control_count).with_threshold(threshold)
+ end
+
+ it_behaves_like '404 if feature flag disabled'
+ end
+
+ private
+
+ def show_experiment
+ get project_ml_experiment_path(project, experiment.iid), params: params
+ end
+
+ def list_experiments
+ get project_ml_experiments_path(project), params: params
+ end
+end
diff --git a/spec/requests/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb
index 48114834c65..17389cdcce7 100644
--- a/spec/requests/projects/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -6,11 +6,11 @@ RSpec.describe Projects::Settings::AccessTokensController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:resource) { create(:project, group: group) }
- let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:access_token_user) { create(:user, :project_bot) }
before_all do
resource.add_maintainer(user)
- resource.add_maintainer(bot_user)
+ resource.add_maintainer(access_token_user)
end
before do
@@ -28,13 +28,24 @@ RSpec.describe Projects::Settings::AccessTokensController do
end
describe 'GET /:namespace/:project/-/settings/access_tokens' do
- subject do
+ let(:get_access_tokens) do
get project_settings_access_tokens_path(resource)
response
end
+ let(:get_access_tokens_json) do
+ get project_settings_access_tokens_path(resource), params: { format: :json }
+ response
+ end
+
+ subject(:get_access_tokens_with_page) do
+ get project_settings_access_tokens_path(resource), params: { page: 1 }
+ response
+ end
+
it_behaves_like 'feature unavailable'
it_behaves_like 'GET resource access tokens available'
+ it_behaves_like 'GET access tokens are paginated and ordered'
end
describe 'POST /:namespace/:project/-/settings/access_tokens' do
@@ -78,7 +89,7 @@ RSpec.describe Projects::Settings::AccessTokensController do
end
describe 'PUT /:namespace/:project/-/settings/access_tokens/:id', :sidekiq_inline do
- let(:resource_access_token) { create(:personal_access_token, user: bot_user) }
+ let(:resource_access_token) { create(:personal_access_token, user: access_token_user) }
subject do
put revoke_project_settings_access_token_path(resource, resource_access_token)
@@ -90,17 +101,17 @@ RSpec.describe Projects::Settings::AccessTokensController do
end
describe '#index' do
- let_it_be(:resource_access_tokens) { create_list(:personal_access_token, 3, user: bot_user) }
+ let_it_be(:resource_access_tokens) { create_list(:personal_access_token, 3, user: access_token_user) }
before do
get project_settings_access_tokens_path(resource)
end
it 'includes details of the active project access tokens' do
- active_resource_access_tokens =
+ active_access_tokens =
::ProjectAccessTokenSerializer.new.represent(resource_access_tokens.reverse, project: resource)
- expect(assigns(:active_resource_access_tokens).to_json).to eq(active_resource_access_tokens.to_json)
+ expect(assigns(:active_access_tokens).to_json).to eq(active_access_tokens.to_json)
end
end
end
diff --git a/spec/requests/projects/work_items_spec.rb b/spec/requests/projects/work_items_spec.rb
index e6365a3824a..4d7acc73d4f 100644
--- a/spec/requests/projects/work_items_spec.rb
+++ b/spec/requests/projects/work_items_spec.rb
@@ -15,24 +15,10 @@ RSpec.describe 'Work Items' do
sign_in(developer)
end
- context 'when the work_items feature flag is enabled' do
- it 'renders index' do
- get project_work_items_url(work_item.project, work_items_path: work_item.id)
+ it 'renders index' do
+ get project_work_items_url(work_item.project, work_items_path: work_item.id)
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when the work_items feature flag is disabled' do
- before do
- stub_feature_flags(work_items: false)
- end
-
- it 'returns 404' do
- get project_work_items_url(work_item.project, work_items_path: work_item.id)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb
index 93a2fecab74..613732c19ea 100644
--- a/spec/requests/search_controller_spec.rb
+++ b/spec/requests/search_controller_spec.rb
@@ -103,13 +103,17 @@ RSpec.describe SearchController, type: :request do
expect(response).not_to redirect_to(project_commit_path(project, sha))
end
- it 'does not redirect if user cannot download_code from project' do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :download_code, project).and_return(false)
+ context 'when user cannot read_code' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_code, project).and_return(false)
+ end
- send_search_request({ search: sha, project_id: project.id })
+ it 'does not redirect' do
+ send_search_request({ search: sha, project_id: project.id })
- expect(response).not_to redirect_to(project_commit_path(project, sha))
+ expect(response).not_to redirect_to(project_commit_path(project, sha))
+ end
end
it 'does not redirect if commit sha not found in project' do
diff --git a/spec/requests/self_monitoring_project_spec.rb b/spec/requests/self_monitoring_project_spec.rb
index f7227f71b05..64c5f94657d 100644
--- a/spec/requests/self_monitoring_project_spec.rb
+++ b/spec/requests/self_monitoring_project_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'when the self monitoring project is created' do
+ context 'when the self-monitoring project is created' do
let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
@@ -41,7 +41,7 @@ RSpec.describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'when the self monitoring project is being created' do
+ context 'when the self-monitoring project is being created' do
it_behaves_like 'handles invalid job_id'
context 'when job is in progress' do
@@ -121,7 +121,7 @@ RSpec.describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'when the self monitoring project is deleted' do
+ context 'when the self-monitoring project is deleted' do
let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path }
it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
@@ -145,7 +145,7 @@ RSpec.describe 'Self-Monitoring project requests' do
login_as(admin)
end
- context 'when the self monitoring project is being deleted' do
+ context 'when the self-monitoring project is being deleted' do
it_behaves_like 'handles invalid job_id'
context 'when job is in progress' do
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index e8d3e94bd0e..34fda1cce4d 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -30,6 +30,97 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
end
end
+ shared_examples_for 'rate limited' do
+ it 'redirects to the login form and shows an alert message' do
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:alert])
+ .to eq(s_('IdentityVerification|Maximum login attempts exceeded. Wait 10 minutes and try again.'))
+ end
+ end
+
+ shared_examples_for 'two factor prompt or successful login' do
+ it 'shows the 2FA prompt when enabled or redirects to the root path' do
+ if user.two_factor_enabled?
+ expect(response.body).to include('Two-factor authentication code')
+ else
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
+
+ shared_examples_for 'verifying with email' do
+ context 'when rate limited' do
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+ sign_in
+ end
+
+ it_behaves_like 'rate limited'
+ end
+
+ context 'when the user already has an unlock_token set' do
+ before do
+ user.update!(unlock_token: 'token')
+ sign_in
+ end
+
+ it_behaves_like 'prompt for email verification'
+ end
+
+ context 'when the user is already locked' do
+ before do
+ user.update!(locked_at: Time.current)
+ perform_enqueued_jobs { sign_in }
+ end
+
+ it_behaves_like 'send verification instructions'
+ it_behaves_like 'prompt for email verification'
+ end
+
+ context 'when the user is signing in from an unknown ip address' do
+ before do
+ allow(AuthenticationEvent)
+ .to receive(:initial_login_or_known_ip_address?)
+ .and_return(false)
+ perform_enqueued_jobs { sign_in }
+ end
+
+ it_behaves_like 'send verification instructions'
+ it_behaves_like 'prompt for email verification'
+ end
+ end
+
+ shared_examples_for 'not verifying with email' do
+ context 'when rate limited' do
+ before do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+ sign_in
+ end
+
+ it_behaves_like 'two factor prompt or successful login'
+ end
+
+ context 'when the user already has an unlock_token set' do
+ before do
+ user.update!(unlock_token: 'token')
+ sign_in
+ end
+
+ it_behaves_like 'two factor prompt or successful login'
+ end
+
+ context 'when the user is signing in from an unknown ip address' do
+ before do
+ allow(AuthenticationEvent)
+ .to receive(:initial_login_or_known_ip_address?)
+ .and_return(false)
+ sign_in
+ end
+
+ it_behaves_like 'two factor prompt or successful login'
+ end
+ end
+
describe 'verify_with_email' do
context 'when user is locked and a verification_user_id session variable exists' do
before do
@@ -99,69 +190,34 @@ RSpec.describe 'VerifiesWithEmail', :clean_gitlab_redis_sessions, :clean_gitlab_
end
context 'when signing in with a valid password' do
- let(:sign_in) { post(user_session_path(user: { login: user.username, password: user.password })) }
+ let(:headers) { {} }
+ let(:sign_in) do
+ post user_session_path, params: { user: { login: user.username, password: user.password } }, headers: headers
+ end
+
+ it_behaves_like 'not verifying with email'
context 'when the feature flag is toggled on' do
before do
stub_feature_flags(require_email_verification: user)
end
- context 'when rate limited' do
- before do
- allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
- sign_in
- end
+ it_behaves_like 'verifying with email'
- it 'redirects to the login form and shows an alert message' do
- expect(response).to redirect_to(new_user_session_path)
- expect(flash[:alert])
- .to eq(s_('IdentityVerification|Maximum login attempts exceeded. Wait 10 minutes and try again.'))
- end
- end
-
- context 'when the user already has an unlock_token set' do
+ context 'when 2FA is enabled' do
before do
- user.update!(unlock_token: 'token')
- sign_in
+ user.update!(otp_required_for_login: true)
end
- it_behaves_like 'prompt for email verification'
+ it_behaves_like 'not verifying with email'
end
- context 'when the user is already locked' do
+ context 'when request is not from a QA user' do
before do
- user.update!(locked_at: Time.current)
- perform_enqueued_jobs { sign_in }
+ allow(Gitlab::Qa).to receive(:request?).and_return(false)
end
- it_behaves_like 'send verification instructions'
- it_behaves_like 'prompt for email verification'
- end
-
- context 'when the user is signing in from an unknown ip address' do
- before do
- allow(AuthenticationEvent)
- .to receive(:initial_login_or_known_ip_address?)
- .and_return(false)
-
- perform_enqueued_jobs { sign_in }
- end
-
- it_behaves_like 'send verification instructions'
- it_behaves_like 'prompt for email verification'
- end
- end
-
- context 'when the feature flag is toggled off' do
- let(:another_user) { build(:user) }
-
- before do
- stub_feature_flags(require_email_verification: another_user)
- sign_in
- end
-
- it 'redirects to the root path' do
- expect(response).to redirect_to(root_path)
+ it_behaves_like 'verifying with email'
end
end
end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index ae69b222280..68e619e5246 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -72,8 +72,16 @@ RSpec.shared_examples 'groups routing' do
expect(get("groups/#{group_path}/-/harbor/repositories/test/artifacts/test/tags")).to route_to('groups/harbor/tags#index', group_id: group_path, repository_id: 'test', artifact_id: 'test')
end
- it 'routes to the observability controller' do
- expect(get("groups/#{group_path}/-/observability")).to route_to('groups/observability#index', group_id: group_path)
+ it 'routes to the observability controller dashboards method' do
+ expect(get("groups/#{group_path}/-/observability/dashboards")).to route_to('groups/observability#dashboards', group_id: group_path)
+ end
+
+ it 'routes to the observability controller explore method' do
+ expect(get("groups/#{group_path}/-/observability/explore")).to route_to('groups/observability#explore', group_id: group_path)
+ end
+
+ it 'routes to the observability controller manage method' do
+ expect(get("groups/#{group_path}/-/observability/manage")).to route_to('groups/observability#manage', group_id: group_path)
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 875a54de3d1..42196a7d8af 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -548,7 +548,7 @@ RSpec.describe 'project routing' do
method: :get },
{ controller: 'projects/find_file', action: 'show',
namespace_id: 'gitlab', project_id: 'gitlabhq',
- id: "#{newline_file}" })
+ id: newline_file.to_s })
end
it 'to #list' do
@@ -559,7 +559,7 @@ RSpec.describe 'project routing' do
method: :get },
{ controller: 'projects/find_file', action: 'list',
namespace_id: 'gitlab', project_id: 'gitlabhq',
- id: "#{newline_file}" })
+ id: newline_file.to_s })
end
it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/find_file", "/gitlab/gitlabhq/-/find_file"
diff --git a/spec/rubocop/cop/api/ensure_string_detail_spec.rb b/spec/rubocop/cop/api/ensure_string_detail_spec.rb
new file mode 100644
index 00000000000..d4f68711e78
--- /dev/null
+++ b/spec/rubocop/cop/api/ensure_string_detail_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/api/ensure_string_detail'
+
+RSpec.describe RuboCop::Cop::API::EnsureStringDetail do
+ context "when in_api? == true" do
+ before do
+ allow(cop).to receive(:in_api?).and_return(true)
+ end
+
+ context "when detail field uses a string" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail "foo bar"
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses interpolation in a string" do
+ it "does not add an offense" do
+ baz = "bat"
+
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail "foo bar #{baz}"
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses a multiline string" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail "foo bar"\
+ "baz bat"
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses a constant" do
+ it "does not add an offense" do
+ pending
+
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ DESCRIPTION = 'A string'
+
+ desc 'Some API thing related to a project' do
+ detail DESCRIPTION
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses a HEREDOC string" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail <<~END
+ foo bar
+ baz bat
+ END
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses an array" do
+ it "adds an offense" do
+ expect_offense(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ something 'else'
+ detail ["foo", "bar"]
+ ^^^^^^^^^^^^^^^^^^^^^ Only String objects are permitted in API detail field.
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field is outside of desc block" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class Foo
+ detail ["foo", "bar"]
+ end
+ CODE
+ end
+ end
+ end
+
+ context "when in_api? == false" do
+ before do
+ allow(cop).to receive(:in_api?).and_return(false)
+ end
+
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail ["foo", "bar"]
+ end
+ end
+ end
+ CODE
+ end
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb
index e4ec107747d..70f63a78dd1 100644
--- a/spec/rubocop/cop/gitlab/json_spec.rb
+++ b/spec/rubocop/cop/gitlab/json_spec.rb
@@ -5,12 +5,41 @@ require_relative '../../../../rubocop/cop/gitlab/json'
RSpec.describe RuboCop::Cop::Gitlab::Json do
context 'when ::JSON is called' do
- it 'registers an offense' do
+ it 'registers an offense and autocorrects' do
expect_offense(<<~RUBY)
class Foo
def bar
JSON.parse('{ "foo": "bar" }')
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` directly. [...]
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def bar
+ Gitlab::Json.parse('{ "foo": "bar" }')
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when ::JSON is called in EE' do
+ it 'registers an offense and autocorrects' do
+ expect_offense(<<~RUBY, '/path/to/ee/foo.rb')
+ class Foo
+ def bar
+ JSON.parse('{ "foo": "bar" }')
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` directly. [...]
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def bar
+ ::Gitlab::Json.parse('{ "foo": "bar" }')
end
end
RUBY
@@ -18,12 +47,20 @@ RSpec.describe RuboCop::Cop::Gitlab::Json do
end
context 'when ActiveSupport::JSON is called' do
- it 'registers an offense' do
+ it 'registers an offense and autocorrects' do
expect_offense(<<~RUBY)
class Foo
def bar
ActiveSupport::JSON.parse('{ "foo": "bar" }')
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` directly. [...]
+ end
+ end
+ RUBY
+
+ expect_correction(<<~RUBY)
+ class Foo
+ def bar
+ Gitlab::Json.parse('{ "foo": "bar" }')
end
end
RUBY
diff --git a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
index a3c9ae8916e..6e60889f737 100644
--- a/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
+++ b/spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb
@@ -194,6 +194,10 @@ RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
include_examples 'sets flag as used', 'FEATURE_FLAG = :foo', 'foo'
end
+ describe 'ROUTING_FEATURE_FLAG = :foo' do
+ include_examples 'sets flag as used', 'ROUTING_FEATURE_FLAG = :foo', 'foo'
+ end
+
describe 'Worker `data_consistency` method' do
include_examples 'sets flag as used', 'data_consistency :delayed, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'data_consistency :delayed'
diff --git a/spec/rubocop/cop/gitlab/rspec/avoid_setup_spec.rb b/spec/rubocop/cop/gitlab/rspec/avoid_setup_spec.rb
new file mode 100644
index 00000000000..f9226649f65
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/rspec/avoid_setup_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../../rubocop/cop/gitlab/rspec/avoid_setup'
+
+RSpec.describe RuboCop::Cop::Gitlab::RSpec::AvoidSetup do
+ context 'when calling let_it_be' do
+ let(:source) do
+ <<~SRC
+ let_it_be(:user) { create(:user) }
+ ^^^^^^^^^^^^^^^^ Avoid the use of `let_it_be` [...]
+ SRC
+ end
+
+ it 'registers an offense' do
+ expect_offense(source)
+ end
+ end
+
+ context 'without readability issues' do
+ let(:source) do
+ <<~SRC
+ it 'registers the user and sends them to a project listing page' do
+ user_signs_up
+
+ expect_to_see_account_confirmation_page
+ end
+ SRC
+ end
+
+ it 'does not register an offense' do
+ expect_no_offenses(source)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/graphql/enum_names_spec.rb b/spec/rubocop/cop/graphql/enum_names_spec.rb
new file mode 100644
index 00000000000..f45df068381
--- /dev/null
+++ b/spec/rubocop/cop/graphql/enum_names_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/graphql/enum_names'
+
+RSpec.describe RuboCop::Cop::Graphql::EnumNames do
+ describe 'class name' do
+ it 'adds an offense when class name does not end with `Enum`' do
+ expect_offense(<<~ENUM)
+ module Types
+ class Fake < BaseEnum
+ ^^^^ #{described_class::CLASS_NAME_SUFFIX_MSG}
+ graphql_name 'Fake'
+ end
+ end
+ ENUM
+ end
+ end
+
+ describe 'graphql_name' do
+ it 'adds an offense when `graphql_name` is not set' do
+ expect_offense(<<~ENUM)
+ module Types
+ class FakeEnum < BaseEnum
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::GRAPHQL_NAME_MISSING_MSG}
+ end
+ end
+ ENUM
+ end
+
+ it 'adds no offense when `declarative_enum` is used' do
+ expect_no_offenses(<<~ENUM)
+ module Types
+ class FakeEnum < BaseEnum
+ declarative_enum ::FakeModule::FakeDeclarativeEnum
+ end
+ end
+ ENUM
+ end
+
+ it 'adds an offense when `graphql_name` includes `enum`' do
+ expect_offense(<<~ENUM)
+ module Types
+ class FakeEnum < BaseEnum
+ graphql_name 'FakeEnum'
+ ^^^^^^^^^^ #{described_class::GRAPHQL_NAME_WITH_ENUM_MSG}
+ end
+ end
+ ENUM
+ end
+ end
+end
diff --git a/spec/rubocop/cop/graphql/enum_values_spec.rb b/spec/rubocop/cop/graphql/enum_values_spec.rb
new file mode 100644
index 00000000000..5609c86d9ec
--- /dev/null
+++ b/spec/rubocop/cop/graphql/enum_values_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/graphql/enum_values'
+
+RSpec.describe RuboCop::Cop::Graphql::EnumValues do
+ it 'adds an offense when enum value is not uppercase' do
+ expect_offense(<<~ENUM)
+ module Types
+ class FakeEnum < BaseEnum
+ graphql_name 'Fake'
+
+ value 'downcase', description: "Downcase."
+ ^^^^^^^^^^ #{described_class::MSG}
+ end
+ end
+ ENUM
+ end
+
+ context 'when values are set dynamically' do
+ it 'adds an offense when enum value is set without `:upcase`' do
+ expect_offense(<<~ENUM)
+ VALUES = ['FOO', 'bar']
+
+ module Types
+ class FakeEnum < BaseEnum
+ graphql_name 'Fake'
+
+ VALUES.each do |val|
+ value val, description: "Dynamic value."
+ ^^^ #{described_class::MSG}
+ end
+ end
+ end
+ ENUM
+ end
+
+ it 'adds no offense when enum value is deprecated' do
+ expect_no_offenses(<<~ENUM)
+ module Types
+ class FakeEnum < BaseEnum
+ graphql_name 'Fake'
+
+ value 'foo', deprecated: { reason: 'Use something else' }
+ end
+ end
+ ENUM
+ end
+
+ it 'adds no offense when enum value is uppercased literally' do
+ expect_no_offenses(<<~'ENUM')
+ module Types
+ class FakeEnum < BaseEnum
+ graphql_name 'Fake'
+
+ value 'FOO'
+ end
+ end
+ ENUM
+ end
+
+ it 'adds no offense when enum value is calling upcased' do
+ expect_no_offenses(<<~'ENUM')
+ VALUES = ['FOO', 'bar']
+
+ module Types
+ class FakeEnum < BaseEnum
+ graphql_name 'Fake'
+
+ VALUES.each do |val|
+ value val.underscore.upcase, description: "Dynamic value."
+ value "#{field.upcase.tr(' ', '_')}_ASC"
+ end
+ end
+ end
+ ENUM
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/schema_addition_methods_no_post_spec.rb b/spec/rubocop/cop/migration/schema_addition_methods_no_post_spec.rb
new file mode 100644
index 00000000000..fb087269e2d
--- /dev/null
+++ b/spec/rubocop/cop/migration/schema_addition_methods_no_post_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/migration/schema_addition_methods_no_post'
+
+RSpec.describe RuboCop::Cop::Migration::SchemaAdditionMethodsNoPost do
+ before do
+ allow(cop).to receive(:time_enforced?).and_return true
+ end
+
+ it "does not allow 'add_column' to be called" do
+ expect_offense(<<~CODE)
+ add_column
+ ^^^^^^^^^^ #{described_class::MSG}
+ CODE
+ end
+
+ it "does not allow 'create_table' to be called" do
+ expect_offense(<<~CODE)
+ create_table
+ ^^^^^^^^^^^^ #{described_class::MSG}
+ CODE
+ end
+end
diff --git a/spec/rubocop/cop/rake/require_spec.rb b/spec/rubocop/cop/rake/require_spec.rb
new file mode 100644
index 00000000000..bb8c6a1f063
--- /dev/null
+++ b/spec/rubocop/cop/rake/require_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../rubocop/cop/rake/require'
+
+RSpec.describe RuboCop::Cop::Rake::Require do
+ let(:msg) { described_class::MSG }
+
+ it 'registers an offenses for require methods' do
+ expect_offense(<<~RUBY)
+ require 'json'
+ ^^^^^^^^^^^^^^ #{msg}
+ require_relative 'gitlab/json'
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg}
+ RUBY
+ end
+
+ it 'does not register offense inside `task` definition' do
+ expect_no_offenses(<<~RUBY)
+ task :parse do
+ require 'json'
+ end
+
+ namespace :some do
+ task parse: :env do
+ require_relative 'gitlab/json'
+ end
+ end
+ RUBY
+ end
+
+ it 'does not register offense inside a block definition' do
+ expect_no_offenses(<<~RUBY)
+ RSpec::Core::RakeTask.new(:parse_json) do |t, args|
+ require 'json'
+ end
+ RUBY
+ end
+
+ it 'does not register offense inside a method definition' do
+ expect_no_offenses(<<~RUBY)
+ def load_deps
+ require 'json'
+ end
+
+ task :parse do
+ load_deps
+ end
+ RUBY
+ end
+
+ it 'does not register offense when require task related files' do
+ expect_no_offenses(<<~RUBY)
+ require 'rubocop/rake_tasks'
+ require 'gettext_i18n_rails/tasks'
+ require_relative '../../rubocop/check_graceful_task'
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb b/spec/rubocop/cop/rspec/duplicate_spec_location_spec.rb
index 0a121a495c9..f209ae81661 100644
--- a/spec/rubocop/cop/gitlab/duplicate_spec_location_spec.rb
+++ b/spec/rubocop/cop/rspec/duplicate_spec_location_spec.rb
@@ -2,16 +2,16 @@
require 'rubocop_spec_helper'
-require_relative '../../../../rubocop/cop/gitlab/duplicate_spec_location'
+require_relative '../../../../rubocop/cop/rspec/duplicate_spec_location'
-RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation, type: :rubocop_rspec do
+RSpec.describe RuboCop::Cop::RSpec::DuplicateSpecLocation do
let(:rails_root) { '../../../../' }
def full_path(path)
File.expand_path(File.join(rails_root, path), __dir__)
end
- context 'Non-EE spec file' do
+ context 'for a non-EE spec file' do
it 'registers no offenses' do
expect_no_offenses(<<~SOURCE, full_path('spec/foo_spec.rb'))
describe 'Foo' do
@@ -20,7 +20,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation, type: :rubocop_rspec
end
end
- context 'Non-EE application file' do
+ context 'for a non-EE application file' do
it 'registers no offenses' do
expect_no_offenses(<<~SOURCE, full_path('app/models/blog_post.rb'))
class BlogPost
@@ -29,7 +29,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation, type: :rubocop_rspec
end
end
- context 'EE application file' do
+ context 'for an EE application file' do
it 'registers no offenses' do
expect_no_offenses(<<~SOURCE, full_path('ee/app/models/blog_post.rb'))
class BlogPost
@@ -38,7 +38,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation, type: :rubocop_rspec
end
end
- context 'EE spec file for EE only code' do
+ context 'for an EE spec file for EE only code' do
let(:spec_file_path) { full_path('ee/spec/controllers/foo_spec.rb') }
it 'registers no offenses' do
@@ -48,7 +48,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation, type: :rubocop_rspec
SOURCE
end
- context 'there is a duplicate file' do
+ context 'when there is a duplicate file' do
before do
allow(File).to receive(:exist?).and_call_original
@@ -67,7 +67,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation, type: :rubocop_rspec
end
end
- context 'EE spec file for EE extension' do
+ context 'for an EE spec file for EE extension' do
let(:spec_file_path) { full_path('ee/spec/controllers/ee/foo_spec.rb') }
it 'registers no offenses' do
@@ -77,7 +77,7 @@ RSpec.describe RuboCop::Cop::Gitlab::DuplicateSpecLocation, type: :rubocop_rspec
SOURCE
end
- context 'there is a duplicate file' do
+ context 'when there is a duplicate file' do
before do
allow(File).to receive(:exist?).and_call_original
diff --git a/spec/rubocop/cop/rspec/factory_bot/strategy_in_callback_spec.rb b/spec/rubocop/cop/rspec/factory_bot/strategy_in_callback_spec.rb
new file mode 100644
index 00000000000..9dcfebc7c1d
--- /dev/null
+++ b/spec/rubocop/cop/rspec/factory_bot/strategy_in_callback_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../../rubocop/cop/rspec/factory_bot/strategy_in_callback'
+
+RSpec.describe RuboCop::Cop::RSpec::FactoryBot::StrategyInCallback do
+ shared_examples 'an offensive factory call' do |namespace|
+ described_class::FORBIDDEN_METHODS.each do |forbidden_method|
+ namespaced_forbidden_method = "#{namespace}#{forbidden_method}(:ci_job_artifact, :archive)"
+
+ it "registers an offence for multiple #{namespaced_forbidden_method} calls" do
+ expect_offense(<<-RUBY)
+ FactoryBot.define do
+ factory :ci_build, class: 'Ci::Build', parent: :ci_processable do
+ trait :artifacts do
+ before(:create) do
+ #{namespaced_forbidden_method}
+ #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories
+ end
+
+ after(:create) do |build|
+ #{namespaced_forbidden_method}
+ #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories
+ #{namespaced_forbidden_method}
+ #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories
+ end
+ end
+ end
+ end
+ RUBY
+ end
+
+ it "registers an offense for #{namespaced_forbidden_method} when is a send node" do
+ expect_offense(<<-RUBY)
+ FactoryBot.define do
+ factory :ci_build, class: 'Ci::Build', parent: :ci_processable do
+ trait :artifacts do
+ after(:create) do |build|
+ #{namespaced_forbidden_method}
+ #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories
+ end
+ end
+ end
+ end
+ RUBY
+ end
+
+ it "registers an offense for #{namespaced_forbidden_method} when is assigned" do
+ expect_offense(<<-RUBY)
+ FactoryBot.define do
+ factory :ci_build, class: 'Ci::Build', parent: :ci_processable do
+ trait :artifacts do
+ after(:create) do |build|
+ ci_build = #{namespaced_forbidden_method}
+ #{'^' * namespaced_forbidden_method.size} Prefer inline `association` over `#{forbidden_method}`. See https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#factories
+
+ ci_build
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ it_behaves_like 'an offensive factory call', ''
+ it_behaves_like 'an offensive factory call', 'FactoryBot.'
+ it_behaves_like 'an offensive factory call', '::FactoryBot.'
+end
diff --git a/spec/rubocop/migration_helpers_spec.rb b/spec/rubocop/migration_helpers_spec.rb
index 997d4071c29..6e6c3a7a0b9 100644
--- a/spec/rubocop/migration_helpers_spec.rb
+++ b/spec/rubocop/migration_helpers_spec.rb
@@ -52,4 +52,21 @@ RSpec.describe RuboCop::MigrationHelpers do
it { expect(fake_cop.version(node)).to eq(20200210184420) }
end
+
+ describe '#time_enforced?' do
+ before do
+ allow(fake_cop).to receive(:name).and_return("TestCop")
+ allow(fake_cop).to receive(:config).and_return(double(for_cop: { 'EnforcedSince' => 20221018000000 }))
+ end
+
+ where(:name, :expected) do
+ '/gitlab/db/post_migrate/20200210184420_create_operations_scopes_table.rb' | false
+ '/gitlab/db/post_migrate/20220210184420_create_fake_table.rb' | false
+ '/gitlab/db/post_migrate/20221019184420_add_id_to_reports_table.rb' | true
+ end
+
+ with_them do
+ it { expect(fake_cop.time_enforced?(node)).to eq(expected) }
+ end
+ end
end
diff --git a/spec/rubocop_spec_helper.rb b/spec/rubocop_spec_helper.rb
index cf747132ec1..d57461960f2 100644
--- a/spec/rubocop_spec_helper.rb
+++ b/spec/rubocop_spec_helper.rb
@@ -12,10 +12,7 @@ require_relative './support/shared_contexts/rubocop_default_rspec_language_confi
RSpec.configure do |config|
config.define_derived_metadata(file_path: %r{spec/rubocop}) do |metadata|
- # TODO: move DuplicateSpecLocation cop to RSpec::DuplicateSpecLocation
- unless metadata[:type] == :rubocop_rspec
- metadata[:type] = :rubocop
- end
+ metadata[:type] = :rubocop
end
config.define_derived_metadata(file_path: %r{spec/rubocop/cop/rspec}) do |metadata|
diff --git a/spec/scripts/failed_tests_spec.rb b/spec/scripts/failed_tests_spec.rb
index 92eae75b3be..b99fd991c55 100644
--- a/spec/scripts/failed_tests_spec.rb
+++ b/spec/scripts/failed_tests_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
require_relative '../../scripts/failed_tests'
RSpec.describe FailedTests do
diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
index c97226c1a2d..58e016b6d68 100644
--- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
+++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb
@@ -32,8 +32,8 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
subject { described_class.new }
# GLFM input files
- let(:glfm_spec_txt_path) { described_class::GLFM_SPEC_TXT_PATH }
- let(:glfm_spec_txt_local_io) { StringIO.new(glfm_spec_txt_contents) }
+ let(:es_snapshot_spec_md_path) { described_class::ES_SNAPSHOT_SPEC_MD_PATH }
+ let(:es_snapshot_spec_md_local_io) { StringIO.new(es_snapshot_spec_md_contents) }
let(:glfm_example_status_yml_path) { described_class::GLFM_EXAMPLE_STATUS_YML_PATH }
let(:glfm_example_metadata_yml_path) { described_class::GLFM_EXAMPLE_METADATA_YML_PATH }
let(:glfm_example_normalizations_yml_path) { described_class::GLFM_EXAMPLE_NORMALIZATIONS_YML_PATH }
@@ -53,16 +53,11 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
# Internal tempfiles
let(:static_html_tempfile_path) { Tempfile.new.path }
- let(:glfm_spec_txt_contents) do
+ let(:es_snapshot_spec_md_contents) do
<<~MARKDOWN
---
title: GitLab Flavored Markdown Spec
...
-
- # Introduction
-
- GLFM intro text...
-
# Inlines
## Strong
@@ -96,7 +91,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
### Another H3
This is a second consecutive third-level heading. It exists to drive full code coverage
- for this scenario, although it doesn't (yet) exist in the actual spec.txt.
+ for this scenario, although it doesn't (yet) exist in the actual snapshot_spec.md.
## An H2 with all disabled examples
@@ -243,12 +238,6 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
.
<p><a href="project-wikis-test-file">project-wikis-test-file</a></p>
````````````````````````````````
-
- <!-- END TESTS -->
-
- # Appendix
-
- Appendix text.
MARKDOWN
end
@@ -305,11 +294,11 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
---
01_00_00__obsolete_entry_to_be_deleted__001:
canonical: |
- This entry is no longer exists in the spec.txt, so it will be deleted.
+ This entry is no longer exists in the snapshot_spec.md, so it will be deleted.
static: |-
- This entry is no longer exists in the spec.txt, so it will be deleted.
+ This entry is no longer exists in the snapshot_spec.md, so it will be deleted.
wysiwyg: |-
- This entry is no longer exists in the spec.txt, so it will be deleted.
+ This entry is no longer exists in the snapshot_spec.md, so it will be deleted.
02_01_00__inlines__strong__001:
canonical: |
This entry is existing, but not skipped, so it will be overwritten.
@@ -332,7 +321,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
---
01_00_00__obsolete_entry_to_be_deleted__001: |-
{
- "obsolete": "This entry is no longer exists in the spec.txt, and is not skipped, so it will be deleted."
+ "obsolete": "This entry is no longer exists in the snapshot_spec.md, and is not skipped, so it will be deleted."
}
02_01_00__inlines__strong__001: |-
{
@@ -370,7 +359,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
# actual network and filesystem I/O during the spec run.
# input files
- allow(File).to receive(:open).with(glfm_spec_txt_path) { glfm_spec_txt_local_io }
+ allow(File).to receive(:open).with(es_snapshot_spec_md_path) { es_snapshot_spec_md_local_io }
allow(File).to receive(:open).with(glfm_example_status_yml_path) do
StringIO.new(glfm_example_status_yml_contents)
end
@@ -454,43 +443,43 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
<<~YAML
---
02_01_00__inlines__strong__001:
- spec_txt_example_position: 1
+ spec_example_position: 1
source_specification: commonmark
02_01_00__inlines__strong__002:
- spec_txt_example_position: 2
+ spec_example_position: 2
source_specification: github
02_03_00__inlines__strikethrough_extension__001:
- spec_txt_example_position: 4
+ spec_example_position: 4
source_specification: github
03_01_00__first_gitlab_specific_section_with_examples__strong_but_with_two_asterisks__001:
- spec_txt_example_position: 5
+ spec_example_position: 5
source_specification: gitlab
03_02_01__first_gitlab_specific_section_with_examples__h2_which_contains_an_h3__example_in_an_h3__001:
- spec_txt_example_position: 6
+ spec_example_position: 6
source_specification: gitlab
04_01_00__second_gitlab_specific_section_with_examples__strong_but_with_html__001:
- spec_txt_example_position: 7
+ spec_example_position: 7
source_specification: gitlab
05_01_00__third_gitlab_specific_section_with_skipped_examples__strong_but_skipped__001:
- spec_txt_example_position: 8
+ spec_example_position: 8
source_specification: gitlab
05_02_00__third_gitlab_specific_section_with_skipped_examples__strong_but_manually_modified_and_skipped__001:
- spec_txt_example_position: 9
+ spec_example_position: 9
source_specification: gitlab
06_01_00__api_request_overrides__group_upload_link__001:
- spec_txt_example_position: 10
+ spec_example_position: 10
source_specification: gitlab
06_02_00__api_request_overrides__project_repo_link__001:
- spec_txt_example_position: 11
+ spec_example_position: 11
source_specification: gitlab
06_03_00__api_request_overrides__project_snippet_ref__001:
- spec_txt_example_position: 12
+ spec_example_position: 12
source_specification: gitlab
06_04_00__api_request_overrides__personal_snippet_ref__001:
- spec_txt_example_position: 13
+ spec_example_position: 13
source_specification: gitlab
06_05_00__api_request_overrides__project_wiki_link__001:
- spec_txt_example_position: 14
+ spec_example_position: 14
source_specification: gitlab
YAML
end
@@ -696,7 +685,7 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do
canonical: |
<p><a href="projects-test-file">projects-test-file</a></p>
static: |-
- <p data-sourcepos="1:1-1:40" dir="auto"><a href="/glfm_group/glfm_project/-/blob/master/projects-test-file">projects-test-file</a></p>
+ <p data-sourcepos="1:1-1:40" dir="auto"><a href="/glfm_group/glfm_project/-/blob/master/projects-test-file" class="gfm">projects-test-file</a></p>
wysiwyg: |-
<p><a target="_blank" rel="noopener noreferrer nofollow" href="projects-test-file">projects-test-file</a></p>
06_03_00__api_request_overrides__project_snippet_ref__001:
diff --git a/spec/scripts/lib/glfm/update_specification_spec.rb b/spec/scripts/lib/glfm/update_specification_spec.rb
index 852b2b580e6..ccf1a8fd26a 100644
--- a/spec/scripts/lib/glfm/update_specification_spec.rb
+++ b/spec/scripts/lib/glfm/update_specification_spec.rb
@@ -37,18 +37,44 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
let(:ghfm_spec_md_path) { described_class::GHFM_SPEC_MD_PATH }
let(:ghfm_spec_txt_local_io) { StringIO.new(ghfm_spec_txt_contents) }
- let(:glfm_intro_md_path) { described_class::GLFM_INTRO_MD_PATH }
- let(:glfm_intro_md_io) { StringIO.new(glfm_intro_md_contents) }
- let(:glfm_official_specification_examples_md_path) { described_class::GLFM_OFFICIAL_SPECIFICATION_EXAMPLES_MD_PATH }
- let(:glfm_official_specification_examples_md_io) { StringIO.new(glfm_official_specification_examples_md_contents) }
- let(:glfm_internal_extension_examples_md_path) { described_class::GLFM_INTERNAL_EXTENSION_EXAMPLES_MD_PATH }
- let(:glfm_internal_extension_examples_md_io) { StringIO.new(glfm_internal_extension_examples_md_contents) }
+ let(:glfm_official_specification_md_path) { described_class::GLFM_OFFICIAL_SPECIFICATION_MD_PATH }
+ let(:glfm_official_specification_md_io) { StringIO.new(glfm_official_specification_md_contents) }
+ let(:glfm_internal_extensions_md_path) { described_class::GLFM_INTERNAL_EXTENSIONS_MD_PATH }
+ let(:glfm_internal_extensions_md_io) { StringIO.new(glfm_internal_extensions_md_contents) }
let(:glfm_spec_txt_path) { described_class::GLFM_SPEC_TXT_PATH }
let(:glfm_spec_txt_io) { StringIO.new }
let(:glfm_spec_html_path) { described_class::GLFM_SPEC_HTML_PATH }
let(:glfm_spec_html_io) { StringIO.new }
+ let(:es_snapshot_spec_md_path) { described_class::ES_SNAPSHOT_SPEC_MD_PATH }
+ let(:es_snapshot_spec_md_io) { StringIO.new }
+ let(:es_snapshot_spec_html_path) { described_class::ES_SNAPSHOT_SPEC_HTML_PATH }
+ let(:es_snapshot_spec_html_io) { StringIO.new }
let(:markdown_tempfile_io) { StringIO.new }
+ let(:ghfm_spec_txt_examples) do
+ <<~MARKDOWN
+ # Section with Examples
+
+ ## Emphasis and Strong
+
+ ```````````````````````````````` example
+ _EMPHASIS LINE 1_
+ _EMPHASIS LINE 2_
+ .
+ <p><em>EMPHASIS LINE 1</em>
+ <em>EMPHASIS LINE 2</em></p>
+ ````````````````````````````````
+
+ ```````````````````````````````` example
+ __STRONG!__
+ .
+ <p><strong>STRONG!</strong></p>
+ ````````````````````````````````
+
+ End of last GitHub examples section.
+ MARKDOWN
+ end
+
let(:ghfm_spec_txt_contents) do
<<~MARKDOWN
---
@@ -60,22 +86,9 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
# Introduction
- ## What is GitHub Flavored Markdown?
-
- It's like GLFM, but with an H.
-
- # Section with Examples
-
- ## Strong
-
- ```````````````````````````````` example
- __bold__
- .
- <p><strong>bold</strong></p>
- ````````````````````````````````
-
- End of last GitHub examples section.
+ GHFM Intro.
+ #{ghfm_spec_txt_examples}
<!-- END TESTS -->
# Appendix
@@ -84,26 +97,28 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
MARKDOWN
end
- let(:glfm_intro_md_contents) do
- # language=Markdown
+ let(:glfm_official_specification_md_examples) do
<<~MARKDOWN
- # Introduction
-
- ## What is GitLab Flavored Markdown?
+ # Official Specification Section with Examples
- Intro text about GitLab Flavored Markdown.
+ Some examples.
MARKDOWN
end
- let(:glfm_official_specification_examples_md_contents) do
+ let(:glfm_official_specification_md_contents) do
<<~MARKDOWN
- # Official Specification Section with Examples
+ # GLFM Introduction
- Some examples.
+ GLFM intro text.
+
+ <!-- BEGIN TESTS -->
+ #{glfm_official_specification_md_examples}
+ <!-- END TESTS -->
+ # Non-example official content
MARKDOWN
end
- let(:glfm_internal_extension_examples_md_contents) do
+ let(:glfm_internal_extensions_md_examples) do
<<~MARKDOWN
# Internal Extension Section with Examples
@@ -111,10 +126,19 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
MARKDOWN
end
+ let(:glfm_internal_extensions_md_contents) do
+ <<~MARKDOWN
+ # Non-example internal content
+ <!-- BEGIN TESTS -->
+ #{glfm_internal_extensions_md_examples}
+ <!-- END TESTS -->
+ # More non-example internal content
+ MARKDOWN
+ end
+
before do
# Mock default ENV var values
- allow(ENV).to receive(:[]).with('UPDATE_GHFM_SPEC_MD').and_return(nil)
- allow(ENV).to receive(:[]).and_call_original
+ stub_env('UPDATE_GHFM_SPEC_MD')
# We mock out the URI and local file IO objects with real StringIO, instead of just mock
# objects. This gives better and more realistic coverage, while still avoiding
@@ -124,17 +148,18 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
allow(URI).to receive(:parse).with(ghfm_spec_txt_uri).and_return(ghfm_spec_txt_uri_parsed)
allow(ghfm_spec_txt_uri_parsed).to receive(:open).and_return(ghfm_spec_txt_uri_io)
allow(File).to receive(:open).with(ghfm_spec_md_path) { ghfm_spec_txt_local_io }
- allow(File).to receive(:open).with(glfm_intro_md_path) { glfm_intro_md_io }
- allow(File).to receive(:open).with(glfm_official_specification_examples_md_path) do
- glfm_official_specification_examples_md_io
+ allow(File).to receive(:open).with(glfm_official_specification_md_path) do
+ glfm_official_specification_md_io
end
- allow(File).to receive(:open).with(glfm_internal_extension_examples_md_path) do
- glfm_internal_extension_examples_md_io
+ allow(File).to receive(:open).with(glfm_internal_extensions_md_path) do
+ glfm_internal_extensions_md_io
end
# output files
allow(File).to receive(:open).with(glfm_spec_txt_path, 'w') { glfm_spec_txt_io }
allow(File).to receive(:open).with(glfm_spec_html_path, 'w') { glfm_spec_html_io }
+ allow(File).to receive(:open).with(es_snapshot_spec_md_path, 'w') { es_snapshot_spec_md_io }
+ allow(File).to receive(:open).with(es_snapshot_spec_html_path, 'w') { es_snapshot_spec_html_io }
# Allow normal opening of Tempfile files created during script execution.
tempfile_basenames = [
@@ -166,7 +191,7 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
let(:ghfm_spec_txt_local_io) { StringIO.new }
before do
- allow(ENV).to receive(:[]).with('UPDATE_GHFM_SPEC_MD').and_return('true')
+ stub_env('UPDATE_GHFM_SPEC_MD', 'true')
allow(File).to receive(:open).with(ghfm_spec_md_path, 'w') { ghfm_spec_txt_local_io }
end
@@ -217,90 +242,121 @@ RSpec.describe Glfm::UpdateSpecification, '#process' do
end
end
- describe 'writing GLFM spec.txt' do
- let(:glfm_contents) { reread_io(glfm_spec_txt_io) }
+ describe 'writing output_spec/spec.txt' do
+ let(:glfm_spec_txt_contents) { reread_io(glfm_spec_txt_io) }
before do
subject.process(skip_spec_html_generation: true)
end
- it 'replaces the header text with the GitLab version' do
- expect(glfm_contents).not_to match(/GitHub Flavored Markdown Spec/m)
- expect(glfm_contents).not_to match(/^version: \d\.\d/m)
- expect(glfm_contents).not_to match(/^date: /m)
- expect(glfm_contents).not_to match(/^license: /m)
- expect(glfm_contents).to match(/#{Regexp.escape(described_class::GLFM_SPEC_TXT_HEADER)}\n/mo)
+ it 'includes only the header and official examples' do
+ expected = described_class::GLFM_SPEC_TXT_HEADER + glfm_official_specification_md_contents
+ expect(glfm_spec_txt_contents).to eq(expected)
end
+ end
- it 'replaces the intro section with the GitLab version' do
- expect(glfm_contents).not_to match(/What is GitHub Flavored Markdown/m)
- expect(glfm_contents).to match(/#{Regexp.escape(glfm_intro_md_contents)}/m)
- end
-
- it 'inserts the GitLab official spec and internal extension examples sections before the appendix section' do
- expected = <<~MARKDOWN
- End of last GitHub examples section.
-
- # Official Specification Section with Examples
-
- Some examples.
+ describe 'writing output_example_snapshots/snapshot_spec.md' do
+ let(:es_snapshot_spec_md_contents) { reread_io(es_snapshot_spec_md_io) }
- # Internal Extension Section with Examples
+ before do
+ subject.process(skip_spec_html_generation: true)
+ end
- Some examples.
+ it 'replaces the header text with the GitLab version' do
+ expect(es_snapshot_spec_md_contents).not_to match(/GitHub Flavored Markdown Spec/m)
+ expect(es_snapshot_spec_md_contents).not_to match(/^version: \d\.\d/m)
+ expect(es_snapshot_spec_md_contents).not_to match(/^date: /m)
- <!-- END TESTS -->
+ expect(es_snapshot_spec_md_contents).to match(/#{Regexp.escape(described_class::GLFM_SPEC_TXT_HEADER)}/mo)
+ end
- # Appendix
- MARKDOWN
- expect(glfm_contents).to match(/#{Regexp.escape(expected)}/m)
+ it 'includes header and all examples', :unlimited_max_formatted_output_length do
+ # rubocop:disable Style/StringConcatenation (string contatenation is more readable)
+ expected = described_class::GLFM_SPEC_TXT_HEADER +
+ ghfm_spec_txt_examples +
+ "\n" +
+ glfm_official_specification_md_examples +
+ "\n\n" + # NOTE: We want a blank line between the official and internal examples
+ glfm_internal_extensions_md_examples +
+ "\n"
+ # rubocop:enable Style/StringConcatenation
+ expect(es_snapshot_spec_md_contents).to eq(expected)
end
end
- describe 'writing GLFM spec.html' do
- let(:glfm_contents) { reread_io(glfm_spec_html_io) }
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
+ describe 'writing output html files' do
+ let(:spec_html_contents) { reread_io(glfm_spec_html_io) }
+ let(:snapshot_spec_html_contents) { reread_io(es_snapshot_spec_html_io) }
before do
subject.process
end
- it 'renders HTML from spec.txt', :unlimited_max_formatted_output_length do
- expected = <<~HTML
+ it 'renders expected HTML', :unlimited_max_formatted_output_length do
+ # NOTE: We do assertions for both output HTML files in this same `it` example block,
+ # because calling a full `subject.process` without `skip_spec_html_generation: true`
+ # is very slow, and want to avoid doing it twice.
+
+ expected_spec_html = <<~RENDERED_HTML
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="1:1-4:3" lang="yaml" class="code highlight js-syntax-highlight language-yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">GitLab Flavored Markdown (GLFM) Spec</span></span>
+ <span id="LC2" class="line" lang="yaml"><span class="na">version</span><span class="pi">:</span> <span class="s">alpha</span></span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ <h1 data-sourcepos="5:1-5:19" dir="auto">
+ <a id="user-content-glfm-introduction" class="anchor" href="#glfm-introduction" aria-hidden="true"></a>GLFM Introduction</h1>
+ <p data-sourcepos="7:1-7:16" dir="auto">GLFM intro text.</p>
+
+ <h1 data-sourcepos="10:1-10:46" dir="auto">
+ <a id="user-content-official-specification-section-with-examples" class="anchor" href="#official-specification-section-with-examples" aria-hidden="true"></a>Official Specification Section with Examples</h1>
+ <p data-sourcepos="12:1-12:14" dir="auto">Some examples.</p>
+
+ <h1 data-sourcepos="15:1-15:30" dir="auto">
+ <a id="user-content-non-example-official-content" class="anchor" href="#non-example-official-content" aria-hidden="true"></a>Non-example official content</h1>
+ RENDERED_HTML
+ expect(spec_html_contents).to be == expected_spec_html
+
+ expected_snapshot_spec_html = <<~RENDERED_HTML
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="1:1-4:3" class="code highlight js-syntax-highlight language-yaml" lang="yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">GitLab Flavored Markdown (GLFM) Spec</span></span>
+ <pre data-sourcepos="1:1-4:3" lang="yaml" class="code highlight js-syntax-highlight language-yaml" data-lang-params="frontmatter" v-pre="true"><code><span id="LC1" class="line" lang="yaml"><span class="na">title</span><span class="pi">:</span> <span class="s">GitLab Flavored Markdown (GLFM) Spec</span></span>
<span id="LC2" class="line" lang="yaml"><span class="na">version</span><span class="pi">:</span> <span class="s">alpha</span></span></code></pre>
<copy-code></copy-code>
</div>
- <h1 data-sourcepos="6:1-6:14" dir="auto">
- <a id="user-content-introduction" class="anchor" href="#introduction" aria-hidden="true"></a>Introduction</h1>
- <h2 data-sourcepos="8:1-8:36" dir="auto">
- <a id="user-content-what-is-gitlab-flavored-markdown" class="anchor" href="#what-is-gitlab-flavored-markdown" aria-hidden="true"></a>What is GitLab Flavored Markdown?</h2>
- <p data-sourcepos="10:1-10:42" dir="auto">Intro text about GitLab Flavored Markdown.</p>
- <h1 data-sourcepos="12:1-12:23" dir="auto">
+ <h1 data-sourcepos="5:1-5:23" dir="auto">
<a id="user-content-section-with-examples" class="anchor" href="#section-with-examples" aria-hidden="true"></a>Section with Examples</h1>
- <h2 data-sourcepos="14:1-14:9" dir="auto">
- <a id="user-content-strong" class="anchor" href="#strong" aria-hidden="true"></a>Strong</h2>
+ <h2 data-sourcepos="7:1-7:22" dir="auto">
+ <a id="user-content-emphasis-and-strong" class="anchor" href="#emphasis-and-strong" aria-hidden="true"></a>Emphasis and Strong</h2>
<div class="gl-relative markdown-code-block js-markdown-code">
- <pre data-sourcepos="16:1-20:32" class="code highlight js-syntax-highlight language-plaintext" lang="plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">__bold__</span>
- <span id="LC2" class="line" lang="plaintext">.</span>
- <span id="LC3" class="line" lang="plaintext">&lt;p&gt;&lt;strong&gt;bold&lt;/strong&gt;&lt;/p&gt;</span></code></pre>
+ <pre data-sourcepos="9:1-12:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">_EMPHASIS LINE 1_</span>
+ <span id="LC2" class="line" lang="plaintext">_EMPHASIS LINE 2_</span></code></pre>
<copy-code></copy-code>
</div>
- <p data-sourcepos="22:1-22:36" dir="auto">End of last GitHub examples section.</p>
- <h1 data-sourcepos="24:1-24:46" dir="auto">
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="14:1-17:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;em&gt;EMPHASIS LINE 1&lt;/em&gt;</span>
+ <span id="LC2" class="line" lang="plaintext">&lt;em&gt;EMPHASIS LINE 2&lt;/em&gt;&lt;/p&gt;</span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="19:1-21:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="example" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">__STRONG!__</span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ <div class="gl-relative markdown-code-block js-markdown-code">
+ <pre data-sourcepos="23:1-25:32" lang="plaintext" class="code highlight js-syntax-highlight language-plaintext" data-canonical-lang="" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">&lt;p&gt;&lt;strong&gt;STRONG!&lt;/strong&gt;&lt;/p&gt;</span></code></pre>
+ <copy-code></copy-code>
+ </div>
+ <p data-sourcepos="27:1-27:36" dir="auto">End of last GitHub examples section.</p>
+ <h1 data-sourcepos="29:1-29:46" dir="auto">
<a id="user-content-official-specification-section-with-examples" class="anchor" href="#official-specification-section-with-examples" aria-hidden="true"></a>Official Specification Section with Examples</h1>
- <p data-sourcepos="26:1-26:14" dir="auto">Some examples.</p>
- <h1 data-sourcepos="28:1-28:42" dir="auto">
+ <p data-sourcepos="31:1-31:14" dir="auto">Some examples.</p>
+ <h1 data-sourcepos="34:1-34:42" dir="auto">
<a id="user-content-internal-extension-section-with-examples" class="anchor" href="#internal-extension-section-with-examples" aria-hidden="true"></a>Internal Extension Section with Examples</h1>
- <p data-sourcepos="30:1-30:14" dir="auto">Some examples.</p>
-
- <h1 data-sourcepos="34:1-34:10" dir="auto">
- <a id="user-content-appendix" class="anchor" href="#appendix" aria-hidden="true"></a>Appendix</h1>
- <p data-sourcepos="36:1-36:14" dir="auto">Appendix text.</p>
- HTML
- expect(glfm_contents).to be == expected
+ <p data-sourcepos="36:1-36:14" dir="auto">Some examples.</p>
+ RENDERED_HTML
+ expect(snapshot_spec_html_contents).to be == expected_snapshot_spec_html
end
end
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
def reread_io(io)
# Reset the io StringIO to the beginning position of the buffer
diff --git a/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb b/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb
index fca037c9ff3..c678565fe90 100644
--- a/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb
+++ b/spec/scripts/lib/glfm/verify_all_generated_files_are_up_to_date_spec.rb
@@ -9,8 +9,8 @@ require_relative '../../../../scripts/lib/glfm/verify_all_generated_files_are_up
RSpec.describe Glfm::VerifyAllGeneratedFilesAreUpToDate, '#process' do
subject { described_class.new }
- let(:output_path) { described_class::GLFM_SPEC_OUTPUT_PATH }
- let(:snapshots_path) { described_class::EXAMPLE_SNAPSHOTS_PATH }
+ let(:output_path) { described_class::GLFM_OUTPUT_SPEC_PATH }
+ let(:snapshots_path) { described_class::ES_OUTPUT_EXAMPLE_SNAPSHOTS_PATH }
let(:verify_cmd) { "git status --porcelain #{output_path} #{snapshots_path}" }
before do
@@ -52,10 +52,13 @@ RSpec.describe Glfm::VerifyAllGeneratedFilesAreUpToDate, '#process' do
before do
# Simulate a clean repo, then simulate changes to generated files
allow(subject).to receive(:run_external_cmd).twice.with(verify_cmd).and_return('', "M #{snapshots_path}")
+ allow(subject).to receive(:run_external_cmd).with('git diff')
+ allow(subject).to receive(:warn).and_call_original
end
it 'raises an error', :unlimited_max_formatted_output_length do
- expect { subject.process }.to raise_error(/following files were modified.*#{snapshots_path}/m)
+ expect(subject).to receive(:warn).with(/following files were modified.*#{snapshots_path}/m)
+ expect { subject.process }.to raise_error(/The generated files are not up to date/)
end
end
end
diff --git a/spec/scripts/pipeline_test_report_builder_spec.rb b/spec/scripts/pipeline_test_report_builder_spec.rb
index 198cdefc530..b51b4dc4887 100644
--- a/spec/scripts/pipeline_test_report_builder_spec.rb
+++ b/spec/scripts/pipeline_test_report_builder_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
require_relative '../../scripts/pipeline_test_report_builder'
RSpec.describe PipelineTestReportBuilder do
diff --git a/spec/serializers/ci/pipeline_entity_spec.rb b/spec/serializers/ci/pipeline_entity_spec.rb
index 808dc38f653..ff364918b4f 100644
--- a/spec/serializers/ci/pipeline_entity_spec.rb
+++ b/spec/serializers/ci/pipeline_entity_spec.rb
@@ -15,12 +15,13 @@ RSpec.describe Ci::PipelineEntity do
subject { entity.as_json }
context 'when pipeline is empty' do
- let(:pipeline) { create(:ci_empty_pipeline) }
+ let(:pipeline) { create(:ci_empty_pipeline, name: 'Build pipeline') }
it 'contains required fields' do
expect(subject).to include :id, :iid, :user, :path, :coverage, :source
expect(subject).to include :ref, :commit
expect(subject).to include :updated_at, :created_at
+ expect(subject[:name]).to eq('Build pipeline')
end
it 'excludes coverage data when disabled' do
@@ -31,10 +32,15 @@ RSpec.describe Ci::PipelineEntity do
end
it 'contains details' do
+ allow(pipeline).to receive(:merge_request_event_type).and_return(:merged_result)
+
expect(subject).to include :details
expect(subject[:details])
- .to include :duration, :finished_at, :name
+ .to include :duration, :finished_at, :name, :event_type_name
expect(subject[:details][:status]).to include :icon, :favicon, :text, :label, :tooltip
+
+ expect(subject[:details][:event_type_name]).to eq('Merged result pipeline')
+ expect(subject[:details][:name]).to eq('Merged result pipeline')
end
it 'contains flags' do
@@ -43,6 +49,16 @@ RSpec.describe Ci::PipelineEntity do
.to include :stuck, :auto_devops, :yaml_errors,
:retryable, :cancelable, :merge_request
end
+
+ context 'when pipeline_name feature flag is disabled' do
+ before do
+ stub_feature_flags(pipeline_name: false)
+ end
+
+ it 'does not return name' do
+ is_expected.not_to include(:name)
+ end
+ end
end
context 'when default branch not protected' do
diff --git a/spec/serializers/codequality_degradation_entity_spec.rb b/spec/serializers/codequality_degradation_entity_spec.rb
index f56420bfdbd..0390e232fd5 100644
--- a/spec/serializers/codequality_degradation_entity_spec.rb
+++ b/spec/serializers/codequality_degradation_entity_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:severity]).to eq("major")
expect(subject[:file_path]).to eq("file_a.rb")
expect(subject[:line]).to eq(10)
+ expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_a.rb#L10")
end
end
@@ -28,6 +29,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:severity]).to eq("minor")
expect(subject[:file_path]).to eq("file_b.rb")
expect(subject[:line]).to eq(10)
+ expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10")
end
end
@@ -43,6 +45,7 @@ RSpec.describe CodequalityDegradationEntity do
expect(subject[:severity]).to eq("minor")
expect(subject[:file_path]).to eq("file_b.rb")
expect(subject[:line]).to eq(10)
+ expect(subject[:web_url]).to eq("http://localhost/root/test-project/-/blob/f572d396fae9206628714fb2ce00f72e94f2258f/file_b.rb#L10")
end
end
end
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 48099cb1fdf..fbb45162136 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -80,47 +80,13 @@ RSpec.describe DiffFileEntity do
end
end
- describe '#is_fully_expanded' do
- context 'file with a conflict' do
- let(:options) { { conflicts: { diff_file.new_path => double(diff_lines_for_serializer: [], conflict_type: :both_modified) } } }
-
- it 'returns false' do
- expect(diff_file).not_to receive(:fully_expanded?)
- expect(subject[:is_fully_expanded]).to eq(false)
- end
- end
- end
-
describe '#highlighted_diff_lines' do
- context 'file without a conflict' do
- let(:options) { { conflicts: {} } }
+ let(:options) { { conflicts: {} } }
- it 'calls diff_lines_for_serializer on diff_file' do
- # #diff_lines_for_serializer gets called in #fully_expanded? as well so we expect twice
- expect(diff_file).to receive(:diff_lines_for_serializer).twice.and_return([])
- expect(subject[:highlighted_diff_lines]).to eq([])
- end
- end
-
- context 'file with a conflict' do
- let(:conflict_file) { instance_double(Gitlab::Conflict::File, conflict_type: :both_modified) }
- let(:options) { { conflicts: { diff_file.new_path => conflict_file } } }
-
- it 'calls diff_lines_for_serializer on matching conflict file' do
- expect(conflict_file).to receive(:diff_lines_for_serializer).and_return([])
- expect(subject[:highlighted_diff_lines]).to eq([])
- end
-
- context 'when Gitlab::Git::Conflict::Parser::UnmergeableFile gets raised' do
- before do
- allow(conflict_file).to receive(:diff_lines_for_serializer).and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
- end
-
- it 'falls back to diff_file diff_lines_for_serializer' do
- expect(diff_file).to receive(:diff_lines_for_serializer).and_return([])
- expect(subject[:highlighted_diff_lines]).to eq([])
- end
- end
+ it 'calls diff_lines_for_serializer on diff_file' do
+ # #diff_lines_for_serializer gets called in #fully_expanded? as well so we expect twice
+ expect(diff_file).to receive(:diff_lines_for_serializer).twice.and_return([])
+ expect(subject[:highlighted_diff_lines]).to eq([])
end
end
diff --git a/spec/serializers/diffs_entity_spec.rb b/spec/serializers/diffs_entity_spec.rb
index 72777bde30c..ba40d538ccb 100644
--- a/spec/serializers/diffs_entity_spec.rb
+++ b/spec/serializers/diffs_entity_spec.rb
@@ -9,13 +9,13 @@ RSpec.describe DiffsEntity do
let(:request) { EntityRequest.new(project: project, current_user: user) }
let(:merge_request_diffs) { merge_request.merge_request_diffs }
- let(:allow_tree_conflicts) { false }
+ let(:merge_conflicts_in_diff) { false }
let(:options) do
{
request: request,
merge_request: merge_request,
merge_request_diffs: merge_request_diffs,
- allow_tree_conflicts: allow_tree_conflicts
+ merge_conflicts_in_diff: merge_conflicts_in_diff
}
end
@@ -87,60 +87,39 @@ RSpec.describe DiffsEntity do
end
end
- context 'when there are conflicts' do
+ describe 'diff_files' do
let(:diff_files) { merge_request_diffs.first.diffs.diff_files }
- let(:diff_file_with_conflict) { diff_files.to_a.last }
- let(:diff_file_without_conflict) { diff_files.to_a[-2] }
- let(:resolvable_conflicts) { true }
- let(:conflict_file) { double(path: diff_file_with_conflict.new_path, conflict_type: :both_modified) }
- let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: resolvable_conflicts) }
+ it 'serializes diff files using DiffFileEntity' do
+ expect(DiffFileEntity)
+ .to receive(:represent)
+ .with(
+ diff_files,
+ hash_including(options.merge(conflicts: nil))
+ )
- let(:merge_ref_head_diff) { true }
- let(:options) { super().merge(merge_ref_head_diff: merge_ref_head_diff) }
-
- before do
- allow(merge_request).to receive(:cannot_be_merged?).and_return(true)
- allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts)
- end
-
- it 'conflicts are highlighted' do
- expect(conflict_file).to receive(:diff_lines_for_serializer)
- expect(diff_file_with_conflict).not_to receive(:diff_lines_for_serializer)
- expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
-
- subject
- end
-
- context 'merge ref head diff is not chosen to be displayed' do
- let(:merge_ref_head_diff) { false }
-
- it 'conflicts are not calculated' do
- expect(MergeRequests::Conflicts::ListService).not_to receive(:new)
- end
+ subject[:diff_files]
end
- context 'when conflicts cannot be resolved' do
- let(:resolvable_conflicts) { false }
+ context 'when merge_conflicts_in_diff is true' do
+ let(:conflict_file) { double(path: diff_files.first.new_path, conflict_type: :both_modified) }
+ let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: false) }
+ let(:merge_conflicts_in_diff) { true }
- it 'conflicts are not highlighted' do
- expect(conflict_file).not_to receive(:diff_lines_for_serializer)
- expect(diff_file_with_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
- expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
-
- subject
+ before do
+ allow(merge_request).to receive(:cannot_be_merged?).and_return(true)
+ allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts)
end
- context 'when allow_tree_conflicts is set to true' do
- let(:allow_tree_conflicts) { true }
-
- it 'conflicts are still highlighted' do
- expect(conflict_file).to receive(:diff_lines_for_serializer)
- expect(diff_file_with_conflict).not_to receive(:diff_lines_for_serializer)
- expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
+ it 'serializes diff files with conflicts' do
+ expect(DiffFileEntity)
+ .to receive(:represent)
+ .with(
+ diff_files,
+ hash_including(options.merge(conflicts: { conflict_file.path => conflict_file }))
+ )
- subject
- end
+ subject[:diff_files]
end
end
end
diff --git a/spec/serializers/diffs_metadata_entity_spec.rb b/spec/serializers/diffs_metadata_entity_spec.rb
index 0e3d808aaac..04db576ffb5 100644
--- a/spec/serializers/diffs_metadata_entity_spec.rb
+++ b/spec/serializers/diffs_metadata_entity_spec.rb
@@ -9,6 +9,7 @@ RSpec.describe DiffsMetadataEntity do
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_diffs) { merge_request.merge_request_diffs }
let(:merge_request_diff) { merge_request_diffs.last }
+ let(:merge_conflicts_in_diff) { false }
let(:options) { {} }
let(:entity) do
@@ -17,7 +18,8 @@ RSpec.describe DiffsMetadataEntity do
options.merge(
request: request,
merge_request: merge_request,
- merge_request_diffs: merge_request_diffs
+ merge_request_diffs: merge_request_diffs,
+ merge_conflicts_in_diff: merge_conflicts_in_diff
)
)
end
@@ -54,49 +56,36 @@ RSpec.describe DiffsMetadataEntity do
end
end
- it 'returns diff files metadata' do
- payload = DiffFileMetadataEntity.represent(raw_diff_files).as_json
+ it 'serializes diff files metadata using DiffFileMetadataEntity' do
+ expect(DiffFileMetadataEntity)
+ .to receive(:represent)
+ .with(
+ raw_diff_files,
+ hash_including(options.merge(conflicts: nil))
+ )
- expect(subject[:diff_files]).to eq(payload)
+ subject[:diff_files]
end
- context 'when merge_ref_head_diff and allow_tree_conflicts options are set' do
+ context 'when merge_conflicts_in_diff is true' do
let(:conflict_file) { double(path: raw_diff_files.first.new_path, conflict_type: :both_modified) }
let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: false) }
+ let(:merge_conflicts_in_diff) { true }
before do
allow(merge_request).to receive(:cannot_be_merged?).and_return(true)
allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts)
end
- context 'when merge_ref_head_diff is true and allow_tree_conflicts is false' do
- let(:options) { { merge_ref_head_diff: true, allow_tree_conflicts: false } }
+ it 'serializes diff files with conflicts' do
+ expect(DiffFileMetadataEntity)
+ .to receive(:represent)
+ .with(
+ raw_diff_files,
+ hash_including(options.merge(conflicts: { conflict_file.path => conflict_file }))
+ )
- it 'returns diff files metadata without conflicts' do
- payload = DiffFileMetadataEntity.represent(raw_diff_files).as_json
-
- expect(subject[:diff_files]).to eq(payload)
- end
- end
-
- context 'when merge_ref_head_diff is false and allow_tree_conflicts is true' do
- let(:options) { { merge_ref_head_diff: false, allow_tree_conflicts: true } }
-
- it 'returns diff files metadata without conflicts' do
- payload = DiffFileMetadataEntity.represent(raw_diff_files).as_json
-
- expect(subject[:diff_files]).to eq(payload)
- end
- end
-
- context 'when merge_ref_head_diff and allow_tree_conflicts are true' do
- let(:options) { { merge_ref_head_diff: true, allow_tree_conflicts: true } }
-
- it 'returns diff files metadata with conflicts' do
- payload = DiffFileMetadataEntity.represent(raw_diff_files, conflicts: { conflict_file.path => conflict_file }).as_json
-
- expect(subject[:diff_files]).to eq(payload)
- end
+ subject[:diff_files]
end
end
end
diff --git a/spec/serializers/integrations/event_entity_spec.rb b/spec/serializers/integrations/event_entity_spec.rb
index 07281248f5b..1b72b5d290c 100644
--- a/spec/serializers/integrations/event_entity_spec.rb
+++ b/spec/serializers/integrations/event_entity_spec.rb
@@ -35,6 +35,7 @@ RSpec.describe Integrations::EventEntity do
expect(subject[:value]).to eq(false)
expect(subject[:field][:name]).to eq('note_channel')
expect(subject[:field][:value]).to eq('note-channel')
+ expect(subject[:field][:placeholder]).to eq('#general, #development')
end
end
end
diff --git a/spec/serializers/integrations/field_entity_spec.rb b/spec/serializers/integrations/field_entity_spec.rb
index 7af17cf6df6..4212a1ee6a2 100644
--- a/spec/serializers/integrations/field_entity_spec.rb
+++ b/spec/serializers/integrations/field_entity_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Integrations::FieldEntity do
describe '#as_json' do
context 'with Jira integration' do
- let(:integration) { create(:jira_integration) }
+ let(:integration) { build(:jira_integration) }
context 'with field with type text' do
let(:field) { integration_field('username') }
@@ -59,7 +59,7 @@ RSpec.describe Integrations::FieldEntity do
end
context 'with EmailsOnPush integration' do
- let(:integration) { create(:emails_on_push_integration, send_from_committer_email: '1') }
+ let(:integration) { build(:emails_on_push_integration, send_from_committer_email: '1') }
context 'with field with type checkbox' do
let(:field) { integration_field('send_from_committer_email') }
@@ -111,6 +111,36 @@ RSpec.describe Integrations::FieldEntity do
end
end
end
+
+ context 'with chat integration' do
+ let(:integration) { build(:mattermost_integration) }
+ let(:field) { integration_field('webhook') }
+
+ it 'exposes correct attributes but masks webhook' do
+ expected_hash = {
+ section: nil,
+ type: 'text',
+ name: 'webhook',
+ title: nil,
+ placeholder: nil,
+ help: 'http://mattermost.example.com/hooks/',
+ required: true,
+ choices: nil,
+ value: '************',
+ checkbox_label: nil
+ }
+
+ is_expected.to eq(expected_hash)
+ end
+
+ context 'when webhook was not set' do
+ let(:integration) { build(:mattermost_integration, webhook: nil) }
+
+ it 'does not show the masked webhook' do
+ expect(subject[:value]).to be_nil
+ end
+ end
+ end
end
def integration_field(name)
diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb
index 7a6a496912f..0c9c8f05e17 100644
--- a/spec/serializers/issue_board_entity_spec.rb
+++ b/spec/serializers/issue_board_entity_spec.rb
@@ -57,8 +57,18 @@ RSpec.describe IssueBoardEntity do
context 'when issue is of type task' do
let(:resource) { create(:issue, :task, project: project) }
- it 'has a work item path' do
- expect(subject[:real_path]).to eq(project_work_items_path(project, resource.id))
+ context 'when the use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
+ it 'has a work item path' do
+ expect(subject[:real_path]).to eq(project_work_items_path(project, resource.id))
+ end
+ end
+
+ it 'has a work item path with iid' do
+ expect(subject[:real_path]).to eq(project_work_items_path(project, resource.iid, iid_path: true))
end
end
end
diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb
index 25e9e8c17e2..6161d4d7ec2 100644
--- a/spec/serializers/issue_entity_spec.rb
+++ b/spec/serializers/issue_entity_spec.rb
@@ -17,9 +17,19 @@ RSpec.describe IssueEntity do
context 'when issue is of type task' do
let(:resource) { create(:issue, :task, project: project) }
- # This was already a path and not a url when the work items change was introduced
- it 'has a work item path' do
- expect(subject[:web_url]).to eq(project_work_items_path(project, resource.id))
+ context 'when use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
+ # This was already a path and not a url when the work items change was introduced
+ it 'has a work item path' do
+ expect(subject[:web_url]).to eq(project_work_items_path(project, resource.id))
+ end
+ end
+
+ it 'has a work item path with iid' do
+ expect(subject[:web_url]).to eq(project_work_items_path(project, resource.iid, iid_path: true))
end
end
end
diff --git a/spec/serializers/linked_project_issue_entity_spec.rb b/spec/serializers/linked_project_issue_entity_spec.rb
index c4646754f16..523b89921b6 100644
--- a/spec/serializers/linked_project_issue_entity_spec.rb
+++ b/spec/serializers/linked_project_issue_entity_spec.rb
@@ -51,8 +51,20 @@ RSpec.describe LinkedProjectIssueEntity do
related_issue.update!(issue_type: :task, work_item_type: WorkItems::Type.default_by_type(:task))
end
- it 'returns a work items path' do
- expect(serialized_entity).to include(path: project_work_items_path(related_issue.project, related_issue.id))
+ context 'when use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ end
+
+ it 'returns a work items path' do
+ expect(serialized_entity).to include(path: project_work_items_path(related_issue.project, related_issue.id))
+ end
+ end
+
+ it 'returns a work items path using iid' do
+ expect(serialized_entity).to include(
+ path: project_work_items_path(related_issue.project, related_issue.iid, iid_path: true)
+ )
end
end
end
diff --git a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
index e9c1fe23855..702c6d9fe98 100644
--- a/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_cached_widget_entity_spec.rb
@@ -184,22 +184,6 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
end
end
- describe 'auto merge' do
- context 'when auto merge is enabled' do
- let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
-
- it 'returns auto merge related information' do
- expect(subject[:auto_merge_enabled]).to be_truthy
- end
- end
-
- context 'when auto merge is not enabled' do
- it 'returns auto merge related information' do
- expect(subject[:auto_merge_enabled]).to be_falsy
- end
- end
- end
-
describe 'squash defaults for projects' do
where(:squash_option, :value, :default, :readonly) do
'always' | true | true | true
diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb
index 59ffba0e7a9..418f629a301 100644
--- a/spec/serializers/merge_request_poll_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb
@@ -184,10 +184,4 @@ RSpec.describe MergeRequestPollWidgetEntity do
end
end
end
-
- describe '#mergeable_discussions_state?' do
- it 'returns mergeable discussions state' do
- expect(subject[:mergeable_discussions_state]).to eq(true)
- end
- end
end
diff --git a/spec/serializers/merge_requests/pipeline_entity_spec.rb b/spec/serializers/merge_requests/pipeline_entity_spec.rb
index ee99ab2e7dd..a8f4fc44f10 100644
--- a/spec/serializers/merge_requests/pipeline_entity_spec.rb
+++ b/spec/serializers/merge_requests/pipeline_entity_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe MergeRequests::PipelineEntity do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
- let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, name: 'Build pipeline') }
let(:request) { double('request') }
@@ -25,15 +25,20 @@ RSpec.describe MergeRequests::PipelineEntity do
describe '#as_json' do
it 'contains required fields' do
+ allow(pipeline).to receive(:merge_request_event_type).and_return(:merged_result)
+
is_expected.to include(
:id, :path, :active, :coverage, :ref, :commit, :details,
- :flags, :triggered, :triggered_by
+ :flags, :triggered, :triggered_by, :name
)
expect(subject[:commit]).to include(:short_id, :commit_path)
expect(subject[:ref]).to include(:branch)
- expect(subject[:details]).to include(:artifacts, :name, :status, :stages, :finished_at)
+ expect(subject[:details]).to include(:artifacts, :name, :event_type_name, :status, :stages, :finished_at)
expect(subject[:details][:status]).to include(:icon, :favicon, :text, :label, :tooltip)
expect(subject[:flags]).to include(:merge_request_pipeline)
+
+ expect(subject[:details][:event_type_name]).to eq('Merged result pipeline')
+ expect(subject[:details][:name]).to eq('Merged result pipeline')
end
it 'returns presented coverage' do
@@ -46,5 +51,15 @@ RSpec.describe MergeRequests::PipelineEntity do
expect(entity.as_json).not_to include(:coverage)
end
+
+ context 'when pipeline_name feature flag is disabled' do
+ before do
+ stub_feature_flags(pipeline_name: false)
+ end
+
+ it 'does not return name' do
+ is_expected.not_to include(:name)
+ end
+ end
end
end
diff --git a/spec/serializers/paginated_diff_entity_spec.rb b/spec/serializers/paginated_diff_entity_spec.rb
index 9d4456c11d6..3d77beb9abc 100644
--- a/spec/serializers/paginated_diff_entity_spec.rb
+++ b/spec/serializers/paginated_diff_entity_spec.rb
@@ -7,13 +7,13 @@ RSpec.describe PaginatedDiffEntity do
let(:request) { double('request', current_user: user) }
let(:merge_request) { create(:merge_request) }
let(:diff_batch) { merge_request.merge_request_diff.diffs_in_batch(2, 3, diff_options: nil) }
- let(:allow_tree_conflicts) { false }
+ let(:merge_conflicts_in_diff) { false }
let(:options) do
{
request: request,
merge_request: merge_request,
pagination_data: diff_batch.pagination_data,
- allow_tree_conflicts: allow_tree_conflicts
+ merge_conflicts_in_diff: merge_conflicts_in_diff
}
end
@@ -29,61 +29,39 @@ RSpec.describe PaginatedDiffEntity do
expect(subject[:pagination]).to eq(total_pages: 20)
end
- context 'when there are conflicts' do
- let(:diff_batch) { merge_request.merge_request_diff.diffs_in_batch(7, 3, diff_options: nil) }
- let(:diff_files) { diff_batch.diff_files.to_a }
- let(:diff_file_with_conflict) { diff_files.last }
- let(:diff_file_without_conflict) { diff_files.first }
+ describe 'diff_files' do
+ let(:diff_files) { diff_batch.diff_files(sorted: true) }
- let(:resolvable_conflicts) { true }
- let(:conflict_file) { double(path: diff_file_with_conflict.new_path, conflict_type: :both_modified) }
- let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: resolvable_conflicts) }
+ it 'serializes diff files using DiffFileEntity' do
+ expect(DiffFileEntity)
+ .to receive(:represent)
+ .with(
+ diff_files,
+ hash_including(options.merge(conflicts: nil))
+ )
- let(:merge_ref_head_diff) { true }
- let(:options) { super().merge(merge_ref_head_diff: merge_ref_head_diff) }
-
- before do
- allow(merge_request).to receive(:cannot_be_merged?).and_return(true)
- allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts)
- end
-
- it 'conflicts are highlighted' do
- expect(conflict_file).to receive(:diff_lines_for_serializer)
- expect(diff_file_with_conflict).not_to receive(:diff_lines_for_serializer)
- expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
-
- subject
- end
-
- context 'merge ref head diff is not chosen to be displayed' do
- let(:merge_ref_head_diff) { false }
-
- it 'conflicts are not calculated' do
- expect(MergeRequests::Conflicts::ListService).not_to receive(:new)
- end
+ subject[:diff_files]
end
- context 'when conflicts cannot be resolved' do
- let(:resolvable_conflicts) { false }
+ context 'when merge_conflicts_in_diff is true' do
+ let(:conflict_file) { double(path: diff_files.first.new_path, conflict_type: :both_modified) }
+ let(:conflicts) { double(conflicts: double(files: [conflict_file]), can_be_resolved_in_ui?: false) }
+ let(:merge_conflicts_in_diff) { true }
- it 'conflicts are not highlighted' do
- expect(conflict_file).not_to receive(:diff_lines_for_serializer)
- expect(diff_file_with_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
- expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
-
- subject
+ before do
+ allow(merge_request).to receive(:cannot_be_merged?).and_return(true)
+ allow(MergeRequests::Conflicts::ListService).to receive(:new).and_return(conflicts)
end
- context 'when allow_tree_conflicts is set to true' do
- let(:allow_tree_conflicts) { true }
-
- it 'conflicts are still highlighted' do
- expect(conflict_file).to receive(:diff_lines_for_serializer)
- expect(diff_file_with_conflict).not_to receive(:diff_lines_for_serializer)
- expect(diff_file_without_conflict).to receive(:diff_lines_for_serializer).twice # for highlighted_diff_lines and is_fully_expanded
+ it 'serializes diff files with conflicts' do
+ expect(DiffFileEntity)
+ .to receive(:represent)
+ .with(
+ diff_files,
+ hash_including(options.merge(conflicts: { conflict_file.path => conflict_file }))
+ )
- subject
- end
+ subject[:diff_files]
end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 4d9bdc4bb17..9caaeb3450b 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -163,10 +163,8 @@ RSpec.describe PipelineSerializer do
context 'with different refs' do
before do
- # rubocop:disable Rails/SkipsModelValidations
Ci::Pipeline.update_all(%(ref = 'feature-' || id))
Ci::Build.update_all(%(ref = 'feature-' || stage_id))
- # rubocop:enable Rails/SkipsModelValidations
end
it 'verifies number of queries', :request_store do
@@ -240,6 +238,7 @@ RSpec.describe PipelineSerializer do
create(:ci_empty_pipeline,
project: project,
status: status,
+ name: 'Build pipeline',
ref: 'feature').tap do |pipeline|
Ci::Build::AVAILABLE_STATUSES.each do |build_status|
create_build(pipeline, status, build_status)
diff --git a/spec/serializers/remote_mirror_entity_spec.rb b/spec/serializers/remote_mirror_entity_spec.rb
index 4cbf87e4d67..c6290e15995 100644
--- a/spec/serializers/remote_mirror_entity_spec.rb
+++ b/spec/serializers/remote_mirror_entity_spec.rb
@@ -3,8 +3,7 @@
require 'spec_helper'
RSpec.describe RemoteMirrorEntity do
- let(:project) { create(:project, :repository, :remote_mirror, url: "https://test:password@gitlab.com") }
- let(:remote_mirror) { project.remote_mirrors.first }
+ let(:remote_mirror) { build_stubbed(:remote_mirror, url: "https://test:password@gitlab.com") }
let(:entity) { described_class.new(remote_mirror) }
subject { entity.as_json }
diff --git a/spec/services/admin/set_feature_flag_service_spec.rb b/spec/services/admin/set_feature_flag_service_spec.rb
index 6fa806644c9..9a9c5545e23 100644
--- a/spec/services/admin/set_feature_flag_service_spec.rb
+++ b/spec/services/admin/set_feature_flag_service_spec.rb
@@ -130,6 +130,15 @@ RSpec.describe Admin::SetFeatureFlagService do
end
end
+ context 'when enabling for a repository' do
+ let(:params) { { value: 'true', repository: project.repository.full_path } }
+
+ it 'enables the feature flag' do
+ expect(Feature).to receive(:enable).with(feature_name, project.repository)
+ expect(subject).to be_success
+ end
+ end
+
context 'when enabling for a user actor and a feature group' do
let(:params) { { value: 'true', user: user.username, feature_group: 'perf_team' } }
let(:feature_group) { Feature.group('perf_team') }
diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index 73d185283b6..676f55be28a 100644
--- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -225,7 +225,7 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
let!(:build) do
create(:ci_build, :created, pipeline: pipeline, ref: ref,
- name: 'build', ci_stage: build_stage )
+ name: 'build', ci_stage: build_stage)
end
let!(:test) do
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 72027911e51..1959710bb0c 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Boards::Issues::ListService do
let_it_be(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let_it_be(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2]) }
- let_it_be(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Reopened Issue 1' ) }
+ let_it_be(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Reopened Issue 1') }
let_it_be(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, development]) }
let_it_be(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
@@ -110,7 +110,7 @@ RSpec.describe Boards::Issues::ListService do
let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2, p2_project]) }
let!(:opened_issue3) { create(:labeled_issue, project: project_archived, milestone: m1, title: 'Issue 3', labels: [bug]) }
- let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Reopened Issue 1', closed_at: Time.current ) }
+ let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Reopened Issue 1', closed_at: Time.current) }
let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, p2_project, development]) }
let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb
index 26cc1a0665e..19a32aafa38 100644
--- a/spec/services/branches/create_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -56,17 +56,6 @@ RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
end
end
- context 'when an ambiguous branch name is provided' do
- let(:branches) { { 'ambiguous/test' => 'master', 'ambiguous' => 'master' } }
-
- it 'returns an error that branch could not be created' do
- err_msg = 'Failed to create branch \'ambiguous\': 13:reference is ambiguous.'
-
- expect(subject[:status]).to eq(:error)
- expect(subject[:message]).to match_array([err_msg])
- end
- end
-
context 'when PreReceiveError exception' do
let(:branches) { { 'error' => 'master' } }
@@ -184,18 +173,6 @@ RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do
end
end
- context 'when an ambiguous branch name is provided' do
- it 'returns an error that branch could not be created' do
- err_msg = 'Failed to create branch \'feature\': 13:reference is ambiguous.'
-
- service.execute('feature/widget', 'master')
- result = service.execute('feature', 'master')
-
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq(err_msg)
- end
- end
-
it 'logs and returns an error if there is a PreReceiveError exception' do
error_message = 'pre receive error'
raw_message = "GitLab: #{error_message}"
diff --git a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
index 0de962328c5..5a7852fc32f 100644
--- a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
+++ b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb
@@ -77,6 +77,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
importer: 'gitlab_migration',
pipeline_name: 'PipelineClass4',
minimum_source_version: '15.1.0',
@@ -88,6 +90,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do
message: 'Pipeline skipped as source instance version not compatible with pipeline',
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
importer: 'gitlab_migration',
pipeline_name: 'PipelineClass5',
minimum_source_version: '16.0.0',
diff --git a/spec/services/ci/after_requeue_job_service_spec.rb b/spec/services/ci/after_requeue_job_service_spec.rb
index 1f692bdb71a..e6f46fb9ebe 100644
--- a/spec/services/ci/after_requeue_job_service_spec.rb
+++ b/spec/services/ci/after_requeue_job_service_spec.rb
@@ -120,26 +120,6 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
)
end
- context 'when the FF ci_requeue_with_dag_object_hierarchy is disabled' do
- before do
- stub_feature_flags(ci_requeue_with_dag_object_hierarchy: false)
- end
-
- it 'marks subsequent skipped jobs as processable but leaves a3 created' do
- execute_after_requeue_service(a1)
-
- check_jobs_statuses(
- a1: 'pending',
- a2: 'created',
- a3: 'skipped',
- b1: 'success',
- b2: 'created',
- c1: 'created',
- c2: 'created'
- )
- end
- end
-
context 'when executed by a different user than the original owner' do
let(:retryer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:service) { described_class.new(project, retryer) }
@@ -312,22 +292,6 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do
c: 'created'
)
end
-
- context 'when the FF ci_requeue_with_dag_object_hierarchy is disabled' do
- before do
- stub_feature_flags(ci_requeue_with_dag_object_hierarchy: false)
- end
-
- it 'marks the next subsequent skipped job as processable but leaves c skipped' do
- execute_after_requeue_service(a)
-
- check_jobs_statuses(
- a: 'pending',
- b: 'created',
- c: 'skipped'
- )
- end
- end
end
private
diff --git a/spec/services/ci/compare_test_reports_service_spec.rb b/spec/services/ci/compare_test_reports_service_spec.rb
index 6d3df0f5383..f259072fe87 100644
--- a/spec/services/ci/compare_test_reports_service_spec.rb
+++ b/spec/services/ci/compare_test_reports_service_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe Ci::CompareTestReportsService do
it 'returns a parsed TestReports success status and failure on the individual suite' do
expect(comparison[:status]).to eq(:parsed)
expect(comparison.dig(:data, 'status')).to eq('success')
- expect(comparison.dig(:data, 'suites', 0, 'status') ).to eq('error')
+ expect(comparison.dig(:data, 'suites', 0, 'status')).to eq('error')
end
end
diff --git a/spec/services/ci/create_pipeline_service/partitioning_spec.rb b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
index 43fbb74ede4..f34d103d965 100644
--- a/spec/services/ci/create_pipeline_service/partitioning_spec.rb
+++ b/spec/services/ci/create_pipeline_service/partitioning_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :aggregate_failures do
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :aggregate_failures,
+:ci_partitionable do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
@@ -31,7 +32,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
let(:pipeline) { service.execute(:push).payload }
- let(:current_partition_id) { 123 }
+ let(:current_partition_id) { ci_testing_partition_id }
before do
stub_ci_pipeline_yaml_file(config)
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index c737b8cc329..5fdefb2b306 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -1425,5 +1425,43 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
it_behaves_like 'comparing file changes with workflow rules'
end
end
+
+ context 'workflow name with rules' do
+ let(:ref) { 'refs/heads/feature' }
+
+ let(:variables) do
+ [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAL' }]
+ end
+
+ let(:pipeline) do
+ execute_service do |pipeline|
+ pipeline.variables.build(variables)
+ end.payload
+ end
+
+ let(:config) do
+ <<-EOY
+ workflow:
+ name: '$PIPELINE_NAME $SOME_VARIABLE'
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ variables:
+ PIPELINE_NAME: 'Name 1'
+ - if: $CI_COMMIT_REF_NAME =~ /feature/
+ variables:
+ PIPELINE_NAME: 'Name 2'
+
+ job:
+ stage: test
+ script: echo 'hello'
+ EOY
+ end
+
+ it 'substitutes variables in pipeline name' do
+ expect(response).not_to be_error
+ expect(pipeline).to be_persisted
+ expect(pipeline.name).to eq('Name 2 SOME_VAL')
+ end
+ end
end
end
diff --git a/spec/services/ci/create_pipeline_service/variables_spec.rb b/spec/services/ci/create_pipeline_service/variables_spec.rb
new file mode 100644
index 00000000000..e9e0cf2c6e0
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/variables_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.first_owner }
+
+ let(:service) { described_class.new(project, user, { ref: 'master' }) }
+ let(:pipeline) { service.execute(:push).payload }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when using variables' do
+ context 'when variables have expand: true/false' do
+ let(:config) do
+ <<-YAML
+ variables:
+ VAR7:
+ value: "value 7 $CI_PIPELINE_ID"
+ expand: false
+ VAR8:
+ value: "value 8 $CI_PIPELINE_ID"
+ expand: false
+
+ rspec:
+ script: rspec
+ variables:
+ VAR1: "JOBID-$CI_JOB_ID"
+ VAR2: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ VAR3:
+ value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ expand: false
+ VAR4:
+ value: "JOBID-$CI_JOB_ID"
+ expand: false
+ VAR5: "PIPELINEID-$CI_PIPELINE_ID and $VAR4"
+ VAR6:
+ value: "PIPELINEID-$CI_PIPELINE_ID and $VAR4"
+ expand: false
+ VAR7: "overridden value 7 $CI_PIPELINE_ID"
+ YAML
+ end
+
+ let(:rspec) { find_job('rspec') }
+
+ it 'creates the pipeline with a job that has variable expanded according to "expand"' do
+ expect(pipeline).to be_created_successfully
+
+ expect(Ci::BuildRunnerPresenter.new(rspec).runner_variables).to include(
+ { key: 'VAR1', value: "JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR3', value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1", public: true, masked: false, raw: true },
+ { key: 'VAR4', value: "JOBID-$CI_JOB_ID", public: true, masked: false, raw: true },
+ { key: 'VAR5', value: "PIPELINEID-#{pipeline.id} and $VAR4", public: true, masked: false },
+ { key: 'VAR6', value: "PIPELINEID-$CI_PIPELINE_ID and $VAR4", public: true, masked: false, raw: true },
+ { key: 'VAR7', value: "overridden value 7 #{pipeline.id}", public: true, masked: false },
+ { key: 'VAR8', value: "value 8 $CI_PIPELINE_ID", public: true, masked: false, raw: true }
+ )
+ end
+
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
+ before do
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
+ end
+
+ it 'creates the pipeline with a job that has all variables expanded' do
+ expect(pipeline).to be_created_successfully
+
+ expect(Ci::BuildRunnerPresenter.new(rspec).runner_variables).to include(
+ { key: 'VAR1', value: "JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR3', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR4', value: "JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR5', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR6', value: "PIPELINEID-#{pipeline.id} and JOBID-#{rspec.id}", public: true, masked: false },
+ { key: 'VAR7', value: "overridden value 7 #{pipeline.id}", public: true, masked: false },
+ { key: 'VAR8', value: "value 8 #{pipeline.id}", public: true, masked: false }
+ )
+ end
+ end
+ end
+
+ context 'when trigger variables have expand: true/false' do
+ let(:config) do
+ <<-YAML
+ child:
+ variables:
+ VAR1: "PROJECTID-$CI_PROJECT_ID"
+ VAR2: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ VAR3:
+ value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1"
+ expand: false
+ trigger:
+ include: child.yml
+ YAML
+ end
+
+ let(:child) { find_job('child') }
+
+ it 'creates the pipeline with a trigger job that has downstream_variables expanded according to "expand"' do
+ expect(pipeline).to be_created_successfully
+
+ expect(child.downstream_variables).to include(
+ { key: 'VAR1', value: "PROJECTID-#{project.id}" },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and PROJECTID-$CI_PROJECT_ID" },
+ { key: 'VAR3', value: "PIPELINEID-$CI_PIPELINE_ID and $VAR1", raw: true }
+ )
+ end
+
+ context 'when the FF ci_raw_variables_in_yaml_config is disabled' do
+ before do
+ stub_feature_flags(ci_raw_variables_in_yaml_config: false)
+ end
+
+ it 'creates the pipeline with a job that has all variables expanded' do
+ expect(pipeline).to be_created_successfully
+
+ expect(child.downstream_variables).to include(
+ { key: 'VAR1', value: "PROJECTID-#{project.id}" },
+ { key: 'VAR2', value: "PIPELINEID-#{pipeline.id} and PROJECTID-$CI_PROJECT_ID" },
+ { key: 'VAR3', value: "PIPELINEID-#{pipeline.id} and PROJECTID-$CI_PROJECT_ID" }
+ )
+ end
+ end
+ end
+ end
+
+ private
+
+ def find_job(name)
+ pipeline.processables.find { |job| job.name == name }
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 458692ba1c0..67c13649c6f 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -135,7 +135,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
execute_service
expect(histogram).to have_received(:observe)
- .with({ source: 'push' }, 5)
+ .with({ source: 'push', plan: project.actual_plan_name }, 5)
end
it 'tracks included template usage' do
@@ -1867,49 +1867,4 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
end
end
end
-
- describe '#execute!' do
- subject { service.execute!(*args) }
-
- let(:service) { described_class.new(project, user, ref: ref_name) }
- let(:args) { [:push] }
-
- context 'when user has a permission to create a pipeline' do
- let(:user) { create(:user) }
-
- before do
- project.add_developer(user)
- end
-
- it 'does not raise an error' do
- expect { subject }.not_to raise_error
- end
-
- it 'creates a pipeline' do
- expect { subject }.to change { Ci::Pipeline.count }.by(1)
- end
- end
-
- context 'when user does not have a permission to create a pipeline' do
- let(:user) { create(:user) }
-
- it 'raises an error' do
- expect { subject }
- .to raise_error(described_class::CreateError)
- .with_message('Insufficient permissions to create a new pipeline')
- end
- end
-
- context 'when a user with permissions has been blocked' do
- before do
- user.block!
- end
-
- it 'raises an error' do
- expect { subject }
- .to raise_error(described_class::CreateError)
- .with_message('Insufficient permissions to create a new pipeline')
- end
- end
- end
end
diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb
index 030ba84951e..5df590a1b78 100644
--- a/spec/services/ci/job_artifacts/create_service_spec.rb
+++ b/spec/services/ci/job_artifacts/create_service_spec.rb
@@ -181,8 +181,8 @@ RSpec.describe Ci::JobArtifacts::CreateService do
end
end
- context 'with job partitioning' do
- let(:pipeline) { create(:ci_pipeline, project: project, partition_id: 123) }
+ context 'with job partitioning', :ci_partitionable do
+ let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'sets partition_id on artifacts' do
@@ -190,7 +190,7 @@ RSpec.describe Ci::JobArtifacts::CreateService do
artifacts_partitions = job.job_artifacts.map(&:partition_id).uniq
- expect(artifacts_partitions).to eq([123])
+ expect(artifacts_partitions).to eq([ci_testing_partition_id])
end
end
diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
index 54d1cacc068..79920dcb2c7 100644
--- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
+++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb
@@ -60,10 +60,9 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
execute
end
- it 'preserves trace artifacts and removes any timestamp' do
+ it 'preserves trace artifacts' do
expect { subject }
- .to change { trace_artifact.reload.expire_at }.from(trace_artifact.expire_at).to(nil)
- .and not_change { Ci::JobArtifact.exists?(trace_artifact.id) }
+ .to not_change { Ci::JobArtifact.exists?(trace_artifact.id) }
end
context 'when artifact belongs to a project that is undergoing stats refresh' do
@@ -277,81 +276,5 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do
is_expected.to eq(destroyed_artifacts_count: 0, statistics_updates: {}, status: :success)
end
end
-
- context 'with artifacts that has backfilled expire_at' do
- let!(:created_on_00_30_45_minutes_on_21_22_23) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 01:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-22 12:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-22 12:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 23:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 23:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-23 06:45:00.000'))
- ]
- end
-
- let!(:created_close_to_00_or_30_minutes) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:00:00.001')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-21 00:30:00.999'))
- ]
- end
-
- let!(:created_on_00_or_30_minutes_on_other_dates) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-01 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 12:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 23:30:00.000'))
- ]
- end
-
- let!(:created_at_other_times) do
- [
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-19 00:30:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 00:00:00.000')),
- create(:ci_job_artifact, expire_at: Time.zone.parse('2022-01-24 00:30:00.000'))
- ]
- end
-
- let(:artifacts_to_keep) { created_on_00_30_45_minutes_on_21_22_23 }
- let(:artifacts_to_delete) { created_close_to_00_or_30_minutes + created_on_00_or_30_minutes_on_other_dates + created_at_other_times }
- let(:all_artifacts) { artifacts_to_keep + artifacts_to_delete }
-
- let(:artifacts) { Ci::JobArtifact.where(id: all_artifacts.map(&:id)) }
-
- it 'deletes job artifacts that do not have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(artifacts_to_delete.size * -1)
- end
-
- it 'keeps job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do
- expect { subject }.not_to change { Ci::JobArtifact.where(id: artifacts_to_keep.map(&:id)).count }
- end
-
- it 'removes expire_at on job artifacts that have expire_at on 00, 30 or 45 minute of 21, 22, 23 of the month' do
- subject
-
- expect(artifacts_to_keep.all? { |artifact| artifact.reload.expire_at.nil? }).to be(true)
- end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ci_detect_wrongly_expired_artifacts: false)
- end
-
- it 'deletes all job artifacts' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(all_artifacts.size * -1)
- end
- end
-
- context 'when fix_expire_at is false' do
- let(:service) { described_class.new(artifacts, pick_up_at: Time.current, fix_expire_at: false) }
-
- it 'deletes all job artifacts' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(all_artifacts.size * -1)
- end
- end
- end
end
end
diff --git a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
index 6d9fc4c8e34..d4d56825e1f 100644
--- a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
+++ b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb
@@ -9,7 +9,9 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
- let(:test_event_name) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_1) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_2) { 'i_testing_coverage_report_uploaded' }
+
let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
let(:start_time) { 1.week.ago }
let(:end_time) { 1.week.from_now }
@@ -25,15 +27,15 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
end
end
- it 'tracks the event using HLLRedisCounter' do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ it 'tracks the test event using HLLRedisCounter' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_1, values: user1.id)
.and_call_original
expect { track_artifact_report }
.to change {
- counter.unique_events(event_names: test_event_name,
+ counter.unique_events(event_names: test_event_name_1,
start_date: start_time,
end_date: end_time)
}
@@ -44,12 +46,20 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
context 'when pipeline does not have test reports' do
let_it_be(:pipeline) { create(:ci_empty_pipeline) }
- it 'does not track the event' do
+ it 'does not track the test event' do
track_artifact_report
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.not_to receive(:track_event)
- .with(anything, test_event_name)
+ .with(anything, test_event_name_1)
+ end
+
+ it 'does not track the coverage test event' do
+ track_artifact_report
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to receive(:track_event)
+ .with(anything, test_event_name_2)
end
end
@@ -57,15 +67,15 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
- it 'tracks all pipelines using HLLRedisCounter by one user_id' do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ it 'tracks all pipelines using HLLRedisCounter by one user_id for the test event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_1, values: user1.id)
.and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_1, values: user1.id)
.and_call_original
expect do
@@ -73,7 +83,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
described_class.new.execute(pipeline2)
end
.to change {
- counter.unique_events(event_names: test_event_name,
+ counter.unique_events(event_names: test_event_name_1,
start_date: start_time,
end_date: end_time)
}
@@ -85,25 +95,92 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) }
let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user2) }
- it 'tracks all pipelines using HLLRedisCounter by multiple users' do
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ it 'tracks all pipelines using HLLRedisCounter by multiple users for test reports' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name_1, values: user1.id)
+ .and_call_original
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name_1, values: user2.id)
+ .and_call_original
+
+ expect do
+ described_class.new.execute(pipeline1)
+ described_class.new.execute(pipeline2)
+ end
+ .to change {
+ counter.unique_events(event_names: test_event_name_1,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 2
+ end
+ end
+
+ context 'when pipeline has coverage test reports' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user1) }
+
+ before do
+ 2.times do
+ pipeline.builds << build(:ci_build, :coverage_reports, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
+ it 'tracks the coverage test event using HLLRedisCounter' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_2, values: user1.id)
.and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ expect { track_artifact_report }
+ .to change {
+ counter.unique_events(event_names: test_event_name_2,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 1
+ end
+ end
+
+ context 'when a single user started multiple pipelines with coverage reports' do
+ let_it_be(:pipeline1) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
+
+ it 'tracks all pipelines using HLLRedisCounter by one user_id for the coverage test event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user1.id)
+ .with(test_event_name_2, values: user1.id)
+ .twice
.and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
- .to receive(:track_event)
- .with(test_event_name, values: user2.id)
- .and_call_original
+ expect do
+ described_class.new.execute(pipeline1)
+ described_class.new.execute(pipeline2)
+ end
+ .to change {
+ counter.unique_events(event_names: test_event_name_2,
+ start_date: start_time,
+ end_date: end_time)
+ }
+ .by 1
+ end
+ end
+
+ context 'when multiple users started multiple pipelines with coverage test reports' do
+ let_it_be(:pipeline1) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user1) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, :with_coverage_reports, project: project, user: user2) }
+
+ it 'tracks all pipelines using HLLRedisCounter by multiple users for coverage test reports' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(test_event_name_2, values: user1.id)
+ .and_call_original
- allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event)
- .with(test_event_name, values: user2.id)
+ .with(test_event_name_2, values: user2.id)
.and_call_original
expect do
@@ -111,7 +188,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do
described_class.new.execute(pipeline2)
end
.to change {
- counter.unique_events(event_names: test_event_name,
+ counter.unique_events(event_names: test_event_name_2,
start_date: start_time,
end_date: end_time)
}
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
index 7578afa7c50..d0aa1ba4c6c 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
@@ -104,7 +104,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection
describe '#processing_processables' do
it 'returns processables marked as processing' do
- expect(collection.processing_processables.map { |processable| processable[:id] } )
+ expect(collection.processing_processables.map { |processable| processable[:id] })
.to contain_exactly(build_a.id, build_b.id, test_a.id, test_b.id, deploy.id)
end
end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index 06bb6d39fe5..1fbefc1fa22 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
check_expectation(test_file.dig('init', 'expect'), "init")
test_file['transitions'].each_with_index do |transition, idx|
- event_on_jobs(transition['event'], transition['jobs'])
+ process_events(transition)
Sidekiq::Worker.drain_all # ensure that all async jobs are executed
check_expectation(transition['expect'], "transition:#{idx}")
end
@@ -48,20 +48,37 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService do
}
end
+ def process_events(transition)
+ if transition['jobs']
+ event_on_jobs(transition['event'], transition['jobs'])
+ else
+ event_on_pipeline(transition['event'])
+ end
+ end
+
def event_on_jobs(event, job_names)
statuses = pipeline.latest_statuses.by_name(job_names).to_a
expect(statuses.count).to eq(job_names.count) # ensure that we have the same counts
statuses.each do |status|
- if event == 'play'
+ case event
+ when 'play'
status.play(user)
- elsif event == 'retry'
+ when 'retry'
::Ci::RetryJobService.new(project, user).execute(status)
else
status.public_send("#{event}!")
end
end
end
+
+ def event_on_pipeline(event)
+ if event == 'retry'
+ pipeline.retry_failed(user)
+ else
+ pipeline.public_send("#{event}!")
+ end
+ end
end
end
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml
new file mode 100644
index 00000000000..a50fe56f8d4
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_manual_build.yml
@@ -0,0 +1,54 @@
+config:
+ test1:
+ script: exit 0
+
+ test2:
+ when: manual
+ script: exit 1
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ test1: pending
+ test2: manual
+
+transitions:
+ - event: success
+ jobs: [test1]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: manual
+ - event: play
+ jobs: [test2]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ test1: success
+ test2: pending
+ - event: drop
+ jobs: [test2]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: failed
+ - event: retry
+ jobs: [test2]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ test1: success
+ test2: pending
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml
new file mode 100644
index 00000000000..a6112a95a12
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_one_test_succeeds_one_manual_test_fails_and_retry_pipeline.yml
@@ -0,0 +1,53 @@
+config:
+ test1:
+ script: exit 0
+
+ test2:
+ when: manual
+ script: exit 1
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ jobs:
+ test1: pending
+ test2: manual
+
+transitions:
+ - event: success
+ jobs: [test1]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: manual
+ - event: play
+ jobs: [test2]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ jobs:
+ test1: success
+ test2: pending
+ - event: drop
+ jobs: [test2]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: failed
+ - event: retry
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ jobs:
+ test1: success
+ test2: manual
diff --git a/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb b/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb
new file mode 100644
index 00000000000..9a3aad20d89
--- /dev/null
+++ b/spec/services/ci/pipeline_schedules/take_ownership_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::PipelineSchedules::TakeOwnershipService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: owner) }
+
+ before_all do
+ project.add_maintainer(user)
+ project.add_maintainer(owner)
+ project.add_reporter(reporter)
+ end
+
+ describe '#execute' do
+ context 'when user does not have permission' do
+ subject(:service) { described_class.new(pipeline_schedule, reporter) }
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(_('Failed to change the owner'))
+ end
+ end
+
+ context 'when user has permission' do
+ subject(:service) { described_class.new(pipeline_schedule, user) }
+
+ it 'returns ServiceResponse.success' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ expect(result.payload).to eq(pipeline_schedule)
+ end
+
+ context 'when schedule update fails' do
+ subject(:service) { described_class.new(pipeline_schedule, owner) }
+
+ before do
+ allow(pipeline_schedule).to receive(:update).and_return(false)
+
+ errors = ActiveModel::Errors.new(pipeline_schedule)
+ errors.add(:base, 'An error occurred')
+ allow(pipeline_schedule).to receive(:errors).and_return(errors)
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = service.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq(['An error occurred'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index 85ef8b60af4..fc07801b672 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -87,11 +87,15 @@ RSpec.describe Ci::PlayBuildService, '#execute' do
expect(build.reload.job_variables.map(&:key)).to contain_exactly('first', 'second')
end
- context 'when variables are invalid' do
+ context 'and variables are invalid' do
let(:job_variables) { [{}] }
- it 'raises an error' do
- expect { subject }.to raise_error(ActiveRecord::RecordInvalid)
+ it 'resets the attributes of the build' do
+ build.update!(job_variables_attributes: [{ key: 'old', value: 'old variable' }])
+
+ subject
+
+ expect(build.job_variables.map(&:key)).to contain_exactly('old')
end
end
diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb
index 2fcb4ce73ff..9301098b083 100644
--- a/spec/services/ci/process_build_service_spec.rb
+++ b/spec/services/ci/process_build_service_spec.rb
@@ -2,147 +2,118 @@
require 'spec_helper'
RSpec.describe Ci::ProcessBuildService, '#execute' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ using RSpec::Parameterized::TableSyntax
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, ref: 'master', project: project) }
subject { described_class.new(project, user).execute(build, current_status) }
- before do
+ before_all do
project.add_maintainer(user)
end
- context 'when build has on_success option' do
- let(:build) { create(:ci_build, :created, when: :on_success, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
- end
-
- context 'when current status is skipped' do
- let(:current_status) { 'skipped' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
+ shared_context 'with enqueue_immediately set' do
+ before do
+ build.set_enqueue_immediately!
end
end
- context 'when build has on_failure option' do
- let(:build) { create(:ci_build, :created, when: :on_failure, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('skipped')
- end
- end
-
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
+ shared_context 'with ci_retry_job_fix disabled' do
+ before do
+ stub_feature_flags(ci_retry_job_fix: false)
end
end
- context 'when build has always option' do
- let(:build) { create(:ci_build, :created, when: :always, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
-
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('pending')
- end
+ context 'for single build' do
+ let!(:build) { create(:ci_build, *[trait].compact, :created, **conditions, pipeline: pipeline) }
+
+ where(:trait, :conditions, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
+ nil | { when: :on_success } | 'success' | 'pending' | 'pending' | 'pending'
+ nil | { when: :on_success } | 'skipped' | 'pending' | 'pending' | 'pending'
+ nil | { when: :on_success } | 'failed' | 'skipped' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'success' | 'skipped' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ nil | { when: :on_failure } | 'failed' | 'pending' | 'pending' | 'pending'
+ nil | { when: :always } | 'success' | 'pending' | 'pending' | 'pending'
+ nil | { when: :always } | 'skipped' | 'pending' | 'pending' | 'pending'
+ nil | { when: :always } | 'failed' | 'pending' | 'pending' | 'pending'
+ :actionable | { when: :manual } | 'success' | 'manual' | 'pending' | 'manual'
+ :actionable | { when: :manual } | 'skipped' | 'manual' | 'pending' | 'manual'
+ :actionable | { when: :manual } | 'failed' | 'skipped' | 'skipped' | 'skipped'
+ :schedulable | { when: :delayed } | 'success' | 'scheduled' | 'pending' | 'scheduled'
+ :schedulable | { when: :delayed } | 'skipped' | 'scheduled' | 'pending' | 'scheduled'
+ :schedulable | { when: :delayed } | 'failed' | 'skipped' | 'skipped' | 'skipped'
end
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('pending')
+ with_them do
+ it 'updates the job status to after_status' do
+ expect { subject }.to change { build.status }.to(after_status)
end
- end
- end
-
- context 'when build has manual option' do
- let(:build) { create(:ci_build, :created, :actionable, user: user, project: project) }
- context 'when current status is success' do
- let(:current_status) { 'success' }
+ context 'when build is set to enqueue immediately' do
+ include_context 'with enqueue_immediately set'
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('manual')
- end
- end
+ it 'updates the job status to retry_after_status' do
+ expect { subject }.to change { build.status }.to(retry_after_status)
+ end
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
+ context 'when feature flag ci_retry_job_fix is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
+ it "updates the job status to retry_disabled_after_status" do
+ expect { subject }.to change { build.status }.to(retry_disabled_after_status)
+ end
+ end
end
end
end
- context 'when build has delayed option' do
- before do
- allow(Ci::BuildScheduleWorker).to receive(:perform_at) {}
+ context 'when build is scheduled with DAG' do
+ let!(:build) do
+ create(
+ :ci_build,
+ *[trait].compact,
+ :dependent,
+ :created,
+ when: build_when,
+ pipeline: pipeline,
+ needed: other_build
+ )
end
- let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
-
- context 'when current status is success' do
- let(:current_status) { 'success' }
+ let!(:other_build) { create(:ci_build, :created, when: :on_success, pipeline: pipeline) }
- it 'changes the build status' do
- expect { subject }.to change { build.status }.to('scheduled')
- end
+ where(:trait, :build_when, :current_status, :after_status, :retry_after_status, :retry_disabled_after_status) do
+ nil | :on_success | 'success' | 'pending' | 'pending' | 'pending'
+ nil | :on_success | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ nil | :manual | 'success' | 'manual' | 'pending' | 'manual'
+ nil | :manual | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ nil | :delayed | 'success' | 'manual' | 'pending' | 'manual'
+ nil | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
+ :schedulable | :delayed | 'success' | 'scheduled' | 'pending' | 'scheduled'
+ :schedulable | :delayed | 'skipped' | 'skipped' | 'skipped' | 'skipped'
end
- context 'when current status is failed' do
- let(:current_status) { 'failed' }
-
- it 'does not change the build status' do
- expect { subject }.to change { build.status }.to('skipped')
+ with_them do
+ it 'updates the job status to after_status' do
+ expect { subject }.to change { build.status }.to(after_status)
end
- end
- end
- context 'when build is scheduled with DAG' do
- using RSpec::Parameterized::TableSyntax
+ context 'when build is set to enqueue immediately' do
+ include_context 'with enqueue_immediately set'
- let(:pipeline) { create(:ci_pipeline, ref: 'master', project: project) }
- let!(:build) { create(:ci_build, :created, when: build_when, pipeline: pipeline, scheduling_type: :dag) }
- let!(:other_build) { create(:ci_build, :created, when: :on_success, pipeline: pipeline) }
- let!(:build_on_other_build) { create(:ci_build_need, build: build, name: other_build.name) }
-
- where(:build_when, :current_status, :after_status) do
- :on_success | 'success' | 'pending'
- :on_success | 'skipped' | 'skipped'
- :manual | 'success' | 'manual'
- :manual | 'skipped' | 'skipped'
- :delayed | 'success' | 'manual'
- :delayed | 'skipped' | 'skipped'
- end
+ it 'updates the job status to retry_after_status' do
+ expect { subject }.to change { build.status }.to(retry_after_status)
+ end
- with_them do
- it 'proceeds the build' do
- expect { subject }.to change { build.status }.to(after_status)
+ context 'when feature flag ci_retry_job_fix is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it "updates the job status to retry_disabled_after_status" do
+ expect { subject }.to change { build.status }.to(retry_disabled_after_status)
+ end
+ end
end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index e2e760b9812..f40f5cc5a62 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -14,25 +14,29 @@ module Ci
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 }
+ subject { described_class.new(shared_runner).execute }
+ context 'checks database loadbalancing stickiness' do
before do
project.update!(shared_runners_enabled: false)
end
- it 'result is valid if replica did caught-up' do
+ it 'result is valid if replica did caught-up', :aggregate_failures do
expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
.with(:runner, shared_runner.id) { true }
expect(subject).to be_valid
+ expect(subject.build).to be_nil
+ expect(subject.build_json).to be_nil
end
- it 'result is invalid if replica did not caught-up' do
+ it 'result is invalid if replica did not caught-up', :aggregate_failures do
expect(ApplicationRecord.sticking).to receive(:all_caught_up?)
.with(:runner, shared_runner.id) { false }
expect(subject).not_to be_valid
+ expect(subject.build).to be_nil
+ expect(subject.build_json).to be_nil
end
end
@@ -954,6 +958,7 @@ module Ci
expect(result).not_to be_valid
expect(result.build).to be_nil
+ expect(result.build_json).to be_nil
end
end
diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb
index 69f19c5acf2..540e700efa6 100644
--- a/spec/services/ci/retry_job_service_spec.rb
+++ b/spec/services/ci/retry_job_service_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::RetryJobService do
+ using RSpec::Parameterized::TableSyntax
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
@@ -11,11 +12,11 @@ RSpec.describe Ci::RetryJobService do
end
let_it_be(:stage) do
- create(:ci_stage, project: project,
- pipeline: pipeline,
- name: 'test')
+ create(:ci_stage, pipeline: pipeline, name: 'test')
end
+ let_it_be(:deploy_stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy', position: stage.position + 1) }
+
let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] }
let(:user) { developer }
@@ -26,24 +27,11 @@ RSpec.describe Ci::RetryJobService do
project.add_reporter(reporter)
end
- shared_context 'retryable bridge' do
- let_it_be(:downstream_project) { create(:project, :repository) }
-
- let_it_be_with_refind(:job) do
- create(:ci_bridge, :success,
- pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage
- )
- end
-
- let_it_be(:job_to_clone) { job }
-
- before do
- job.update!(retried: false)
+ shared_context 'retryable build' do
+ let_it_be_with_reload(:job) do
+ create(:ci_build, :success, pipeline: pipeline, ci_stage: stage)
end
- end
- shared_context 'retryable build' do
- let_it_be_with_refind(:job) { create(:ci_build, :success, pipeline: pipeline, ci_stage: stage) }
let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
let_it_be(:job_to_clone) do
@@ -60,6 +48,12 @@ RSpec.describe Ci::RetryJobService do
end
end
+ shared_context 'with ci_retry_job_fix disabled' do
+ before do
+ stub_feature_flags(ci_retry_job_fix: false)
+ end
+ end
+
shared_examples_for 'clones the job' do
let(:job) { job_to_clone }
@@ -87,8 +81,7 @@ RSpec.describe Ci::RetryJobService do
context 'when the job has needs' do
before do
- create(:ci_build_need, build: job, name: 'build1')
- create(:ci_build_need, build: job, name: 'build2')
+ create_list(:ci_build_need, 2, build: job)
end
it 'bulk inserts all the needs' do
@@ -123,16 +116,12 @@ RSpec.describe Ci::RetryJobService do
end
context 'when there are subsequent processables that are skipped' do
- let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
-
let!(:subsequent_build) do
- create(:ci_build, :skipped, stage_idx: 2,
- pipeline: pipeline,
- ci_stage: stage)
+ create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:subsequent_bridge) do
- create(:ci_bridge, :skipped, stage_idx: 2, pipeline: pipeline, ci_stage: stage)
+ create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
it 'resumes pipeline processing in the subsequent stage' do
@@ -152,10 +141,9 @@ RSpec.describe Ci::RetryJobService do
end
context 'when the pipeline has other jobs' do
- let!(:stage2) { create(:ci_stage, project: project, pipeline: pipeline, name: 'deploy') }
- let!(:build2) { create(:ci_build, pipeline: pipeline, ci_stage: stage ) }
- let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: stage2) }
- let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: build2.name) }
+ let!(:other_test_build) { create(:ci_build, pipeline: pipeline, ci_stage: stage) }
+ let!(:deploy) { create(:ci_build, pipeline: pipeline, ci_stage: deploy_stage) }
+ let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: other_test_build.name) }
context 'when job has a nil scheduling_type' do
before do
@@ -166,7 +154,7 @@ RSpec.describe Ci::RetryJobService do
it 'populates scheduling_type of processables' do
expect(new_job.scheduling_type).to eq('stage')
expect(job.reload.scheduling_type).to eq('stage')
- expect(build2.reload.scheduling_type).to eq('stage')
+ expect(other_test_build.reload.scheduling_type).to eq('stage')
expect(deploy.reload.scheduling_type).to eq('dag')
end
end
@@ -193,6 +181,13 @@ RSpec.describe Ci::RetryJobService do
end
end
+ shared_examples_for 'checks enqueue_immediately?' do
+ it "returns enqueue_immediately" do
+ subject
+ expect(new_job.enqueue_immediately?).to eq enqueue_immediately
+ end
+ end
+
describe '#clone!' do
let(:new_job) { service.clone!(job) }
@@ -200,20 +195,6 @@ RSpec.describe Ci::RetryJobService do
expect { service.clone!(create(:ci_build).present) }.to raise_error(TypeError)
end
- context 'when the job to be cloned is a bridge' do
- include_context 'retryable bridge'
-
- it_behaves_like 'clones the job'
-
- context 'when given variables' do
- let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
-
- it 'does not give variables to the new bridge' do
- expect { new_job }.not_to raise_error
- end
- end
- end
-
context 'when the job to be cloned is a build' do
include_context 'retryable build'
@@ -224,7 +205,7 @@ RSpec.describe Ci::RetryJobService do
context 'when a build with a deployment is retried' do
let!(:job) do
create(:ci_build, :with_deployment, :deploy_to_production,
- pipeline: pipeline, ci_stage: stage, project: project)
+ pipeline: pipeline, ci_stage: stage)
end
it 'creates a new deployment' do
@@ -247,7 +228,6 @@ RSpec.describe Ci::RetryJobService do
options: { environment: { name: environment_name } },
pipeline: pipeline,
ci_stage: stage,
- project: project,
user: other_developer)
end
@@ -282,24 +262,44 @@ RSpec.describe Ci::RetryJobService do
end
end
end
- end
- describe '#execute' do
- let(:new_job) { service.execute(job)[:job] }
+ context 'when enqueue_if_actionable is provided' do
+ let!(:job) do
+ create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
+ end
- context 'when the job to be retried is a bridge' do
- include_context 'retryable bridge'
+ let(:new_job) { subject }
- it_behaves_like 'retries the job'
+ subject { service.clone!(job, enqueue_if_actionable: enqueue_if_actionable) }
- context 'when given variables' do
- let(:new_job) { service.clone!(job, variables: job_variables_attributes) }
+ where(:enqueue_if_actionable, :trait, :enqueue_immediately) do
+ true | nil | false
+ true | :manual | true
+ true | :expired_scheduled | true
+
+ false | nil | false
+ false | :manual | false
+ false | :expired_scheduled | false
+ end
+
+ with_them do
+ it_behaves_like 'checks enqueue_immediately?'
- it 'does not give variables to the new bridge' do
- expect { new_job }.not_to raise_error
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it_behaves_like 'checks enqueue_immediately?' do
+ let(:enqueue_immediately) { false }
+ end
end
end
end
+ end
+
+ describe '#execute' do
+ let(:new_job) { subject[:job] }
+
+ subject { service.execute(job) }
context 'when the job to be retried is a build' do
include_context 'retryable build'
@@ -307,24 +307,18 @@ RSpec.describe Ci::RetryJobService do
it_behaves_like 'retries the job'
context 'when there are subsequent jobs that are skipped' do
- let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, name: 'deploy') }
-
let!(:subsequent_build) do
- create(:ci_build, :skipped, stage_idx: 2,
- pipeline: pipeline,
- stage_id: stage.id)
+ create(:ci_build, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
let!(:subsequent_bridge) do
- create(:ci_bridge, :skipped, stage_idx: 2,
- pipeline: pipeline,
- stage_id: stage.id)
+ create(:ci_bridge, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
end
it 'does not cause an N+1 when updating the job ownership' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { service.execute(job) }.count
- create_list(:ci_build, 2, :skipped, stage_idx: job.stage_idx + 1, pipeline: pipeline, stage_id: stage.id)
+ create_list(:ci_build, 2, :skipped, pipeline: pipeline, ci_stage: deploy_stage)
expect { service.execute(job) }.not_to exceed_all_query_limit(control_count)
end
@@ -352,5 +346,161 @@ RSpec.describe Ci::RetryJobService do
end
end
end
+
+ context 'when job being retried has jobs in previous stages' do
+ let!(:job) do
+ create(
+ :ci_build,
+ :failed,
+ name: 'deploy_a',
+ pipeline: pipeline,
+ ci_stage: deploy_stage
+ )
+ end
+
+ before do
+ create(
+ :ci_build,
+ previous_stage_job_status,
+ name: 'test_a',
+ pipeline: pipeline,
+ ci_stage: stage
+ )
+ end
+
+ where(:previous_stage_job_status, :after_status) do
+ :created | 'created'
+ :pending | 'created'
+ :running | 'created'
+ :manual | 'created'
+ :scheduled | 'created'
+ :success | 'pending'
+ :failed | 'skipped'
+ :skipped | 'pending'
+ end
+
+ with_them do
+ it 'updates the new job status to after_status' do
+ expect(subject).to be_success
+ expect(new_job.status).to eq after_status
+ end
+
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it 'enqueues the new job' do
+ expect(subject).to be_success
+ expect(new_job).to be_pending
+ end
+ end
+ end
+ end
+
+ context 'when job being retried has DAG dependencies' do
+ let!(:job) do
+ create(
+ :ci_build,
+ :failed,
+ :dependent,
+ name: 'deploy_a',
+ pipeline: pipeline,
+ ci_stage: deploy_stage,
+ needed: dependency
+ )
+ end
+
+ let(:dependency) do
+ create(
+ :ci_build,
+ dag_dependency_status,
+ name: 'test_a',
+ pipeline: pipeline,
+ ci_stage: stage
+ )
+ end
+
+ where(:dag_dependency_status, :after_status) do
+ :created | 'created'
+ :pending | 'created'
+ :running | 'created'
+ :manual | 'created'
+ :scheduled | 'created'
+ :success | 'pending'
+ :failed | 'skipped'
+ :skipped | 'skipped'
+ end
+
+ with_them do
+ it 'updates the new job status to after_status' do
+ expect(subject).to be_success
+ expect(new_job.status).to eq after_status
+ end
+
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it 'enqueues the new job' do
+ expect(subject).to be_success
+ expect(new_job).to be_pending
+ end
+ end
+ end
+ end
+
+ context 'when there are other manual/scheduled jobs' do
+ let_it_be(:test_manual_build) do
+ create(:ci_build, :manual, pipeline: pipeline, ci_stage: stage)
+ end
+
+ let_it_be(:subsequent_manual_build) do
+ create(:ci_build, :manual, pipeline: pipeline, ci_stage: deploy_stage)
+ end
+
+ let_it_be(:test_scheduled_build) do
+ create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: stage)
+ end
+
+ let_it_be(:subsequent_scheduled_build) do
+ create(:ci_build, :scheduled, pipeline: pipeline, ci_stage: deploy_stage)
+ end
+
+ let!(:job) do
+ create(:ci_build, *[trait].compact, :failed, pipeline: pipeline, ci_stage: stage)
+ end
+
+ where(:trait, :enqueue_immediately) do
+ nil | false
+ :manual | true
+ :expired_scheduled | true
+ end
+
+ with_them do
+ it 'retries the given job but not the other manual/scheduled jobs' do
+ expect { subject }
+ .to change { Ci::Build.count }.by(1)
+ .and not_change { test_manual_build.reload.status }
+ .and not_change { subsequent_manual_build.reload.status }
+ .and not_change { test_scheduled_build.reload.status }
+ .and not_change { subsequent_scheduled_build.reload.status }
+
+ expect(new_job).to be_pending
+ end
+
+ it_behaves_like 'checks enqueue_immediately?'
+
+ context 'when feature flag is disabled' do
+ include_context 'with ci_retry_job_fix disabled'
+
+ it 'enqueues the new job' do
+ expect(subject).to be_success
+ expect(new_job).to be_pending
+ end
+
+ it_behaves_like 'checks enqueue_immediately?' do
+ let(:enqueue_immediately) { false }
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 96437290ae3..77345096537 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -5,14 +5,16 @@ require 'spec_helper'
RSpec.describe Ci::RetryPipelineService, '#execute' do
include ProjectForksHelper
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let_it_be_with_refind(:user) { create(:user) }
+ let_it_be_with_refind(:project) { create(:project) }
+
let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:service) { described_class.new(project, user) }
let(:build_stage) { create(:ci_stage, name: 'build', position: 0, pipeline: pipeline) }
let(:test_stage) { create(:ci_stage, name: 'test', position: 1, pipeline: pipeline) }
let(:deploy_stage) { create(:ci_stage, name: 'deploy', position: 2, pipeline: pipeline) }
+ subject(:service) { described_class.new(project, user) }
+
context 'when user has full ability to modify pipeline' do
before do
project.add_developer(user)
@@ -272,6 +274,21 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
expect(pipeline.reload).to be_running
end
end
+
+ context 'when there is a failed manual action' do
+ before do
+ create_build('rspec', :success, build_stage)
+ create_build('manual-rspec', :failed, build_stage, when: :manual, allow_failure: true)
+ end
+
+ it 'processes the manual action' do
+ service.execute(pipeline)
+
+ expect(build('rspec')).to be_success
+ expect(build('manual-rspec')).to be_manual
+ expect(pipeline.reload).to be_success
+ end
+ end
end
it 'closes all todos about failed jobs for pipeline' do
diff --git a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
index 8e9fc4e3012..fa8af1100df 100644
--- a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
+++ b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb
@@ -5,78 +5,180 @@ require 'spec_helper'
RSpec.describe ::Ci::Runners::BulkDeleteRunnersService, '#execute' do
subject(:execute) { described_class.new(**service_args).execute }
- let(:service_args) { { runners: runners_arg } }
+ let_it_be(:admin_user) { create(:user, :admin) }
+ let_it_be_with_refind(:owner_user) { create(:user) } # discard memoized ci_owned_runners
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:user) {}
+ let(:service_args) { { runners: runners_arg, current_user: user } }
let(:runners_arg) {}
context 'with runners specified' do
let!(:instance_runner) { create(:ci_runner) }
- let!(:group_runner) { create(:ci_runner, :group) }
- let!(:project_runner) { create(:ci_runner, :project) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
shared_examples 'a service deleting runners in bulk' do
+ let!(:expected_deleted_ids) { expected_deleted_runners.map(&:id) }
+
it 'destroys runners', :aggregate_failures do
- expect { subject }.to change { Ci::Runner.count }.by(-2)
+ expect { execute }.to change { Ci::Runner.count }.by(-expected_deleted_ids.count)
is_expected.to be_success
- expect(execute.payload).to eq({ deleted_count: 2, deleted_ids: [instance_runner.id, project_runner.id] })
- expect(instance_runner[:errors]).to be_nil
- expect(project_runner[:errors]).to be_nil
+ expect(execute.payload).to eq(
+ {
+ deleted_count: expected_deleted_ids.count,
+ deleted_ids: expected_deleted_ids,
+ errors: []
+ })
expect { project_runner.runner_projects.first.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect { group_runner.reload }.not_to raise_error
- expect { instance_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expected_deleted_runners.each do |deleted_runner|
+ expect(deleted_runner[:errors]).to be_nil
+ expect { deleted_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
- context 'with some runners already deleted' do
+ context 'with too many runners specified' do
before do
- instance_runner.destroy!
+ stub_const("#{described_class}::RUNNER_LIMIT", 1)
end
- let(:runners_arg) { [instance_runner.id, project_runner.id] }
-
- it 'destroys runners and returns only deleted runners', :aggregate_failures do
- expect { subject }.to change { Ci::Runner.count }.by(-1)
+ it 'deletes only first RUNNER_LIMIT runners', :aggregate_failures do
+ expect { execute }.to change { Ci::Runner.count }.by(-1)
is_expected.to be_success
- expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [project_runner.id] })
- expect(instance_runner[:errors]).to be_nil
- expect(project_runner[:errors]).to be_nil
- expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(execute.payload).to eq(
+ {
+ deleted_count: 1,
+ deleted_ids: expected_deleted_ids.take(1),
+ errors: ["Can only delete up to 1 runners per call. Ignored the remaining runner(s)."]
+ })
end
end
+ end
- context 'with too many runners specified' do
+ context 'when the user cannot delete runners' do
+ let(:runners_arg) { Ci::Runner.all }
+
+ context 'when user is not group owner' do
before do
- stub_const("#{described_class}::RUNNER_LIMIT", 1)
+ group.add_developer(user)
end
- it 'deletes only first RUNNER_LIMIT runners' do
- expect { subject }.to change { Ci::Runner.count }.by(-1)
+ let(:user) { create(:user) }
- is_expected.to be_success
- expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [instance_runner.id] })
+ it 'does not delete any runner and returns error', :aggregate_failures do
+ expect { execute }.not_to change { Ci::Runner.count }
+ expect(execute[:errors]).to match_array("User does not have permission to delete any of the runners")
end
end
- end
- context 'with runners specified as relation' do
- let(:runners_arg) { Ci::Runner.not_group_type }
+ context 'when user is not part of the group' do
+ let(:user) { create(:user) }
- include_examples 'a service deleting runners in bulk'
+ it 'does not delete any runner and returns error', :aggregate_failures do
+ expect { execute }.not_to change { Ci::Runner.count }
+ expect(execute[:errors]).to match_array("User does not have permission to delete any of the runners")
+ end
+ end
end
- context 'with runners specified as array of IDs' do
- let(:runners_arg) { Ci::Runner.not_group_type.ids }
+ context 'when the user can delete runners' do
+ context 'when user is an admin', :enable_admin_mode do
+ include_examples 'a service deleting runners in bulk' do
+ let(:runners_arg) { Ci::Runner.all }
+ let!(:expected_deleted_runners) { [instance_runner, group_runner, project_runner] }
+ let(:user) { admin_user }
+ end
+
+ context 'with a runner already deleted' do
+ before do
+ group_runner.destroy!
+ end
+
+ include_examples 'a service deleting runners in bulk' do
+ let(:runners_arg) { Ci::Runner.all }
+ let!(:expected_deleted_runners) { [instance_runner, project_runner] }
+ let(:user) { admin_user }
+ end
+ end
+
+ context 'when deleting a single runner' do
+ let(:runners_arg) { Ci::Runner.all }
+
+ 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
+ execute
+ end
+
+ additional_runners = 1
+
+ create_list(:ci_runner, 1 + additional_runners, :instance)
+ create_list(:ci_runner, 1 + additional_runners, :group, groups: [group])
+ create_list(:ci_runner, 1 + additional_runners, :project, projects: [project])
+
+ service = described_class.new(runners: runners_arg, current_user: user)
+
+ # Base cost per runner is:
+ # - 1 `SELECT * FROM "taggings"` query
+ # - 1 `SAVEPOINT` query
+ # - 1 `DELETE FROM "ci_runners"` query
+ # - 1 `RELEASE SAVEPOINT` query
+ # Project runners have an additional query:
+ # - 1 `DELETE FROM "ci_runner_projects"` query, given the call to `destroy_all`
+ instance_runner_cost = 4
+ group_runner_cost = 4
+ project_runner_cost = 5
+ expect { service.execute }
+ .not_to exceed_all_query_limit(control_count)
+ .with_threshold(additional_runners * (instance_runner_cost + group_runner_cost + project_runner_cost))
+ end
+ end
+ end
- include_examples 'a service deleting runners in bulk'
+ context 'when user is group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ include_examples 'a service deleting runners in bulk' do
+ let(:runners_arg) { Ci::Runner.not_instance_type }
+ let!(:expected_deleted_runners) { [group_runner, project_runner] }
+ let(:user) { owner_user }
+ end
+
+ context 'with a runner non-authorised to be deleted' do
+ let(:runners_arg) { Ci::Runner.all }
+ let!(:expected_deleted_runners) { [project_runner] }
+ let(:user) { owner_user }
+
+ it 'destroys only authorised runners', :aggregate_failures do
+ allow(Ability).to receive(:allowed?).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :delete_runner, instance_runner).and_return(false)
+
+ expect { execute }.to change { Ci::Runner.count }.by(-2)
+
+ is_expected.to be_success
+ expect(execute.payload).to eq(
+ {
+ deleted_count: 2,
+ deleted_ids: [group_runner.id, project_runner.id],
+ errors: ["User does not have permission to delete runner(s) ##{instance_runner.id}"]
+ })
+ end
+ end
+ end
end
context 'with no arguments specified' do
let(:runners_arg) { nil }
+ let(:user) { owner_user }
it 'returns 0 deleted runners' do
is_expected.to be_success
- expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [] })
+ expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [], errors: [] })
end
end
end
diff --git a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
index 1f44612947b..e5cba80d567 100644
--- a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
+++ b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
@@ -67,6 +67,19 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute' do
expect(runner.projects.ids).to match_array([owner_project.id] + project_ids)
end
end
+
+ context 'when disassociating all projects' do
+ let(:project_ids) { [] }
+
+ it 'reassigns associated projects and returns success response' do
+ expect(execute).to be_success
+
+ runner.reload
+
+ expect(runner.owner_project).to eq(owner_project)
+ expect(runner.projects.ids).to contain_exactly(owner_project.id)
+ end
+ end
end
context 'with failing assign_to requests' do
diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
deleted file mode 100644
index 605d9e67ab6..00000000000
--- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckIngressIpAddressService do
- include ExclusiveLeaseHelpers
-
- let(:application) { create(:clusters_applications_ingress, :installed) }
- let(:service) { described_class.new(application) }
- let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
- let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" }
-
- let(:ingress) do
- [
- {
- ip: '111.222.111.222',
- hostname: 'localhost.localdomain'
- }
- ]
- end
-
- let(:kube_service) do
- ::Kubeclient::Resource.new(
- {
- status: {
- loadBalancer: {
- ingress: ingress
- }
- }
- }
- )
- end
-
- subject { service.execute }
-
- before do
- stub_exclusive_lease(lease_key, timeout: 15.seconds.to_i)
- allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
- end
-
- include_examples 'check ingress ip executions', :clusters_applications_ingress
-
- include_examples 'check ingress ip executions', :clusters_applications_knative
-end
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
deleted file mode 100644
index 698804ff6af..00000000000
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ /dev/null
@@ -1,204 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
- RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
-
- let(:application) { create(:clusters_applications_helm, :installing) }
- let(:service) { described_class.new(application) }
- let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
- let(:errors) { nil }
-
- shared_examples 'a not yet terminated installation' do |a_phase|
- let(:phase) { a_phase }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- context "when phase is #{a_phase}" do
- context 'when not timed_out' do
- it 'reschedule a new check' do
- expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
- expect(service).not_to receive(:remove_installation_pod)
-
- expect do
- service.execute
-
- application.reload
- end.not_to change(application, :status)
-
- expect(application.status_reason).to be_nil
- end
- end
- end
- end
-
- shared_examples 'error handling' do
- context 'when installation raises a Kubeclient::HttpError' do
- let(:cluster) { create(:cluster, :provided_by_user, :project) }
- let(:logger) { service.send(:logger) }
- let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
-
- before do
- application.update!(cluster: cluster)
-
- expect(service).to receive(:pod_phase).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'Unauthorized' }
- let(:error_code) { 401 }
- end
-
- it 'shows the response code from the error' do
- service.execute
-
- expect(application).to be_errored.or(be_update_errored)
- expect(application.status_reason).to eq('Kubernetes error: 401')
- end
- end
- end
-
- before do
- allow(service).to receive(:installation_errors).and_return(errors)
- allow(service).to receive(:remove_installation_pod).and_return(nil)
- end
-
- context 'when application is updating' do
- let(:application) { create(:clusters_applications_helm, :updating) }
-
- include_examples 'error handling'
-
- RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
-
- context 'when installation POD succeeded' do
- let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'removes the installation POD' do
- expect(service).to receive(:remove_installation_pod).once
-
- service.execute
- end
-
- it 'make the application installed' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_updated
- expect(application.status_reason).to be_nil
- end
- end
-
- context 'when installation POD failed' do
- let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.')
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timed_out, :updating) }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.')
- end
- end
- end
-
- context 'when application is installing' do
- include_examples 'error handling'
-
- RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
-
- context 'when installation POD succeeded' do
- let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'removes the installation POD' do
- expect_next_instance_of(Gitlab::Kubernetes::Helm::API) do |instance|
- expect(instance).to receive(:delete_pod!).with(kind_of(String)).once
- end
- expect(service).to receive(:remove_installation_pod).and_call_original
-
- service.execute
- end
-
- it 'make the application installed' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_installed
- expect(application.status_reason).to be_nil
- end
-
- it 'tracks application install', :snowplow do
- service.execute
-
- expect_snowplow_event(category: 'cluster:applications', action: 'cluster_application_helm_installed')
- end
- end
-
- context 'when installation POD failed' do
- let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_errored
- expect(application.status_reason).to eq('Operation failed. Check pod logs for install-helm for more details.')
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timed_out) }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- expect(ClusterWaitForAppInstallationWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_errored
- expect(application.status_reason).to eq('Operation timed out. Check pod logs for install-helm for more details.')
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
deleted file mode 100644
index 4b8893429cf..00000000000
--- a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
+++ /dev/null
@@ -1,155 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckUninstallProgressService do
- reschedule_phases = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
-
- let(:application) { create(:clusters_applications_prometheus, :uninstalling) }
- let(:service) { described_class.new(application) }
- let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
- let(:errors) { nil }
- let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
-
- before do
- allow(service).to receive(:installation_errors).and_return(errors)
- allow(service).to receive(:remove_installation_pod)
- end
-
- shared_examples 'a not yet terminated installation' do |a_phase|
- let(:phase) { a_phase }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- context "when phase is #{a_phase}" do
- context 'when not timed_out' do
- it 'reschedule a new check' do
- expect(worker_class).to receive(:perform_in).once
- expect(service).not_to receive(:remove_installation_pod)
-
- expect do
- service.execute
-
- application.reload
- end.not_to change(application, :status)
-
- expect(application.status_reason).to be_nil
- end
- end
- end
- end
-
- context 'when application is uninstalling' do
- reschedule_phases.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
-
- context 'when installation POD succeeded' do
- let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- before do
- expect_next_instance_of(Gitlab::Kubernetes::Helm::API) do |instance|
- expect(instance).to receive(:delete_pod!).with(kind_of(String)).once
- end
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'removes the installation POD' do
- expect(service).to receive(:remove_uninstallation_pod).and_call_original
-
- service.execute
- end
-
- it 'runs application post_uninstall' do
- expect(application).to receive(:post_uninstall).and_call_original
-
- service.execute
- end
-
- it 'destroys the application' do
- expect(worker_class).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_destroyed
- end
-
- context 'an error occurs while destroying' do
- before do
- expect(application).to receive(:destroy!).once.and_raise("destroy failed")
- end
-
- it 'still removes the installation POD' do
- expect(service).to receive(:remove_uninstallation_pod).and_call_original
-
- service.execute
- end
-
- it 'makes the application uninstall_errored' do
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed')
- end
- end
- end
-
- context 'when installation POD failed' do
- let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.')
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) }
-
- before do
- expect(service).to receive(:pod_phase).once.and_return(phase)
- end
-
- it 'make the application errored' do
- expect(worker_class).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.')
- end
- end
-
- context 'when installation raises a Kubeclient::HttpError' do
- let(:cluster) { create(:cluster, :provided_by_user, :project) }
- let(:logger) { service.send(:logger) }
- let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
-
- before do
- application.update!(cluster: cluster)
-
- expect(service).to receive(:pod_phase).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'Unauthorized' }
- let(:error_code) { 401 }
- end
-
- it 'shows the response code from the error' do
- service.execute
-
- expect(application).to be_uninstall_errored
- expect(application.status_reason).to eq('Kubernetes error: 401')
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb b/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb
deleted file mode 100644
index dbde8cec9b9..00000000000
--- a/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CheckUpgradeProgressService do
- reschedule_phashes = ::Gitlab::Kubernetes::Pod::PHASES -
- [::Gitlab::Kubernetes::Pod::SUCCEEDED, ::Gitlab::Kubernetes::Pod::FAILED, ::Gitlab].freeze
-
- let(:application) { create(:clusters_applications_prometheus, :updating) }
- let(:service) { described_class.new(application) }
- let(:phase) { ::Gitlab::Kubernetes::Pod::UNKNOWN }
- let(:errors) { nil }
-
- shared_examples 'a terminated upgrade' do
- it 'removes the POD' do
- expect(service).to receive(:remove_pod).once
-
- service.execute
- end
- end
-
- shared_examples 'a not yet terminated upgrade' do |a_phase|
- let(:phase) { a_phase }
-
- context "when phase is #{a_phase}" do
- context 'when not timed out' do
- it 'reschedule a new check' do
- expect(::ClusterWaitForAppUpdateWorker).to receive(:perform_in).once
- expect(service).not_to receive(:remove_pod)
-
- service.execute
-
- expect(application).to be_updating
- expect(application.status_reason).to be_nil
- end
- end
-
- context 'when timed out' do
- let(:application) { create(:clusters_applications_prometheus, :timed_out, :updating) }
-
- it_behaves_like 'a terminated upgrade'
-
- it 'make the application update errored' do
- expect(::ClusterWaitForAppUpdateWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq("Update timed out")
- end
- end
- end
- end
-
- before do
- allow(service).to receive(:phase).once.and_return(phase)
-
- allow(service).to receive(:errors).and_return(errors)
- allow(service).to receive(:remove_pod).and_return(nil)
- end
-
- describe '#execute' do
- context 'when upgrade pod succeeded' do
- let(:phase) { ::Gitlab::Kubernetes::Pod::SUCCEEDED }
-
- it_behaves_like 'a terminated upgrade'
-
- it 'make the application upgraded' do
- expect(::ClusterWaitForAppUpdateWorker).not_to receive(:perform_in)
-
- service.execute
-
- expect(application).to be_updated
- expect(application.status_reason).to be_nil
- end
- end
-
- context 'when upgrade pod failed' do
- let(:phase) { ::Gitlab::Kubernetes::Pod::FAILED }
- let(:errors) { 'test installation failed' }
-
- it_behaves_like 'a terminated upgrade'
-
- it 'make the application update errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(errors)
- end
- end
-
- reschedule_phashes.each { |phase| it_behaves_like 'a not yet terminated upgrade', phase }
- end
-end
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
deleted file mode 100644
index 00a67a9b2ef..00000000000
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ /dev/null
@@ -1,279 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CreateService do
- include TestRequestHelpers
-
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:user) { create(:user) }
- let(:params) { { application: 'ingress' } }
- let(:service) { described_class.new(cluster, user, params) }
-
- describe '#execute' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
- allow(ClusterUpgradeAppWorker).to receive(:perform_async)
- end
-
- subject { service.execute(test_request) }
-
- it 'creates an application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_ingress)
- end
-
- context 'application already installed' do
- let!(:application) { create(:clusters_applications_ingress, :installed, cluster: cluster) }
-
- it 'does not create a new application' do
- expect do
- subject
- end.not_to change(Clusters::Applications::Ingress, :count)
- end
-
- it 'schedules an upgrade for the application' do
- expect(ClusterUpgradeAppWorker).to receive(:perform_async)
-
- subject
- end
- end
-
- context 'known applications' do
- context 'ingress application' do
- let(:params) do
- {
- application: 'ingress'
- }
- end
-
- before do
- expect_any_instance_of(Clusters::Applications::Ingress)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_ingress)
- end
- end
-
- context 'cert manager application' do
- let(:params) do
- {
- application: 'cert_manager',
- email: 'test@example.com'
- }
- end
-
- before do
- expect_any_instance_of(Clusters::Applications::CertManager)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_cert_manager)
- end
-
- it 'sets the email' do
- expect(subject.email).to eq('test@example.com')
- end
- end
-
- context 'jupyter application' do
- let(:params) do
- {
- application: 'jupyter',
- hostname: 'example.com'
- }
- end
-
- before do
- create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster)
- expect_any_instance_of(Clusters::Applications::Jupyter)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_jupyter)
- end
-
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
- end
-
- it 'sets the oauth_application' do
- expect(subject.oauth_application).to be_present
- end
- end
-
- context 'knative application' do
- let(:params) do
- {
- application: 'knative',
- hostname: 'example.com',
- pages_domain_id: domain.id
- }
- end
-
- let(:domain) { create(:pages_domain, :instance_serverless) }
- let(:associate_domain_service) { double('AssociateDomainService') }
-
- before do
- expect_any_instance_of(Clusters::Applications::Knative)
- .to receive(:make_scheduled!)
- .and_call_original
- end
-
- it 'creates the application' do
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, :application_knative)
- end
-
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
- end
-
- it 'executes AssociateDomainService' do
- expect(Serverless::AssociateDomainService).to receive(:new) do |knative, args|
- expect(knative).to be_a(Clusters::Applications::Knative)
- expect(args[:pages_domain_id]).to eq(params[:pages_domain_id])
- expect(args[:creator]).to eq(user)
-
- associate_domain_service
- end
-
- expect(associate_domain_service).to receive(:execute)
-
- subject
- end
- end
- end
-
- context 'invalid application' do
- let(:params) { { application: 'non-existent' } }
-
- it 'raises an error' do
- expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
- end
- end
-
- context 'group cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :group) }
-
- using RSpec::Parameterized::TableSyntax
-
- where(:application, :association, :allowed, :pre_create_ingress) do
- 'ingress' | :application_ingress | true | false
- 'runner' | :application_runner | true | false
- 'prometheus' | :application_prometheus | true | false
- 'jupyter' | :application_jupyter | true | true
- end
-
- with_them do
- before do
- klass = "Clusters::Applications::#{application.titleize}"
- allow_any_instance_of(klass.constantize).to receive(:make_scheduled!).and_call_original
- create(:clusters_applications_ingress, :installed, cluster: cluster, external_hostname: 'example.com') if pre_create_ingress
- end
-
- let(:params) { { application: application } }
-
- it 'executes for each application' do
- if allowed
- expect do
- subject
-
- cluster.reload
- end.to change(cluster, association)
- else
- expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
- end
- end
- end
- end
-
- context 'when application is installable' do
- shared_examples 'installable applications' do
- it 'makes the application scheduled' do
- expect do
- subject
- end.to change { Clusters::Applications::Ingress.with_status(:scheduled).count }.by(1)
- end
-
- it 'schedules an install via worker' do
- expect(ClusterInstallAppWorker)
- .to receive(:perform_async)
- .with(*worker_arguments)
- .once
-
- subject
- end
- end
-
- context 'when application is associated with a cluster' do
- let(:application) { create(:clusters_applications_ingress, :installable, cluster: cluster) }
- let(:worker_arguments) { [application.name, application.id] }
-
- it_behaves_like 'installable applications'
- end
-
- context 'when application is not associated with a cluster' do
- let(:worker_arguments) { [params[:application], kind_of(Numeric)] }
-
- it_behaves_like 'installable applications'
- end
- end
-
- context 'when installation is already in progress' do
- let!(:application) { create(:clusters_applications_ingress, :installing, cluster: cluster) }
-
- it 'raises an exception' do
- expect { subject }
- .to raise_exception(StateMachines::InvalidTransition)
- .and not_change(application.class.with_status(:scheduled), :count)
- end
-
- it 'does not schedule a cluster worker' do
- expect(ClusterInstallAppWorker).not_to receive(:perform_async)
- end
- end
-
- context 'when application is installed' do
- %i(installed updated).each do |status|
- let(:application) { create(:clusters_applications_ingress, status, cluster: cluster) }
-
- it 'schedules an upgrade via worker' do
- expect(ClusterUpgradeAppWorker)
- .to receive(:perform_async)
- .with(application.name, application.id)
- .once
-
- subject
-
- expect(application.reload).to be_scheduled
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb
deleted file mode 100644
index 281da62b80b..00000000000
--- a/spec/services/clusters/applications/patch_service_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::PatchService do
- describe '#execute' do
- let(:application) { create(:clusters_applications_knative, :scheduled) }
- let!(:update_command) { application.update_command }
- let(:service) { described_class.new(application) }
- let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::API) }
-
- before do
- allow(service).to receive(:update_command).and_return(update_command)
- allow(service).to receive(:helm_api).and_return(helm_client)
- end
-
- context 'when there are no errors' do
- before do
- expect(helm_client).to receive(:update).with(update_command)
- allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
- end
-
- it 'make the application updating' do
- expect(application.cluster).not_to be_nil
- service.execute
-
- expect(application).to be_updating
- end
-
- it 'schedule async installation status check' do
- expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
-
- service.execute
- end
- end
-
- context 'when kubernetes cluster communication fails' do
- let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
-
- before do
- expect(helm_client).to receive(:update).with(update_command).and_raise(error)
- end
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'Kubeclient::HttpError' }
- let(:error_message) { 'system failure' }
- let(:error_code) { 500 }
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Kubernetes error: %{error_code}') % { error_code: 500 })
- end
- end
-
- context 'a non kubernetes error happens' do
- let(:application) { create(:clusters_applications_knative, :scheduled) }
- let(:error) { StandardError.new('something bad happened') }
-
- include_examples 'logs kubernetes errors' do
- let(:error_name) { 'StandardError' }
- let(:error_message) { 'something bad happened' }
- let(:error_code) { nil }
- end
-
- before do
- expect(helm_client).to receive(:update).with(update_command).and_raise(error)
- end
-
- it 'make the application errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to eq(_('Failed to update.'))
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/prometheus_update_service_spec.rb b/spec/services/clusters/applications/prometheus_update_service_spec.rb
deleted file mode 100644
index 615bfc44045..00000000000
--- a/spec/services/clusters/applications/prometheus_update_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::PrometheusUpdateService do
- describe '#execute' do
- let(:project) { create(:project) }
- let(:environment) { create(:environment, project: project) }
- let(:cluster) { create(:cluster, :provided_by_user, :with_installed_helm, projects: [project]) }
- let(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
- let(:empty_alerts_values_update_yaml) { "---\nalertmanager:\n enabled: false\nserverFiles:\n alerts: {}\n" }
- let(:helm_client) { instance_double(::Gitlab::Kubernetes::Helm::API) }
-
- subject(:service) { described_class.new(application, project) }
-
- context 'when prometheus is a Clusters::Integrations::Prometheus' do
- let(:application) { create(:clusters_integrations_prometheus, cluster: cluster) }
-
- it 'raises NotImplementedError' do
- expect { service.execute }.to raise_error(NotImplementedError)
- end
- end
-
- context 'when prometheus is externally installed' do
- let(:application) { create(:clusters_applications_prometheus, :externally_installed, cluster: cluster) }
-
- it 'raises NotImplementedError' do
- expect { service.execute }.to raise_error(NotImplementedError)
- end
- end
-
- context 'when prometheus is a Clusters::Applications::Prometheus' do
- let!(:patch_command) { application.patch_command(empty_alerts_values_update_yaml) }
-
- before do
- allow(service).to receive(:patch_command).with(empty_alerts_values_update_yaml).and_return(patch_command)
- allow(service).to receive(:helm_api).and_return(helm_client)
- end
-
- context 'when there are no errors' do
- before do
- expect(helm_client).to receive(:update).with(patch_command)
-
- allow(::ClusterWaitForAppUpdateWorker)
- .to receive(:perform_in)
- .and_return(nil)
- end
-
- it 'make the application updating' do
- expect(application.cluster).not_to be_nil
-
- service.execute
-
- expect(application).to be_updating
- end
-
- it 'updates current config' do
- prometheus_config_service = spy(:prometheus_config_service)
-
- expect(Clusters::Applications::PrometheusConfigService)
- .to receive(:new)
- .with(project, cluster, application)
- .and_return(prometheus_config_service)
-
- expect(prometheus_config_service)
- .to receive(:execute)
- .and_return(YAML.safe_load(empty_alerts_values_update_yaml))
-
- service.execute
- end
-
- it 'schedules async update status check' do
- expect(::ClusterWaitForAppUpdateWorker).to receive(:perform_in).once
-
- service.execute
- end
- end
-
- context 'when k8s cluster communication fails' do
- before do
- error = ::Kubeclient::HttpError.new(500, 'system failure', nil)
- allow(helm_client).to receive(:update).and_raise(error)
- end
-
- it 'make the application update errored' do
- service.execute
-
- expect(application).to be_update_errored
- expect(application.status_reason).to match(/kubernetes error:/i)
- end
- end
-
- context 'when application cannot be persisted' do
- let(:application) { build(:clusters_applications_prometheus, :installed) }
-
- before do
- allow(application).to receive(:make_updating!).once
- .and_raise(ActiveRecord::RecordInvalid.new(application))
- end
-
- it 'make the application update errored' do
- expect(helm_client).not_to receive(:update)
-
- service.execute
-
- expect(application).to be_update_errored
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/update_service_spec.rb b/spec/services/clusters/applications/update_service_spec.rb
deleted file mode 100644
index 4c05a12a4a1..00000000000
--- a/spec/services/clusters/applications/update_service_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::UpdateService do
- include TestRequestHelpers
-
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:user) { create(:user) }
- let(:params) { { application: 'knative', hostname: 'update.example.com', pages_domain_id: domain.id } }
- let(:service) { described_class.new(cluster, user, params) }
- let(:domain) { create(:pages_domain, :instance_serverless) }
-
- subject { service.execute(test_request) }
-
- describe '#execute' do
- before do
- allow(ClusterPatchAppWorker).to receive(:perform_async)
- end
-
- context 'application is not installed' do
- it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
- expect(ClusterPatchAppWorker).not_to receive(:perform_async)
-
- expect { subject }
- .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
- .and not_change { Clusters::Applications::Knative.count }
- .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
- end
- end
-
- context 'application is installed' do
- context 'application is schedulable' do
- let!(:application) do
- create(:clusters_applications_knative, status: 3, cluster: cluster)
- end
-
- it 'updates the application data' do
- expect do
- subject
- end.to change { application.reload.hostname }.to(params[:hostname])
- end
-
- it 'makes application scheduled!' do
- subject
-
- expect(application.reload).to be_scheduled
- end
-
- it 'schedules ClusterPatchAppWorker' do
- expect(ClusterPatchAppWorker).to receive(:perform_async)
-
- subject
- end
-
- context 'knative application' do
- let(:associate_domain_service) { double('AssociateDomainService') }
-
- it 'executes AssociateDomainService' do
- expect(Serverless::AssociateDomainService).to receive(:new) do |knative, args|
- expect(knative.id).to eq(application.id)
- expect(args[:pages_domain_id]).to eq(params[:pages_domain_id])
- expect(args[:creator]).to eq(user)
-
- associate_domain_service
- end
-
- expect(associate_domain_service).to receive(:execute)
-
- subject
- end
- end
- end
-
- context 'application is not schedulable' do
- let!(:application) do
- create(:clusters_applications_knative, status: 4, cluster: cluster)
- end
-
- it 'raises StateMachines::InvalidTransition' do
- expect(ClusterPatchAppWorker).not_to receive(:perform_async)
-
- expect { subject }
- .to raise_exception { StateMachines::InvalidTransition }
- .and not_change { application.reload.hostname }
- .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
index c5778db6001..c8b7f628e5b 100644
--- a/spec/services/clusters/gcp/provision_service_spec.rb
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -42,7 +42,7 @@ RSpec.describe Clusters::Gcp::ProvisionService do
gcp_project_id, zone,
{
"status": 'unexpected'
- } )
+ })
end
it_behaves_like 'error'
diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
index ccb4b3b6c15..ffe4516c02b 100644
--- a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
+++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
{
"status": 'RUNNING',
"startTime": 1.minute.ago.strftime("%FT%TZ")
- } )
+ })
end
it_behaves_like 'continue_creation'
@@ -56,7 +56,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
{
"status": 'RUNNING',
"startTime": 30.minutes.ago.strftime("%FT%TZ")
- } )
+ })
end
it_behaves_like 'error'
@@ -70,7 +70,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
{
"status": 'PENDING',
"startTime": 1.minute.ago.strftime("%FT%TZ")
- } )
+ })
end
it_behaves_like 'continue_creation'
@@ -82,7 +82,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
gcp_project_id, zone, operation_id,
{
"status": 'DONE'
- } )
+ })
end
it_behaves_like 'finalize_creation'
@@ -94,7 +94,7 @@ RSpec.describe Clusters::Gcp::VerifyProvisionStatusService do
gcp_project_id, zone, operation_id,
{
"status": 'unexpected'
- } )
+ })
end
it_behaves_like 'error'
diff --git a/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb b/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb
deleted file mode 100644
index f26177a56d0..00000000000
--- a/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb
+++ /dev/null
@@ -1,223 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Kubernetes::ConfigureIstioIngressService, '#execute' do
- include KubernetesHelpers
-
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:api_url) { 'https://kubernetes.example.com' }
- let(:project) { cluster.project }
- let(:environment) { create(:environment, project: project) }
- let(:cluster_project) { cluster.cluster_project }
- let(:namespace) { "#{project.name}-#{project.id}-#{environment.slug}" }
- let(:kubeclient) { cluster.kubeclient }
-
- subject do
- described_class.new(
- cluster: cluster
- ).execute
- end
-
- before do
- stub_kubeclient_discover_istio(api_url)
- stub_kubeclient_create_secret(api_url, namespace: namespace)
- stub_kubeclient_put_secret(api_url, "#{namespace}-token", namespace: namespace)
-
- stub_kubeclient_get_secret(
- api_url,
- metadata_name: "#{namespace}-token",
- token: Base64.encode64('sample-token'),
- namespace: namespace
- )
-
- stub_kubeclient_get_secret(
- api_url,
- metadata_name: 'istio-ingressgateway-ca-certs',
- namespace: 'istio-system'
- )
-
- stub_kubeclient_get_secret(
- api_url,
- metadata_name: 'istio-ingressgateway-certs',
- namespace: 'istio-system'
- )
-
- stub_kubeclient_put_secret(api_url, 'istio-ingressgateway-ca-certs', namespace: 'istio-system')
- stub_kubeclient_put_secret(api_url, 'istio-ingressgateway-certs', namespace: 'istio-system')
- stub_kubeclient_get_gateway(api_url, 'knative-ingress-gateway', namespace: 'knative-serving')
- stub_kubeclient_put_gateway(api_url, 'knative-ingress-gateway', namespace: 'knative-serving')
- end
-
- context 'without a serverless_domain_cluster' do
- it 'configures gateway to use PASSTHROUGH' do
- subject
-
- expect(WebMock).to have_requested(:put, api_url + '/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway').with(
- body: hash_including(
- apiVersion: "networking.istio.io/v1alpha3",
- kind: "Gateway",
- metadata: {
- generation: 1,
- labels: {
- "networking.knative.dev/ingress-provider" => "istio",
- "serving.knative.dev/release" => "v0.7.0"
- },
- name: "knative-ingress-gateway",
- namespace: "knative-serving",
- selfLink: "/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway"
- },
- spec: {
- selector: {
- istio: "ingressgateway"
- },
- servers: [
- {
- hosts: ["*"],
- port: {
- name: "http",
- number: 80,
- protocol: "HTTP"
- }
- },
- {
- hosts: ["*"],
- port: {
- name: "https",
- number: 443,
- protocol: "HTTPS"
- },
- tls: {
- mode: "PASSTHROUGH"
- }
- }
- ]
- }
- )
- )
- end
- end
-
- context 'with a serverless_domain_cluster' do
- let(:serverless_domain_cluster) { create(:serverless_domain_cluster) }
- let(:certificate) { OpenSSL::X509::Certificate.new(serverless_domain_cluster.certificate) }
-
- before do
- cluster.application_knative = serverless_domain_cluster.knative
- end
-
- it 'configures certificates' do
- subject
-
- expect(serverless_domain_cluster.reload.key).not_to be_blank
- expect(serverless_domain_cluster.reload.certificate).not_to be_blank
-
- expect(certificate.subject.to_s).to include(serverless_domain_cluster.knative.hostname)
-
- expect(certificate.not_before).to be_within(1.minute).of(Time.current)
- expect(certificate.not_after).to be_within(1.minute).of(Time.current + 1000.years)
-
- expect(WebMock).to have_requested(:put, api_url + '/api/v1/namespaces/istio-system/secrets/istio-ingressgateway-ca-certs').with(
- body: hash_including(
- metadata: {
- name: 'istio-ingressgateway-ca-certs',
- namespace: 'istio-system'
- },
- type: 'Opaque'
- )
- )
-
- expect(WebMock).to have_requested(:put, api_url + '/api/v1/namespaces/istio-system/secrets/istio-ingressgateway-certs').with(
- body: hash_including(
- metadata: {
- name: 'istio-ingressgateway-certs',
- namespace: 'istio-system'
- },
- type: 'kubernetes.io/tls'
- )
- )
- end
-
- it 'configures gateway to use MUTUAL' do
- subject
-
- expect(WebMock).to have_requested(:put, api_url + '/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway').with(
- body: {
- apiVersion: "networking.istio.io/v1alpha3",
- kind: "Gateway",
- metadata: {
- generation: 1,
- labels: {
- "networking.knative.dev/ingress-provider" => "istio",
- "serving.knative.dev/release" => "v0.7.0"
- },
- name: "knative-ingress-gateway",
- namespace: "knative-serving",
- selfLink: "/apis/networking.istio.io/v1alpha3/namespaces/knative-serving/gateways/knative-ingress-gateway"
- },
- spec: {
- selector: {
- istio: "ingressgateway"
- },
- servers: [
- {
- hosts: ["*"],
- port: {
- name: "http",
- number: 80,
- protocol: "HTTP"
- }
- },
- {
- hosts: ["*"],
- port: {
- name: "https",
- number: 443,
- protocol: "HTTPS"
- },
- tls: {
- mode: "MUTUAL",
- privateKey: "/etc/istio/ingressgateway-certs/tls.key",
- serverCertificate: "/etc/istio/ingressgateway-certs/tls.crt",
- caCertificates: "/etc/istio/ingressgateway-ca-certs/cert.pem"
- }
- }
- ]
- }
- }
- )
- end
- end
-
- context 'when there is an error' do
- before do
- cluster.application_knative = create(:clusters_applications_knative)
-
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:configure_passthrough).and_raise(error)
- end
- end
-
- context 'Kubeclient::HttpError' do
- let(:error) { Kubeclient::HttpError.new(404, nil, nil) }
-
- it 'puts Knative into an errored state' do
- subject
-
- expect(cluster.application_knative).to be_errored
- expect(cluster.application_knative.status_reason).to eq('Kubernetes error: 404')
- end
- end
-
- context 'StandardError' do
- let(:error) { RuntimeError.new('something went wrong') }
-
- it 'puts Knative into an errored state' do
- subject
-
- expect(cluster.application_knative).to be_errored
- expect(cluster.application_knative.status_reason).to eq('Failed to update.')
- end
- end
- end
-end
diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
index 064f9e42e96..37478a0bcd9 100644
--- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
@@ -166,7 +166,7 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{role_binding_name}").with(
body: hash_including(
- metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" },
+ metadata: { name: "gitlab-#{namespace}", namespace: namespace.to_s },
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
kind: 'ClusterRole',
diff --git a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
index 607d67d8efe..470c6eb9e03 100644
--- a/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_cached_manifest_service_spec.rb
@@ -39,6 +39,14 @@ RSpec.describe DependencyProxy::FindCachedManifestService do
end
end
+ shared_examples 'returning an error' do
+ it 'returns an error', :aggregate_failures do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:http_status]).to eq(503)
+ expect(subject[:message]).to eq('Failed to download the manifest from the external registry')
+ end
+ end
+
context 'when no manifest exists' do
let_it_be(:image) { 'new-image' }
@@ -101,7 +109,7 @@ RSpec.describe DependencyProxy::FindCachedManifestService do
it_behaves_like 'returning no manifest'
end
- context 'failed connection' do
+ context 'when the connection fails' do
before do
expect(DependencyProxy::HeadManifestService).to receive(:new).and_raise(Net::OpenTimeout)
end
@@ -111,12 +119,24 @@ RSpec.describe DependencyProxy::FindCachedManifestService do
context 'and no manifest is cached' do
let_it_be(:image) { 'new-image' }
- it 'returns an error', :aggregate_failures do
- expect(subject[:status]).to eq(:error)
- expect(subject[:http_status]).to eq(503)
- expect(subject[:message]).to eq('Failed to download the manifest from the external registry')
+ it_behaves_like 'returning an error'
+ end
+ end
+
+ context 'when the connection is successful but with error in result' do
+ before do
+ allow_next_instance_of(DependencyProxy::HeadManifestService) do |service|
+ allow(service).to receive(:execute).and_return(status: :error, http_status: 401, message: "Not found")
end
end
+
+ it_behaves_like 'using the cached manifest'
+
+ context 'and no manifest is cached' do
+ let_it_be(:image) { 'new-image' }
+
+ it_behaves_like 'returning no manifest'
+ end
end
end
end
diff --git a/spec/services/deployments/create_for_build_service_spec.rb b/spec/services/deployments/create_for_build_service_spec.rb
index a2e1acadcc1..3748df87d99 100644
--- a/spec/services/deployments/create_for_build_service_spec.rb
+++ b/spec/services/deployments/create_for_build_service_spec.rb
@@ -30,7 +30,9 @@ RSpec.describe Deployments::CreateForBuildService do
context 'when creation failure occures' do
before do
- allow(build).to receive(:create_deployment!) { raise ActiveRecord::RecordInvalid }
+ allow_next_instance_of(Deployment) do |deployment|
+ allow(deployment).to receive(:save!) { raise ActiveRecord::RecordInvalid }
+ end
end
it 'trackes the exception' do
@@ -79,5 +81,88 @@ RSpec.describe Deployments::CreateForBuildService do
expect { subject }.not_to change { Deployment.count }
end
end
+
+ context 'when build has environment attribute' do
+ let!(:build) do
+ create(:ci_build, environment: 'production', project: project,
+ options: { environment: { name: 'production', **kubernetes_options } })
+ end
+
+ let!(:environment) { create(:environment, project: project, name: build.expanded_environment_name) }
+
+ let(:kubernetes_options) { {} }
+
+ it 'returns a deployment object with environment' do
+ expect(subject).to be_a(Deployment)
+ expect(subject.iid).to be_present
+ expect(subject.environment.name).to eq('production')
+ expect(subject.cluster).to be_nil
+ expect(subject.deployment_cluster).to be_nil
+ end
+
+ context 'when environment has deployment platform' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project], managed: managed_cluster) }
+ let(:managed_cluster) { true }
+
+ it 'sets the cluster and deployment_cluster' do
+ expect(subject.cluster).to eq(cluster) # until we stop double writing in 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
+ expect(subject.deployment_cluster.cluster).to eq(cluster)
+ end
+
+ context 'when a custom namespace is given' do
+ let(:kubernetes_options) { { kubernetes: { namespace: 'the-custom-namespace' } } }
+
+ context 'when cluster is managed' do
+ it 'does not set the custom namespace' do
+ expect(subject.deployment_cluster.kubernetes_namespace).not_to eq('the-custom-namespace')
+ end
+ end
+
+ context 'when cluster is not managed' do
+ let(:managed_cluster) { false }
+
+ it 'sets the custom namespace' do
+ expect(subject.deployment_cluster.kubernetes_namespace).to eq('the-custom-namespace')
+ end
+ end
+ end
+ end
+
+ context 'when build already has deployment' do
+ let!(:build) { create(:ci_build, :with_deployment, project: project, environment: 'production') }
+ let!(:environment) {}
+
+ it 'returns the persisted deployment' do
+ expect { subject }.not_to change { Deployment.count }
+
+ is_expected.to eq(build.deployment)
+ end
+ end
+ end
+
+ context 'when build does not start environment' do
+ where(:action) do
+ %w[stop prepare verify access]
+ end
+
+ with_them do
+ let!(:build) do
+ create(:ci_build, environment: 'production', project: project,
+ options: { environment: { name: 'production', action: action } })
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'when build does not have environment attribute' do
+ let!(:build) { create(:ci_build, project: project) }
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/services/environments/create_for_build_service_spec.rb
index 2b9d8127886..721822f355b 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb
+++ b/spec/services/environments/create_for_build_service_spec.rb
@@ -2,27 +2,21 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
- let_it_be(:project) { create(:project) }
+RSpec.describe Environments::CreateForBuildService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:job) { build(:ci_build, project: project, pipeline: pipeline, **attributes) }
+ let(:service) { described_class.new }
+ let(:merge_request) {}
- let(:job) { build(:ci_build, project: project, pipeline: pipeline) }
- let(:seed) { described_class.new(job) }
- let(:attributes) { {} }
-
- before do
- job.assign_attributes(**attributes)
- end
-
- describe '#to_resource' do
- subject { seed.to_resource }
+ describe '#execute' do
+ subject { service.execute(job, merge_request: merge_request) }
shared_examples_for 'returning a correct environment' do
let(:expected_auto_stop_in_seconds) do
- if expected_auto_stop_in
- ChronicDuration.parse(expected_auto_stop_in).seconds
- end
+ ChronicDuration.parse(expected_auto_stop_in).seconds if expected_auto_stop_in
end
it 'returns a persisted environment object' do
@@ -220,5 +214,91 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do
end
end
end
+
+ context 'when a pipeline contains a deployment job' do
+ let!(:job) { build(:ci_build, :start_review_app, project: project) }
+
+ context 'and the environment does not exist' do
+ it 'creates the environment specified by the job' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+
+ context 'and the pipeline is for a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'associates the environment with the merge request' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment.merge_request).to eq(merge_request)
+ end
+ end
+ end
+
+ context 'when an environment already exists' do
+ before do
+ create(:environment, project: project, name: 'review/master')
+ end
+
+ it 'ensures environment existence for the job' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+
+ context 'and the pipeline is for a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'does not associate the environment with the merge request' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(environment.merge_request).to be_nil
+ end
+ end
+ end
+
+ context 'when an environment name contains an invalid character' do
+ before do
+ job.pipeline = build(:ci_pipeline, ref: '!!!', project: project)
+ end
+
+ it 'sets the failure status' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(job).to be_failed
+ expect(job).to be_environment_creation_failure
+ expect(job.persisted_environment).to be_nil
+ end
+ end
+ end
+
+ context 'when a pipeline contains a teardown job' do
+ let!(:job) { build(:ci_build, :stop_review_app, project: project) }
+
+ it 'ensures environment existence for the job' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+ end
+
+ context 'when a pipeline does not contain a deployment job' do
+ let!(:job) { build(:ci_build, project: project) }
+
+ it 'does not create any environments' do
+ expect { subject }.not_to change { Environment.count }
+ end
+ end
+
+ def environment
+ project.environments.find_by_name('review/master')
+ end
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 06f0eb1efbc..c3ae062a4b2 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -20,33 +20,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
end
- shared_examples 'Snowplow event' do
- let(:label) { nil }
-
- it 'is not emitted if FF is disabled' do
- stub_feature_flags(feature_flag_name => false)
-
- subject
-
- expect_no_snowplow_event
- end
-
- it 'is emitted' do
- params = {
- category: category,
- action: action,
- namespace: namespace,
- user: user,
- project: project,
- label: label
- }.compact
-
- subject
-
- expect_snowplow_event(**params)
- end
- end
-
describe 'Issues' do
describe '#open_issue' do
let(:issue) { create(:issue) }
@@ -95,14 +68,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
- it_behaves_like 'Snowplow event' do
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'create' }
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'created' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
@@ -121,14 +97,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
- it_behaves_like 'Snowplow event' do
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'close' }
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'closed' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
@@ -147,14 +126,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION }
end
- it_behaves_like 'Snowplow event' do
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'merge' }
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.name }
+ let(:action) { 'merged' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
let(:project) { merge_request.project }
let(:user) { merge_request.author }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
@@ -330,11 +312,16 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
end
- it_behaves_like 'Snowplow event' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:category) { described_class.to_s }
- let(:action) { 'action_active_users_project_repo' }
+ let(:action) { :push }
let(:namespace) { project.namespace }
let(:feature_flag_name) { :route_hll_to_snowplow }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: 'action_active_users_project_repo').to_context]
+ end
end
end
@@ -355,11 +342,16 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION }
end
- it_behaves_like 'Snowplow event' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:category) { described_class.to_s }
- let(:action) { 'action_active_users_project_repo' }
+ let(:action) { :push }
let(:namespace) { project.namespace }
let(:feature_flag_name) { :route_hll_to_snowplow }
+ let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
+ event: 'action_active_users_project_repo').to_context]
+ end
end
end
@@ -495,7 +487,7 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
stub_feature_flags(route_hll_to_snowplow_phase2: false)
end
- it 'doesnt emit snowwplow events', :snowplow do
+ it 'doesnt emit snowplow events', :snowplow do
subject
expect_no_snowplow_event
@@ -518,19 +510,22 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi
end
context 'when it is a diff note' do
- it_behaves_like "it records the event in the event counter" do
- let(:note) { create(:diff_note_on_merge_request) }
- end
+ let(:note) { create(:diff_note_on_merge_request) }
- it_behaves_like 'Snowplow event' do
+ it_behaves_like "it records the event in the event counter"
+
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:note) { create(:diff_note_on_merge_request) }
- let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s }
- let(:label) { 'merge_requests_users' }
- let(:action) { 'comment' }
- let(:project) { note.project }
+ let(:category) { described_class.name }
+ let(:action) { 'commented' }
+ let(:label) { 'usage_activity_by_stage_monthly.create.merge_requests_users' }
let(:namespace) { project.namespace }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:project) { note.project }
let(:user) { author }
+ let(:context) do
+ [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ end
end
end
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
index a8d753ff124..5afd7b30ab0 100644
--- a/spec/services/git/base_hooks_service_spec.rb
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -4,10 +4,10 @@ require 'spec_helper'
RSpec.describe Git::BaseHooksService do
include RepoHelpers
- include GitHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
let(:oldrev) { Gitlab::Git::BLANK_SHA }
let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
let(:ref) { 'refs/tags/v1.1.0' }
@@ -150,11 +150,16 @@ RSpec.describe Git::BaseHooksService do
end
shared_examples 'creates pipeline with params and expected variables' do
+ let(:pipeline_service) { double(execute: service_response) }
+ let(:service_response) { double(error?: false, payload: pipeline, message: "Error") }
+ let(:pipeline) { double(persisted?: true) }
+
it 'calls the create pipeline service' do
expect(Ci::CreatePipelineService)
.to receive(:new)
.with(project, user, pipeline_params)
- .and_return(double(execute!: true))
+ .and_return(pipeline_service)
+ expect(subject).not_to receive(:log_pipeline_errors)
subject.execute
end
@@ -239,4 +244,85 @@ RSpec.describe Git::BaseHooksService do
it_behaves_like 'creates pipeline with params and expected variables'
end
end
+
+ describe "Pipeline creation" do
+ let(:pipeline_params) do
+ {
+ after: newrev,
+ before: oldrev,
+ checkout_sha: checkout_sha,
+ push_options: push_options,
+ ref: ref,
+ variables_attributes: variables_attributes
+ }
+ end
+
+ let(:pipeline_service) { double(execute: service_response) }
+ let(:push_options) { {} }
+ let(:variables_attributes) { [] }
+
+ context "when the pipeline is persisted" do
+ let(:pipeline) { double(persisted?: true) }
+
+ context "and there are no errors" do
+ let(:service_response) { double(error?: false, payload: pipeline, message: "Error") }
+
+ it "returns success" do
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+
+ context "and there are errors" do
+ let(:service_response) { double(error?: true, payload: pipeline, message: "Error") }
+
+ it "does not log errors and returns success" do
+ # This behaviour is due to the save_on_errors: true setting that is the default in the execute method.
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+ expect(subject).not_to receive(:log_pipeline_errors).with(service_response.message)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+ end
+
+ context "when the pipeline wasn't persisted" do
+ let(:pipeline) { double(persisted?: false) }
+
+ context "and there are no errors" do
+ let(:service_response) { double(error?: false, payload: pipeline, message: nil) }
+
+ it "returns success" do
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+ expect(subject).to receive(:log_pipeline_errors).with(service_response.message)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+
+ context "and there are errors" do
+ let(:service_response) { double(error?: true, payload: pipeline, message: "Error") }
+
+ it "logs errors and returns success" do
+ expect(Ci::CreatePipelineService)
+ .to receive(:new)
+ .with(project, user, pipeline_params)
+ .and_return(pipeline_service)
+ expect(subject).to receive(:log_pipeline_errors).with(service_response.message)
+
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
index 87dbf79a245..597254d46fa 100644
--- a/spec/services/git/tag_push_service_spec.rb
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Git::TagPushService do
include RepoHelpers
- include GitHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/services/google_cloud/generate_pipeline_service_spec.rb b/spec/services/google_cloud/generate_pipeline_service_spec.rb
index 75494f229b5..a78d8ff6661 100644
--- a/spec/services/google_cloud/generate_pipeline_service_spec.rb
+++ b/spec/services/google_cloud/generate_pipeline_service_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe GoogleCloud::GeneratePipelineService do
let_it_be(:service_params) { { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN } }
let_it_be(:service) { described_class.new(project, maintainer, service_params) }
- before do
+ before_all do
project.add_maintainer(maintainer)
file_name = '.gitlab-ci.yml'
@@ -103,6 +103,15 @@ EOF
expect(pipeline[:include]).to be_present
expect(gitlab_ci_yml).to include('https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/gcp/cloud-run.gitlab-ci.yml')
end
+
+ it 'stringifies keys from the existing pipelines' do
+ response = service.execute
+
+ branch_name = response[:branch_name]
+ gitlab_ci_yml = project.repository.gitlab_ci_yml_for(branch_name)
+
+ expect(YAML.safe_load(gitlab_ci_yml).keys).to eq(%w[stages build-java test-java include])
+ end
end
describe 'when there is an existing pipeline with `deploy` stage' do
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 0a8164c9ca3..0425ba3e631 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -271,33 +271,4 @@ RSpec.describe Groups::CreateService, '#execute' do
end
end
end
-
- describe 'logged_out_marketing_header experiment', :experiment do
- let(:service) { described_class.new(user, group_params) }
-
- subject { service.execute }
-
- before do
- stub_experiments(logged_out_marketing_header: :candidate)
- end
-
- it 'tracks signed_up event' do
- expect(experiment(:logged_out_marketing_header)).to track(
- :namespace_created,
- namespace: an_instance_of(Group)
- ).on_next_instance.with_context(actor: user)
-
- subject
- end
-
- context 'when group has not been persisted' do
- let(:service) { described_class.new(user, group_params.merge(name: '<script>alert("Attack!")</script>')) }
-
- it 'does not track signed_up event' do
- expect(experiment(:logged_out_marketing_header)).not_to track(:namespace_created)
-
- subject
- end
- end
- end
end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 36e868fa5f1..f2dbb69f855 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -36,37 +36,17 @@ RSpec.describe Groups::DestroyService do
end
context 'bot tokens', :sidekiq_inline do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates group bot removal', :aggregate_failures do
- bot = create(:user, :project_bot)
- group.add_developer(bot)
- create(:personal_access_token, user: bot)
-
- destroy_group(group, user, async)
-
- expect(
- Users::GhostUserMigration.where(user: bot,
- initiator_user: user)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'removes group bot', :aggregate_failures do
- bot = create(:user, :project_bot)
- group.add_developer(bot)
- token = create(:personal_access_token, user: bot)
+ it 'initiates group bot removal', :aggregate_failures do
+ bot = create(:user, :project_bot)
+ group.add_developer(bot)
+ create(:personal_access_token, user: bot)
- destroy_group(group, user, async)
+ destroy_group(group, user, async)
- expect(PersonalAccessToken.find_by(id: token.id)).to be_nil
- expect(User.find_by(id: bot.id)).to be_nil
- expect(User.find_by(id: user.id)).not_to be_nil
- end
+ expect(
+ Users::GhostUserMigration.where(user: bot,
+ initiator_user: user)
+ ).to be_exists
end
end
@@ -146,7 +126,7 @@ RSpec.describe Groups::DestroyService do
end
expect { destroy_group(group, user, false) }
- .to raise_error(Groups::DestroyService::DestroyError, "Project #{project.id} can't be deleted" )
+ .to raise_error(Groups::DestroyService::DestroyError, "Project #{project.id} can't be deleted")
end
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index b543661e9a0..3cf2c875341 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -200,7 +200,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let_it_be(:project2) { create(:project, :private, namespace: group) }
it_behaves_like 'project namespace path is in sync with project path' do
- let(:group_full_path) { "#{group.path}" }
+ let(:group_full_path) { group.path.to_s }
let(:projects_with_project_namespace) { [project1, project2] }
end
end
@@ -274,7 +274,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
it_behaves_like 'project namespace path is in sync with project path' do
- let(:group_full_path) { "#{new_parent_group.full_path}" }
+ let(:group_full_path) { new_parent_group.full_path.to_s }
let(:projects_with_project_namespace) { [project] }
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 5c87b9ac8bb..c758d3d5477 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -101,6 +101,15 @@ RSpec.describe Groups::UpdateService do
expect(public_group.reload.name).to eq('new-name')
end
end
+
+ context 'when the path does not change' do
+ let(:params) { { name: 'new-name', path: public_group.path } }
+
+ it 'allows the update' do
+ expect(subject).to be true
+ expect(public_group.reload.name).to eq('new-name')
+ end
+ end
end
context 'within subgroup' do
diff --git a/spec/services/groups/update_shared_runners_service_spec.rb b/spec/services/groups/update_shared_runners_service_spec.rb
index 6e938984052..98eccedeace 100644
--- a/spec/services/groups/update_shared_runners_service_spec.rb
+++ b/spec/services/groups/update_shared_runners_service_spec.rb
@@ -127,7 +127,7 @@ RSpec.describe Groups::UpdateSharedRunnersService do
end
context 'when parent does not allow' do
- let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false ) }
+ let_it_be(:parent) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false) }
let_it_be(:group) { create(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent: parent) }
it 'results error' do
diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb
index 7b86c5c45b0..027d0240a7a 100644
--- a/spec/services/import/fogbugz_service_spec.rb
+++ b/spec/services/import/fogbugz_service_spec.rb
@@ -119,7 +119,7 @@ RSpec.describe Import::FogbugzService do
let(:error_messages_array) { instance_double(Array, join: "something went wrong") }
let(:errors_double) { instance_double(ActiveModel::Errors, full_messages: error_messages_array, :[] => nil) }
let(:project_double) { instance_double(Project, persisted?: false, errors: errors_double) }
- let(:project_creator) { instance_double(Gitlab::FogbugzImport::ProjectCreator, execute: project_double ) }
+ let(:project_creator) { instance_double(Gitlab::FogbugzImport::ProjectCreator, execute: project_double) }
before do
allow(Gitlab::FogbugzImport::ProjectCreator).to receive(:new).and_return(project_creator)
diff --git a/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb b/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
index 28af6219812..3c788138157 100644
--- a/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
+++ b/spec/services/import/gitlab_projects/file_acquisition_strategies/file_upload_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::FileUpload, :aggregate_failures do
- let(:file) { UploadedFile.new( File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') ) }
+ let(:file) { UploadedFile.new(File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz')) }
describe 'validation' do
it 'validates presence of file' do
diff --git a/spec/services/incident_management/timeline_event_tags/create_service_spec.rb b/spec/services/incident_management/timeline_event_tags/create_service_spec.rb
new file mode 100644
index 00000000000..c1b993ce3d9
--- /dev/null
+++ b/spec/services/incident_management/timeline_event_tags/create_service_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IncidentManagement::TimelineEventTags::CreateService do
+ let_it_be(:user_with_permissions) { create(:user) }
+ let_it_be(:user_without_permissions) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project) }
+
+ let(:current_user) { user_with_permissions }
+ let(:args) { { 'name': 'Test tag 1', 'project_path': project.full_path } }
+
+ let(:service) { described_class.new(project, current_user, args) }
+
+ before do
+ project.add_maintainer(user_with_permissions)
+ project.add_developer(user_without_permissions)
+ end
+
+ describe '#execute' do
+ shared_examples 'error response' do |message|
+ it 'has an informative message' do
+ expect(execute).to be_error
+ expect(execute.message).to eq(message)
+ end
+ end
+
+ shared_examples 'success response' do
+ it 'has timeline event tag' do
+ expect(execute).to be_success
+
+ result = execute.payload[:timeline_event_tag]
+ expect(result).to be_a(::IncidentManagement::TimelineEventTag)
+ expect(result.name).to eq(args[:name])
+ expect(result.project).to eq(project)
+ end
+ end
+
+ subject(:execute) { service.execute }
+
+ context 'when current user is nil' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'error response',
+ 'You have insufficient permissions to manage timeline event tags for this project'
+ end
+
+ context 'when user does not have permissions to create tags' do
+ let(:current_user) { user_without_permissions }
+
+ it_behaves_like 'error response',
+ 'You have insufficient permissions to manage timeline event tags for this project'
+ end
+
+ context 'when error occurs during creation' do
+ let(:args) { {} }
+
+ it_behaves_like 'error response', "Name can't be blank and Name is invalid"
+ end
+
+ context 'when user has permissions' do
+ it_behaves_like 'success response'
+
+ it 'creates database record' do
+ expect { execute }.to change {
+ ::IncidentManagement::TimelineEventTag.where(project_id: project.id).count
+ }.by(1)
+ end
+ end
+ end
+end
diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb
index a7f448c825f..b10862a78b5 100644
--- a/spec/services/incident_management/timeline_events/create_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/create_service_spec.rb
@@ -8,6 +8,9 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:incident) { create(:incident, project: project) }
let_it_be(:comment) { create(:note, project: project, noteable: incident) }
+ let_it_be(:timeline_event_tag) do
+ create(:incident_management_timeline_event_tag, name: 'Test tag 1', project: project)
+ end
let(:args) do
{
@@ -134,6 +137,67 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
end
end
+ context 'when timeline event tag names are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ timeline_event_tag_names: ['Test tag 1']
+ }
+ end
+
+ it_behaves_like 'success response'
+
+ it 'matches the tag name' do
+ result = execute.payload[:timeline_event]
+ expect(result.timeline_event_tags.first).to eq(timeline_event_tag)
+ end
+
+ context 'when predefined tags are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ timeline_event_tag_names: ['start time', 'end time']
+ }
+ end
+
+ it_behaves_like 'success response'
+
+ it 'matches the two tags on the event and creates on project' do
+ result = execute.payload[:timeline_event]
+
+ expect(result.timeline_event_tags.count).to eq(2)
+ expect(result.timeline_event_tags.by_names(['Start time', 'End time']).pluck_names)
+ .to match_array(['Start time', 'End time'])
+ expect(project.incident_management_timeline_event_tags.pluck_names)
+ .to include('Start time', 'End time')
+ end
+ end
+
+ context 'when invalid tag names are passed' do
+ let(:args) do
+ {
+ note: 'note',
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ timeline_event_tag_names: ['some other time']
+ }
+ end
+
+ it_behaves_like 'error response', "Following tags don't exist: [\"some other time\"]"
+
+ it 'does not create timeline event' do
+ expect { execute }.not_to change(IncidentManagement::TimelineEvent, :count)
+ end
+ end
+ end
+
context 'with editable param' do
let(:args) do
{
@@ -161,6 +225,38 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it 'successfully creates a database record', :aggregate_failures do
expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1)
end
+
+ context 'when note is more than 280 characters long' do
+ let(:args) do
+ {
+ note: 'a' * 281,
+ occurred_at: Time.current,
+ action: 'new comment',
+ promoted_from_note: comment,
+ auto_created: auto_created
+ }
+ end
+
+ let(:auto_created) { false }
+
+ context 'when was not promoted from note' do
+ let(:comment) { nil }
+
+ context 'when auto_created is true' do
+ let(:auto_created) { true }
+
+ it_behaves_like 'success response'
+ end
+
+ context 'when auto_created is false' do
+ it_behaves_like 'error response', 'Timeline text is too long (maximum is 280 characters)'
+ end
+ end
+
+ context 'when promoted from note' do
+ it_behaves_like 'success response'
+ end
+ end
end
describe 'automatically created timeline events' do
@@ -229,6 +325,17 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do
it_behaves_like 'successfully created timeline event'
end
+ describe '.change_severity' do
+ subject(:execute) { described_class.change_severity(incident, current_user) }
+
+ let_it_be(:severity) { create(:issuable_severity, severity: :critical, issue: incident) }
+
+ let(:expected_note) { "@#{current_user.username} changed the incident severity to **Critical - S1**" }
+ let(:expected_action) { 'severity' }
+
+ it_behaves_like 'successfully created timeline event'
+ end
+
describe '.change_labels' do
subject(:execute) do
described_class.change_labels(incident, current_user, added_labels: added, removed_labels: removed)
diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb
index 5d8518cf2ef..2373a73e108 100644
--- a/spec/services/incident_management/timeline_events/update_service_spec.rb
+++ b/spec/services/incident_management/timeline_events/update_service_spec.rb
@@ -87,6 +87,12 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do
it_behaves_like 'error response', "Timeline text can't be blank"
end
+ context 'when note is more than 280 characters long' do
+ let(:params) { { note: 'n' * 281, occurred_at: occurred_at } }
+
+ it_behaves_like 'error response', 'Timeline text is too long (maximum is 280 characters)'
+ end
+
context 'when occurred_at is nil' do
let(:params) { { note: 'Updated note' } }
diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb
new file mode 100644
index 00000000000..2ce47f42a72
--- /dev/null
+++ b/spec/services/issuable/discussions_list_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Issuable::DiscussionsListService do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, :repository, :private, group: group) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:label) { create(:label, project: project) }
+
+ let(:finder_params_for_issuable) { {} }
+
+ subject(:discussions_service) { described_class.new(current_user, issuable, finder_params_for_issuable) }
+
+ describe 'fetching notes for issue' do
+ let_it_be(:issuable) { create(:issue, project: project) }
+
+ it_behaves_like 'listing issuable discussions', :guest, 1, 7
+ end
+
+ describe 'fetching notes for merge requests' do
+ let_it_be(:issuable) { create(:merge_request, source_project: project, target_project: project) }
+
+ it_behaves_like 'listing issuable discussions', :reporter, 0, 6
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index e88fe1b42f0..ef92b6984d5 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -397,9 +397,26 @@ RSpec.describe Issues::CloseService do
end
context 'when issue is not confidential' do
+ let(:expected_payload) do
+ include(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ closed_at: { current: kind_of(Time), previous: nil },
+ state_id: { current: 2, previous: 1 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ },
+ object_attributes: include(
+ closed_at: kind_of(Time),
+ state: 'closed',
+ action: 'close'
+ )
+ )
+ end
+
it 'executes issue hooks' do
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
described_class.new(project: project, current_user: user).close_issue(issue)
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 5fe4c693451..5ddf91e167e 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -391,22 +391,61 @@ RSpec.describe Issues::CreateService do
end
end
- it 'executes issue hooks when issue is not confidential' do
- opts = { title: 'Title', description: 'Description', confidential: false }
+ describe 'executing hooks' do
+ let(:opts) { { title: 'Title', description: 'Description' } }
+ let(:expected_payload) do
+ include(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ author_id: { current: user.id, previous: nil },
+ created_at: { current: kind_of(Time), previous: nil },
+ description: { current: opts[:description], previous: nil },
+ id: { current: kind_of(Integer), previous: nil },
+ iid: { current: kind_of(Integer), previous: nil },
+ project_id: { current: project.id, previous: nil },
+ title: { current: opts[:title], previous: nil },
+ updated_at: { current: kind_of(Time), previous: nil }
+ },
+ object_attributes: include(
+ opts.merge(
+ author_id: user.id,
+ project_id: project.id
+ )
+ )
+ )
+ end
+
+ it 'executes issue hooks' do
+ expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :issue_hooks)
+ described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ end
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
- end
+ context 'when issue is confidential' do
+ let(:expected_payload) do
+ include(
+ event_type: 'confidential_issue',
+ object_kind: 'issue',
+ changes: include(
+ confidential: { current: true, previous: false }
+ ),
+ object_attributes: include(confidential: true)
+ )
+ end
- it 'executes confidential issue hooks when issue is confidential' do
- opts = { title: 'Title', description: 'Description', confidential: true }
+ before do
+ opts[:confidential] = true
+ end
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :confidential_issue_hooks)
+ it 'executes confidential issue hooks' do
+ expect(project).to receive(:execute_hooks).with(expected_payload, :confidential_issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :confidential_issue_hooks)
- described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute
+ end
+ end
end
context 'after_save callback to store_mentions' do
diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb
index d04480bec18..66d017464bf 100644
--- a/spec/services/issues/export_csv_service_spec.rb
+++ b/spec/services/issues/export_csv_service_spec.rb
@@ -185,7 +185,7 @@ RSpec.describe Issues::ExportCsvService do
labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) }
expect(labeled_rows.count).to eq(2)
- expect(labeled_rows.map { |entry| entry['Labels'] }).to all( eq("Feature,Idea") )
+ expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea"))
end
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 23180f75eb3..655c5085fdc 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -228,18 +228,48 @@ RSpec.describe Issues::MoveService do
end
context 'project issue hooks' do
- let!(:hook) { create(:project_hook, project: old_project, issues_events: true) }
+ let_it_be(:old_project_hook) { create(:project_hook, project: old_project, issues_events: true) }
+ let_it_be(:new_project_hook) { create(:project_hook, project: new_project, issues_events: true) }
+
+ let(:expected_new_project_hook_payload) do
+ hash_including(
+ event_type: 'issue',
+ object_kind: 'issue',
+ object_attributes: include(
+ project_id: new_project.id,
+ state: 'opened',
+ action: 'open'
+ )
+ )
+ end
+
+ let(:expected_old_project_hook_payload) do
+ hash_including(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ state_id: { current: 2, previous: 1 },
+ closed_at: { current: kind_of(Time), previous: nil },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ },
+ object_attributes: include(
+ id: old_issue.id,
+ closed_at: kind_of(Time),
+ state: 'closed',
+ action: 'close'
+ )
+ )
+ end
- it 'executes project issue hooks' do
- allow_next_instance_of(WebHookService) do |instance|
- allow(instance).to receive(:execute)
+ it 'executes project issue hooks for both projects' do
+ expect_next_instance_of(WebHookService, new_project_hook, expected_new_project_hook_payload, 'issue_hooks') do |service|
+ expect(service).to receive(:async_execute).once
+ end
+ expect_next_instance_of(WebHookService, old_project_hook, expected_old_project_hook_payload, 'issue_hooks') do |service|
+ expect(service).to receive(:async_execute).once
end
- # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1,
- # but since the entire spec run takes place in a transaction, we never
- # actually get to the `after_commit` hook that queues these jobs.
- expect { move_service.execute(old_issue, new_project) }
- .not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
+ move_service.execute(old_issue, new_project)
end
end
diff --git a/spec/services/issues/relative_position_rebalancing_service_spec.rb b/spec/services/issues/relative_position_rebalancing_service_spec.rb
index 37a94e1d6a2..27c0394ac8b 100644
--- a/spec/services/issues/relative_position_rebalancing_service_spec.rb
+++ b/spec/services/issues/relative_position_rebalancing_service_spec.rb
@@ -34,10 +34,6 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s
end
end
- before do
- stub_feature_flags(issue_rebalancing_with_retry: false)
- end
-
def issues_in_position_order
project.reload.issues.order_by_relative_position.to_a
end
@@ -97,8 +93,12 @@ RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_s
it 'resumes a started rebalance even if there are already too many rebalances running' do
Gitlab::Redis::SharedState.with do |redis|
- redis.sadd("gitlab:issues-position-rebalances:running_rebalances", "#{::Gitlab::Issues::Rebalancing::State::PROJECT}/#{project.id}")
- redis.sadd("gitlab:issues-position-rebalances:running_rebalances", "1/100")
+ redis.sadd("gitlab:issues-position-rebalances:running_rebalances",
+ [
+ "#{::Gitlab::Issues::Rebalancing::State::PROJECT}/#{project.id}",
+ "1/100"
+ ]
+ )
end
caching = service.send(:caching)
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 477b44f4c2c..6013826f9b1 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -85,9 +85,25 @@ RSpec.describe Issues::ReopenService do
end
context 'when issue is not confidential' do
+ let(:expected_payload) do
+ include(
+ event_type: 'issue',
+ object_kind: 'issue',
+ changes: {
+ closed_at: { current: nil, previous: kind_of(Time) },
+ state_id: { current: 1, previous: 2 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ },
+ object_attributes: include(
+ state: 'opened',
+ action: 'reopen'
+ )
+ )
+ end
+
it 'executes issue hooks' do
- expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
- expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
+ expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks)
execute
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 20b1a1f58bb..f1ee62fd589 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -104,10 +104,33 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.issue_customer_relations_contacts.last.contact).to eq contact
end
- it 'updates issue milestone when passing `milestone` param' do
- update_issue(milestone: milestone)
+ context 'when updating milestone' do
+ before do
+ update_issue({ milestone: nil })
+ end
- expect(issue.milestone).to eq milestone
+ it 'updates issue milestone when passing `milestone` param' do
+ expect { update_issue({ milestone: milestone }) }
+ .to change(issue, :milestone).to(milestone).from(nil)
+ end
+
+ it "triggers 'issuableMilestoneUpdated'" do
+ expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(issue).and_call_original
+
+ update_issue({ milestone: milestone })
+ end
+
+ context 'when milestone remains unchanged' do
+ before do
+ update_issue({ title: 'abc', milestone: milestone })
+ end
+
+ it "does not trigger 'issuableMilestoneUpdated'" do
+ expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
+
+ update_issue({ milestone: milestone })
+ end
+ end
end
context 'when sentry identifier is given' do
@@ -520,7 +543,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
- context 'when decription is not changed' do
+ context 'when description is not changed' do
it 'does not trigger GraphQL description updated subscription' do
expect(GraphqlTriggers).not_to receive(:issuable_description_updated)
@@ -1379,7 +1402,7 @@ RSpec.describe Issues::UpdateService, :mailer do
end
end
- include_examples 'issuable update service' do
+ it_behaves_like 'issuable update service' do
let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) }
end
diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb
index a10aaa14030..3af6cf4c8f4 100644
--- a/spec/services/labels/promote_service_spec.rb
+++ b/spec/services/labels/promote_service_spec.rb
@@ -171,7 +171,7 @@ RSpec.describe Labels::PromoteService do
end
context 'when there is an existing identical group label' do
- let!(:existing_group_label) { create(:group_label, group: group_1, title: project_label_1_1.title ) }
+ let!(:existing_group_label) { create(:group_label, group: group_1, title: project_label_1_1.title) }
it 'uses the existing group label' do
expect { service.execute(project_label_1_1) }
diff --git a/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb
new file mode 100644
index 00000000000..1824f822ba8
--- /dev/null
+++ b/spec/services/loose_foreign_keys/process_deleted_records_service_spec.rb
@@ -0,0 +1,198 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe LooseForeignKeys::ProcessDeletedRecordsService do
+ include MigrationsHelpers
+
+ def create_table_structure
+ migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers)
+
+ migration.create_table :_test_loose_fk_parent_table_1
+ migration.create_table :_test_loose_fk_parent_table_2
+
+ migration.create_table :_test_loose_fk_child_table_1_1 do |t|
+ t.bigint :parent_id
+ end
+
+ migration.create_table :_test_loose_fk_child_table_1_2 do |t|
+ t.bigint :parent_id_with_different_column
+ end
+
+ migration.create_table :_test_loose_fk_child_table_2_1 do |t|
+ t.bigint :parent_id
+ end
+
+ migration.track_record_deletions(:_test_loose_fk_parent_table_1)
+ migration.track_record_deletions(:_test_loose_fk_parent_table_2)
+ end
+
+ let(:all_loose_foreign_key_definitions) do
+ {
+ '_test_loose_fk_parent_table_1' => [
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ '_test_loose_fk_child_table_1_1',
+ '_test_loose_fk_parent_table_1',
+ {
+ column: 'parent_id',
+ on_delete: :async_delete,
+ gitlab_schema: :gitlab_main
+ }
+ ),
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ '_test_loose_fk_child_table_1_2',
+ '_test_loose_fk_parent_table_1',
+ {
+ column: 'parent_id_with_different_column',
+ on_delete: :async_nullify,
+ gitlab_schema: :gitlab_main
+ }
+ )
+ ],
+ '_test_loose_fk_parent_table_2' => [
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ '_test_loose_fk_child_table_2_1',
+ '_test_loose_fk_parent_table_2',
+ {
+ column: 'parent_id',
+ on_delete: :async_delete,
+ gitlab_schema: :gitlab_main
+ }
+ )
+ ]
+ }
+ end
+
+ let(:connection) { ::ApplicationRecord.connection }
+
+ let(:loose_fk_parent_table_1) { table(:_test_loose_fk_parent_table_1) }
+ let(:loose_fk_parent_table_2) { table(:_test_loose_fk_parent_table_2) }
+ let(:loose_fk_child_table_1_1) { table(:_test_loose_fk_child_table_1_1) }
+ let(:loose_fk_child_table_1_2) { table(:_test_loose_fk_child_table_1_2) }
+ let(:loose_fk_child_table_2_1) { table(:_test_loose_fk_child_table_2_1) }
+
+ before(:all) do
+ create_table_structure
+ end
+
+ after(:all) do
+ migration = ActiveRecord::Migration.new
+
+ migration.drop_table :_test_loose_fk_parent_table_1
+ migration.drop_table :_test_loose_fk_parent_table_2
+ migration.drop_table :_test_loose_fk_child_table_1_1
+ migration.drop_table :_test_loose_fk_child_table_1_2
+ migration.drop_table :_test_loose_fk_child_table_2_1
+ end
+
+ before do
+ allow(Gitlab::Database::LooseForeignKeys).to receive(:definitions_by_table)
+ .and_return(all_loose_foreign_key_definitions)
+
+ parent_record_1 = loose_fk_parent_table_1.create!
+ loose_fk_child_table_1_1.create!(parent_id: parent_record_1.id)
+ loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_1.id)
+
+ parent_record_2 = loose_fk_parent_table_1.create!
+ 2.times { loose_fk_child_table_1_1.create!(parent_id: parent_record_2.id) }
+ 3.times { loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_2.id) }
+
+ parent_record_3 = loose_fk_parent_table_2.create!
+ 5.times { loose_fk_child_table_2_1.create!(parent_id: parent_record_3.id) }
+
+ loose_fk_parent_table_1.delete_all
+ loose_fk_parent_table_2.delete_all
+ end
+
+ describe '#execute' do
+ def execute
+ ::Gitlab::Database::SharedModel.using_connection(connection) do
+ described_class.new(connection: connection).execute
+ end
+ end
+
+ it 'cleans up all rows' do
+ execute
+
+ expect(loose_fk_child_table_1_1.count).to eq(0)
+ expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
+ expect(loose_fk_child_table_2_1.count).to eq(0)
+ end
+
+ it 'returns stats for records cleaned up' do
+ stats = execute
+
+ expect(stats[:delete_count]).to eq(8)
+ expect(stats[:update_count]).to eq(4)
+ end
+
+ it 'records the Apdex as success: true' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex)
+ .with(success: true, db_config_name: 'main')
+
+ execute
+ end
+
+ it 'records the error rate as error: false' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate)
+ .with(error: false, db_config_name: 'main')
+
+ execute
+ end
+
+ context 'when the amount of records to clean up exceeds BATCH_SIZE' do
+ before do
+ stub_const('LooseForeignKeys::CleanupWorker::BATCH_SIZE', 2)
+ end
+
+ it 'cleans up everything over multiple batches' do
+ expect(LooseForeignKeys::BatchCleanerService).to receive(:new).exactly(:twice).and_call_original
+
+ execute
+
+ expect(loose_fk_child_table_1_1.count).to eq(0)
+ expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
+ expect(loose_fk_child_table_2_1.count).to eq(0)
+ end
+ end
+
+ context 'when the amount of records to clean up exceeds the total MAX_DELETES' do
+ def count_deletable_rows
+ loose_fk_child_table_1_1.count + loose_fk_child_table_2_1.count
+ end
+
+ before do
+ stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 2)
+ stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1)
+ end
+
+ it 'cleans up MAX_DELETES and leaves the rest for the next run' do
+ expect { execute }.to change { count_deletable_rows }.by(-2)
+ expect(count_deletable_rows).to be > 0
+ end
+
+ it 'records the Apdex as success: false' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex)
+ .with(success: false, db_config_name: 'main')
+
+ execute
+ end
+ end
+
+ context 'when cleanup raises an error' do
+ before do
+ expect_next_instance_of(::LooseForeignKeys::BatchCleanerService) do |service|
+ allow(service).to receive(:execute).and_raise("Something broke")
+ end
+ end
+
+ it 'records the error rate as error: true and does not increment apdex' do
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate)
+ .with(error: true, db_config_name: 'main')
+ expect(::Gitlab::Metrics::LooseForeignKeysSlis).not_to receive(:record_apdex)
+
+ expect { execute }.to raise_error("Something broke")
+ end
+ end
+ end
+end
diff --git a/spec/services/markup/rendering_service_spec.rb b/spec/services/markup/rendering_service_spec.rb
new file mode 100644
index 00000000000..a5711a8cbc4
--- /dev/null
+++ b/spec/services/markup/rendering_service_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Markup::RenderingService do
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) do
+ user = create(:user, username: 'gfm')
+ project.add_maintainer(user)
+ user
+ end
+
+ let_it_be(:context) { { project: project } }
+ let_it_be(:postprocess_context) { { current_user: user } }
+
+ let(:file_name) { nil }
+ let(:text) { 'Noël' }
+
+ subject do
+ described_class
+ .new(text, file_name: file_name, context: context, postprocess_context: postprocess_context)
+ .execute
+ end
+
+ context 'when text is missing' do
+ let(:text) { nil }
+
+ it 'returns an empty string' do
+ is_expected.to eq('')
+ end
+ end
+
+ context 'when file_name is missing' do
+ it 'returns html (rendered by Banzai)' do
+ expected_html = '<p data-sourcepos="1:1-1:5" dir="auto">Noël</p>'
+
+ expect(Banzai).to receive(:render).with(text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when postprocess_context is missing' do
+ let(:file_name) { 'foo.txt' }
+ let(:postprocess_context) { nil }
+
+ it 'returns html (rendered by Banzai)' do
+ expected_html = '<pre class="plain-readme">Noël</pre>'
+
+ expect(Banzai).not_to receive(:post_process) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when rendered context is present' do
+ let(:rendered) { 'rendered text' }
+ let(:file_name) { 'foo.md' }
+
+ it 'returns an empty string' do
+ context[:rendered] = rendered
+
+ is_expected.to eq(rendered)
+ end
+ end
+
+ context 'when file is a markdown file' do
+ let(:file_name) { 'foo.md' }
+
+ it 'returns html (rendered by Banzai)' do
+ expected_html = '<p data-sourcepos="1:1-1:5" dir="auto">Noël</p>'
+
+ expect(Banzai).to receive(:render).with(text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+
+ context 'when renderer returns an error' do
+ before do
+ allow(Banzai).to receive(:render).and_raise(StandardError, "An error")
+ end
+
+ it 'returns html (rendered by ActionView:TextHelper)' do
+ is_expected.to eq('<p>Noël</p>')
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(StandardError),
+ project_id: context[:project].id, file_name: 'foo.md'
+ )
+
+ subject
+ end
+ end
+ end
+
+ context 'when file is asciidoc file' do
+ let(:file_name) { 'foo.adoc' }
+
+ it 'returns html (rendered by Gitlab::Asciidoc)' do
+ expected_html = "<div>\n<p>Noël</p>\n</div>"
+
+ expect(Gitlab::Asciidoc).to receive(:render).with(text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when file is a regular text file' do
+ let(:file_name) { 'foo.txt' }
+
+ it 'returns html (rendered by ActionView::TagHelper)' do
+ is_expected.to eq('<pre class="plain-readme">Noël</pre>')
+ end
+ end
+
+ context 'when file has an unknown type' do
+ let(:file_name) { 'foo.tex' }
+
+ it 'returns html (rendered by Gitlab::OtherMarkup)' do
+ expected_html = 'Noël'
+
+ expect(Gitlab::OtherMarkup).to receive(:render).with(file_name, text, context) { expected_html }
+
+ is_expected.to eq(expected_html)
+ end
+ end
+
+ context 'when rendering takes too long' do
+ let(:file_name) { 'foo.bar' }
+
+ before do
+ stub_const("Markup::RenderingService::RENDER_TIMEOUT", 0.1)
+ allow(Gitlab::OtherMarkup).to receive(:render) do
+ sleep(0.2)
+ text
+ end
+ end
+
+ it 'times out' do
+ expect(Gitlab::RenderTimeout).to receive(:timeout).and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(Timeout::Error),
+ project_id: context[:project].id, file_name: file_name
+ )
+
+ is_expected.to eq("<p>#{text}</p>")
+ end
+
+ context 'when markup_rendering_timeout is disabled' do
+ it 'waits until the execution completes' do
+ stub_feature_flags(markup_rendering_timeout: false)
+
+ expect(Gitlab::RenderTimeout).not_to receive(:timeout)
+
+ is_expected.to eq(text)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 8559c02be57..d0f009f1321 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -77,7 +77,7 @@ RSpec.describe Members::DestroyService do
end
end
- shared_examples 'a service destroying an access requester' do
+ shared_examples 'a service destroying an access request of another user' do
it_behaves_like 'a service destroying a member'
it 'calls Member#after_decline_request' do
@@ -85,12 +85,16 @@ RSpec.describe Members::DestroyService do
described_class.new(current_user).execute(member, **opts)
end
+ end
+
+ shared_examples 'a service destroying an access request of self' do
+ it_behaves_like 'a service destroying a member'
context 'when current user is the member' do
it 'does not call Member#after_decline_request' do
expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
- described_class.new(member_user).execute(member, **opts)
+ described_class.new(current_user).execute(member, **opts)
end
end
end
@@ -277,11 +281,24 @@ RSpec.describe Members::DestroyService do
group.add_owner(current_user)
end
- it_behaves_like 'a service destroying an access requester' do
+ it_behaves_like 'a service destroying an access request of another user' do
+ let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service destroying an access request of another user' do
+ let(:member) { group.requesters.find_by(user_id: member_user.id) }
+ end
+ end
+
+ context 'on withdrawing their own access request' do
+ let(:opts) { { skip_subresources: true } }
+ let(:current_user) { member_user }
+
+ it_behaves_like 'a service destroying an access request of self' do
let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end
- it_behaves_like 'a service destroying an access requester' do
+ it_behaves_like 'a service destroying an access request of self' do
let(:member) { group.requesters.find_by(user_id: member_user.id) }
end
end
diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb
index 6dbe161ee02..23d4d671afc 100644
--- a/spec/services/members/invite_service_spec.rb
+++ b/spec/services/members/invite_service_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
let(:params) { {} }
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 }
+ subject(:result) { described_class.new(user, base_params.merge(params)).execute }
context 'when there is a valid member invited' do
let(:params) { { email: 'email@example.org' } }
@@ -393,7 +393,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
context 'when email is already a member with a user on the project' do
let!(:existing_member) { create(:project_member, :guest, project: project) }
- let(:params) { { email: "#{existing_member.user.email}", access_level: ProjectMember::MAINTAINER } }
+ let(:params) { { email: existing_member.user.email.to_s, access_level: ProjectMember::MAINTAINER } }
it 'allows re-invite of an already invited email and updates the access_level' do
expect { result }.not_to change(ProjectMember, :count)
@@ -403,7 +403,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_
context 'when email belongs to an existing user as a confirmed secondary email' do
let(:secondary_email) { create(:email, :confirmed, email: 'secondary@example.com', user: existing_member.user) }
- let(:params) { { email: "#{secondary_email.email}" } }
+ let(:params) { { email: secondary_email.email.to_s } }
it 'allows re-invite to an already invited email' do
expect_to_create_members(count: 0)
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index f919d6d1516..eb8fae03c39 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -3,18 +3,34 @@
require 'spec_helper'
RSpec.describe Members::UpdateService do
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
- let(:current_user) { create(:user) }
- let(:member_user) { create(:user) }
- let(:permission) { :update }
- let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) }
- let(:access_level) { Gitlab::Access::MAINTAINER }
- let(:params) do
- { access_level: access_level }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:member_user1) { create(:user) }
+ let_it_be(:member_user2) { create(:user) }
+ let_it_be(:member_users) { [member_user1, member_user2] }
+ let_it_be(:permission) { :update }
+ let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
+ let(:members) { source.members_and_requesters.where(user_id: member_users).to_a }
+ let(:update_service) { described_class.new(current_user, params) }
+ let(:params) { { access_level: access_level } }
+ let(:updated_members) do
+ result = subject
+ Array.wrap(result[:members] || result[:member])
end
- subject { described_class.new(current_user, params).execute(member, permission: permission) }
+ before do
+ member_users.first.tap do |member_user|
+ expires_at = 10.days.from_now
+ project.add_member(member_user, Gitlab::Access::DEVELOPER, expires_at: expires_at)
+ group.add_member(member_user, Gitlab::Access::DEVELOPER, expires_at: expires_at)
+ end
+
+ member_users[1..].each do |member_user|
+ project.add_developer(member_user)
+ group.add_developer(member_user)
+ end
+ end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
@@ -23,164 +39,326 @@ RSpec.describe Members::UpdateService do
end
end
- shared_examples 'a service updating a member' do
- it 'updates the member' do
- expect(TodosDestroyer::EntityLeaveWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name)
+ shared_examples 'current user cannot update the given members' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:source) { group }
+ end
+ end
+
+ shared_examples 'returns error status when params are invalid' do
+ let_it_be(:params) { { expires_at: 2.days.ago } }
- updated_member = subject.fetch(:member)
+ specify do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
- expect(updated_member).to be_valid
- expect(updated_member.access_level).to eq(access_level)
+ shared_examples 'a service updating members' do
+ it 'updates the members' do
+ new_access_levels = updated_members.map(&:access_level)
+
+ expect(updated_members).not_to be_empty
+ expect(updated_members).to all(be_valid)
+ expect(new_access_levels).to all(be access_level)
end
it 'returns success status' do
- result = subject.fetch(:status)
+ expect(subject.fetch(:status)).to eq(:success)
+ end
- expect(result).to eq(:success)
+ it 'invokes after_execute with correct args' do
+ members.each do |member|
+ expect(update_service).to receive(:after_execute).with(
+ action: permission,
+ old_access_level: member.human_access,
+ old_expiry: member.expires_at,
+ member: member
+ )
+ end
+
+ subject
end
- context 'when member is downgraded to guest' do
- shared_examples 'schedules to delete confidential todos' do
- it do
- expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
+ it 'authorization update callback is triggered' do
+ expect(members).to all(receive(:refresh_member_authorized_projects).once)
- updated_member = subject.fetch(:member)
+ subject
+ end
+
+ it 'does not enqueues todos for deletion' do
+ members.each do |member|
+ expect(TodosDestroyer::EntityLeaveWorker)
+ .not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name)
+ end
- expect(updated_member).to be_valid
- expect(updated_member.access_level).to eq(Gitlab::Access::GUEST)
+ subject
+ end
+
+ context 'when members are downgraded to guest' do
+ shared_examples 'schedules to delete confidential todos' do
+ it do
+ members.each do |member|
+ expect(TodosDestroyer::EntityLeaveWorker)
+ .to receive(:perform_in)
+ .with(Todo::WAIT_FOR_DELETE, member.user_id, member.source_id, source.class.name).once
+ end
+
+ new_access_levels = updated_members.map(&:access_level)
+ expect(updated_members).to all(be_valid)
+ expect(new_access_levels).to all(be Gitlab::Access::GUEST)
end
end
context 'with Gitlab::Access::GUEST level as a string' do
- let(:params) { { access_level: Gitlab::Access::GUEST.to_s } }
+ let_it_be(:params) { { access_level: Gitlab::Access::GUEST.to_s } }
it_behaves_like 'schedules to delete confidential todos'
end
context 'with Gitlab::Access::GUEST level as an integer' do
- let(:params) { { access_level: Gitlab::Access::GUEST } }
+ let_it_be(:params) { { access_level: Gitlab::Access::GUEST } }
it_behaves_like 'schedules to delete confidential todos'
end
end
context 'when access_level is invalid' do
- let(:params) { { access_level: 'invalid' } }
+ let_it_be(:params) { { access_level: 'invalid' } }
it 'raises an error' do
- expect { described_class.new(current_user, params) }.to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
+ expect { described_class.new(current_user, params) }
+ .to raise_error(ArgumentError, 'invalid value for Integer(): "invalid"')
end
end
- context 'when member is not valid' do
- let(:params) { { expires_at: 2.days.ago } }
+ context 'when members update results in no change' do
+ let(:params) { { access_level: members.first.access_level } }
- it 'returns error status' do
- result = subject
+ it 'does not invoke update! and post_update' do
+ expect(update_service).not_to receive(:save!)
+ expect(update_service).not_to receive(:post_update)
- expect(result[:status]).to eq(:error)
+ expect(subject[:status]).to eq(:success)
end
- end
- end
-
- before do
- project.add_developer(member_user)
- group.add_developer(member_user)
- end
- context 'when current user cannot update the given member' do
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { project }
- end
+ it 'authorization update callback is not triggered' do
+ members.each { |member| expect(member).not_to receive(:refresh_member_authorized_projects) }
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { group }
+ subject
+ end
end
end
- context 'when current user can update the given member' do
+ shared_examples 'updating a project' do
+ let_it_be(:group_project) { create(:project, group: create(:group)) }
+ let_it_be(:source) { group_project }
+
before do
- project.add_maintainer(current_user)
- group.add_owner(current_user)
+ member_users.each { |member_user| group_project.add_developer(member_user) }
end
- it_behaves_like 'a service updating a member' do
- let(:source) { project }
- end
+ context 'as a project maintainer' do
+ before do
+ group_project.add_maintainer(current_user)
+ end
- it_behaves_like 'a service updating a member' do
- let(:source) { group }
- end
- end
+ it_behaves_like 'a service updating members'
- context 'in a project' do
- let_it_be(:group_project) { create(:project, group: create(:group)) }
+ context 'when member update results in an error' do
+ it_behaves_like 'a service returning an error'
+ end
- let(:source) { group_project }
+ context 'and updating members to OWNER' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:access_level) { Gitlab::Access::OWNER }
+ end
+ end
- context 'a project maintainer' do
- before do
- group_project.add_maintainer(current_user)
+ context 'and updating themselves to OWNER' do
+ let(:members) { source.members_and_requesters.find_by!(user_id: current_user.id) }
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let_it_be(:access_level) { Gitlab::Access::OWNER }
+ end
end
- context 'cannot update a member to OWNER' do
+ context 'and downgrading members from OWNER' do
before do
- group_project.add_developer(member_user)
+ member_users.each { |member_user| group_project.add_owner(member_user) }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:access_level) { Gitlab::Access::OWNER }
+ let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
end
end
+ end
- context 'cannot update themselves to OWNER' do
- let(:member) { source.members_and_requesters.find_by!(user_id: current_user.id) }
+ context 'when current_user is considered an owner in the project via inheritance' do
+ before do
+ group_project.group.add_owner(current_user)
+ end
+ context 'and can update members to OWNER' do
before do
- group_project.add_developer(member_user)
+ member_users.each { |member_user| group_project.add_developer(member_user) }
end
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:access_level) { Gitlab::Access::OWNER }
+ it_behaves_like 'a service updating members' do
+ let_it_be(:access_level) { Gitlab::Access::OWNER }
end
end
- context 'cannot downgrade a member from OWNER' do
+ context 'and can downgrade members from OWNER' do
before do
- group_project.add_owner(member_user)
+ member_users.each { |member_user| group_project.add_owner(member_user) }
end
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:access_level) { Gitlab::Access::MAINTAINER }
+ it_behaves_like 'a service updating members' do
+ let_it_be(:access_level) { Gitlab::Access::MAINTAINER }
end
end
end
+ end
+
+ shared_examples 'updating a group' do
+ let_it_be(:source) { group }
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ it_behaves_like 'a service updating members'
+
+ context 'when member update results in an error' do
+ it_behaves_like 'a service returning an error'
+ end
+
+ context 'when group members expiration date is updated' do
+ let_it_be(:params) { { expires_at: 20.days.from_now } }
+ let(:notification_service) { instance_double(NotificationService) }
- context 'owners' do
before do
- # so that `current_user` is considered an `OWNER` in the project via inheritance.
- group_project.group.add_owner(current_user)
+ allow(NotificationService).to receive(:new).and_return(notification_service)
end
- context 'can update a member to OWNER' do
- before do
- group_project.add_developer(member_user)
+ it 'emails the users that their group membership expiry has changed' do
+ members.each do |member|
+ expect(notification_service).to receive(:updated_group_member_expiration).with(member)
end
- it_behaves_like 'a service updating a member' do
- let(:access_level) { Gitlab::Access::OWNER }
- end
+ subject
end
+ end
+ end
- context 'can downgrade a member from OWNER' do
- before do
- group_project.add_owner(member_user)
+ context 'when :bulk_update_membership_roles feature flag is disabled' do
+ let(:member) { source.members_and_requesters.find_by!(user_id: member_user1.id) }
+ let(:members) { [member] }
+
+ subject { update_service.execute(member, permission: permission) }
+
+ shared_examples 'a service returning an error' do
+ before do
+ allow(member).to receive(:save) do
+ member.errors.add(:user_id)
+ member.errors.add(:access_level)
end
+ .and_return(false)
+ end
+
+ it_behaves_like 'returns error status when params are invalid'
+
+ it 'returns the error' do
+ response = subject
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:message]).to eq('User is invalid and Access level is invalid')
+ end
+ end
+
+ before do
+ stub_feature_flags(bulk_update_membership_roles: false)
+ end
+
+ it_behaves_like 'current user cannot update the given members'
+ it_behaves_like 'updating a project'
+ it_behaves_like 'updating a group'
+ end
+
+ subject { update_service.execute(members, permission: permission) }
+
+ shared_examples 'a service returning an error' do
+ it_behaves_like 'returns error status when params are invalid'
- it_behaves_like 'a service updating a member' do
- let(:access_level) { Gitlab::Access::MAINTAINER }
+ context 'when a member update results in invalid record' do
+ let(:member2) { members.second }
+
+ before do
+ allow(member2).to receive(:save!) do
+ member2.errors.add(:user_id)
+ member2.errors.add(:access_level)
+ end.and_raise(ActiveRecord::RecordInvalid)
+ end
+
+ it 'returns the error' do
+ response = subject
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:message]).to eq('User is invalid and Access level is invalid')
+ end
+
+ it 'rollbacks back the entire update' do
+ old_access_levels = members.pluck(:access_level)
+
+ subject
+
+ expect(members.each(&:reset).pluck(:access_level)).to eq(old_access_levels)
+ end
+ end
+ end
+
+ it_behaves_like 'current user cannot update the given members'
+ it_behaves_like 'updating a project'
+ it_behaves_like 'updating a group'
+
+ context 'with a single member' do
+ let(:member) { create(:group_member, group: group) }
+ let(:members) { member }
+
+ before do
+ group.add_owner(current_user)
+ end
+
+ it 'returns the correct response' do
+ expect(subject[:member]).to eq(member)
+ end
+ end
+
+ context 'when current user is an admin', :enable_admin_mode do
+ let_it_be(:current_user) { create(:admin) }
+ let_it_be(:source) { group }
+
+ context 'when all owners are being downgraded' do
+ before do
+ member_users.each { |member_user| group.add_owner(member_user) }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
+ end
+
+ context 'when all blocked owners are being downgraded' do
+ before do
+ member_users.each do |member_user|
+ group.add_owner(member_user)
+ member_user.block
end
end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError'
end
end
end
diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb
index 0846ec7f50e..da6492aca95 100644
--- a/spec/services/merge_requests/approval_service_spec.rb
+++ b/spec/services/merge_requests/approval_service_spec.rb
@@ -33,9 +33,17 @@ RSpec.describe MergeRequests::ApprovalService do
service.execute(merge_request)
end
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
it 'does not publish MergeRequests::ApprovedEvent' do
expect { service.execute(merge_request) }.not_to publish_event(MergeRequests::ApprovedEvent)
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
context 'with an already approved MR' do
@@ -46,6 +54,14 @@ RSpec.describe MergeRequests::ApprovalService do
it 'does not create an approval' do
expect { service.execute(merge_request) }.not_to change { merge_request.approvals.size }
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
context 'with valid approval' do
@@ -67,6 +83,14 @@ RSpec.describe MergeRequests::ApprovalService do
.to publish_event(MergeRequests::ApprovedEvent)
.with(current_user_id: user.id, merge_request_id: merge_request.id)
end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
context 'user cannot update the merge request' do
@@ -77,6 +101,14 @@ RSpec.describe MergeRequests::ApprovalService do
it 'does not update approvals' do
expect { service.execute(merge_request) }.not_to change { merge_request.approvals.size }
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { service.execute(merge_request) }
+ end
end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 6a6f01e6a95..4f27ff30da7 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -93,7 +93,7 @@ RSpec.describe MergeRequests::BuildService do
shared_examples 'with a Default.md template' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'the template description is preferred' do
expect(merge_request.description).to eq('Default template contents')
@@ -306,7 +306,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'appends the closing description to a Default.md template' do
expected_description = ['Default template contents', closing_message].compact.join("\n\n")
@@ -386,7 +386,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'keeps the description from the initial params' do
expect(merge_request.description).to eq(description)
@@ -425,7 +425,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'appends the closing description to a Default.md template' do
expected_description = ['Default template contents', closing_message].compact.join("\n\n")
@@ -486,7 +486,7 @@ RSpec.describe MergeRequests::BuildService do
context 'a Default.md template is defined' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'appends the closing description to a Default.md template' do
expected_description = ['Default template contents', closing_message].compact.join("\n\n")
@@ -715,7 +715,7 @@ RSpec.describe MergeRequests::BuildService do
context 'when a Default template is found' do
context 'when its contents cannot be retrieved' do
let(:files) { { '.gitlab/merge_request_templates/OtherTemplate.md' => 'Other template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'does not modify the merge request description' do
allow(TemplateFinder).to receive(:all_template_names).and_return({
@@ -732,7 +732,7 @@ RSpec.describe MergeRequests::BuildService do
context 'when its contents can be retrieved' do
let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } }
- let(:project) { create(:project, :custom_repo, files: files ) }
+ let(:project) { create(:project, :custom_repo, files: files) }
it 'modifies the merge request description' do
merge_request.description = nil
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 0bc8258af42..da8e8d944d6 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -336,6 +336,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
it_behaves_like 'reviewer_ids filter' do
let(:execute) { service.execute }
end
+
+ context 'when called in a transaction' do
+ it 'does not raise an error' do
+ expect { MergeRequest.transaction { described_class.new(project: project, current_user: user, params: opts).execute } }.not_to raise_error
+ end
+ end
end
it_behaves_like 'issuable record that supports quick actions' do
@@ -495,15 +501,40 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
project.add_developer(user)
end
- it 'creates the merge request', :sidekiq_might_not_need_inline do
- expect_next_instance_of(MergeRequest) do |instance|
- expect(instance).to receive(:eager_fetch_ref!).and_call_original
+ context 'when async_merge_request_diff_creation is enabled' do
+ before do
+ stub_feature_flags(async_merge_request_diff_creation: true)
end
- merge_request = described_class.new(project: project, current_user: user, params: opts).execute
+ it 'creates the merge request', :sidekiq_inline do
+ expect_next_instance_of(MergeRequest) do |instance|
+ expect(instance).not_to receive(:eager_fetch_ref!)
+ end
- expect(merge_request).to be_persisted
- expect(merge_request.iid).to be > 0
+ merge_request = described_class.new(project: project, current_user: user, params: opts).execute
+
+ expect(merge_request).to be_persisted
+ expect(merge_request.iid).to be > 0
+ expect(merge_request.merge_request_diff).not_to be_empty
+ end
+ end
+
+ context 'when async_merge_request_diff_creation is disabled' do
+ before do
+ stub_feature_flags(async_merge_request_diff_creation: false)
+ end
+
+ it 'creates the merge request' do
+ expect_next_instance_of(MergeRequest) do |instance|
+ expect(instance).to receive(:eager_fetch_ref!).and_call_original
+ end
+
+ merge_request = described_class.new(project: project, current_user: user, params: opts).execute
+
+ expect(merge_request).to be_persisted
+ expect(merge_request.iid).to be > 0
+ expect(merge_request.merge_request_diff).not_to be_empty
+ end
end
it 'does not create the merge request when the target project is archived' do
diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
index cf34923795e..c56b38bccc1 100644
--- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
+++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequests::Mergeability::RunChecksService do
+RSpec.describe MergeRequests::Mergeability::RunChecksService, :clean_gitlab_redis_cache do
subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) }
describe '#execute' do
@@ -104,18 +104,6 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(execute.success?).to eq(true)
end
end
-
- context 'when mergeability_caching is turned off' do
- before do
- stub_feature_flags(mergeability_caching: false)
- end
-
- it 'does not call the results store' do
- expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new)
-
- expect(execute.success?).to eq(true)
- end
- end
end
end
@@ -161,11 +149,11 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
let_it_be(:merge_request) { create(:merge_request) }
context 'when the execute method has been executed' do
- before do
- run_checks.execute
- end
-
context 'when all the checks succeed' do
+ before do
+ run_checks.execute
+ end
+
it 'returns nil' do
expect(failure_reason).to eq(nil)
end
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
index c24b83e21a6..ee23238314e 100644
--- a/spec/services/merge_requests/mergeability_check_service_spec.rb
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -190,14 +190,6 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
target_branch: 'conflict-start')
end
- it 'does not change the merge ref HEAD' do
- expect(merge_request.merge_ref_head).to be_nil
-
- subject
-
- expect(merge_request.reload.merge_ref_head).not_to be_nil
- end
-
it 'returns ServiceResponse.error and keeps merge status as cannot_be_merged' do
result = subject
@@ -351,27 +343,5 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
end
end
end
-
- context 'merge with conflicts' do
- it 'calls MergeToRefService with true allow_conflicts param' do
- expect(MergeRequests::MergeToRefService).to receive(:new)
- .with(project: project, current_user: merge_request.author, params: { allow_conflicts: true }).and_call_original
-
- subject
- end
-
- context 'when display_merge_conflicts_in_diff is disabled' do
- before do
- stub_feature_flags(display_merge_conflicts_in_diff: false)
- end
-
- it 'calls MergeToRefService with false allow_conflicts param' do
- expect(MergeRequests::MergeToRefService).to receive(:new)
- .with(project: project, current_user: merge_request.author, params: { allow_conflicts: false }).and_call_original
-
- subject
- end
- end
- end
end
end
diff --git a/spec/services/merge_requests/remove_approval_service_spec.rb b/spec/services/merge_requests/remove_approval_service_spec.rb
index 5a319e90a68..7b38f0d1c45 100644
--- a/spec/services/merge_requests/remove_approval_service_spec.rb
+++ b/spec/services/merge_requests/remove_approval_service_spec.rb
@@ -45,6 +45,14 @@ RSpec.describe MergeRequests::RemoveApprovalService do
execute!
end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { execute! }
+ end
+
+ it_behaves_like 'triggers GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { execute! }
+ end
end
context 'with a user who has not approved' do
@@ -61,6 +69,14 @@ RSpec.describe MergeRequests::RemoveApprovalService do
execute!
end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ let(:action) { execute! }
+ end
+
+ it_behaves_like 'does not trigger GraphQL subscription mergeRequestReviewersUpdated' do
+ let(:action) { execute! }
+ end
end
end
end
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
index 9210242a11e..471bb03f18c 100644
--- a/spec/services/merge_requests/squash_service_spec.rb
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe MergeRequests::SquashService do
- include GitHelpers
-
let(:service) { described_class.new(project: project, current_user: user, params: { merge_request: merge_request }) }
let(:user) { project.first_owner }
let(:project) { create(:project, :repository) }
@@ -109,11 +107,10 @@ RSpec.describe MergeRequests::SquashService do
end
it 'has the same diff as the merge request, but a different SHA' do
- rugged = rugged_repo(project.repository)
- mr_diff = rugged.diff(merge_request.diff_base_sha, merge_request.diff_head_sha)
- squash_diff = rugged.diff(merge_request.diff_start_sha, squash_sha)
+ mr_diff = project.repository.diff(merge_request.diff_base_sha, merge_request.diff_head_sha)
+ squash_diff = project.repository.diff(merge_request.diff_start_sha, squash_sha)
- expect(squash_diff.patch.length).to eq(mr_diff.patch.length)
+ expect(squash_diff.size).to eq(mr_diff.size)
expect(squash_commit.sha).not_to eq(merge_request.diff_head_sha)
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 1d67574b06d..da78f86c7c8 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -794,7 +794,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
it "does not try to mark as unchecked if it's already unchecked" do
- expect(merge_request).to receive(:unchecked?).and_return(true)
+ allow(merge_request).to receive(:unchecked?).twice.and_return(true)
expect(merge_request).not_to receive(:mark_as_unchecked)
update_merge_request({ target_branch: "target" })
@@ -1148,7 +1148,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
end
- include_examples 'issuable update service' do
+ it_behaves_like 'issuable update service' do
let(:open_issuable) { merge_request }
let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
end
diff --git a/spec/services/milestones/transfer_service_spec.rb b/spec/services/milestones/transfer_service_spec.rb
index b15d90d685c..de02226661c 100644
--- a/spec/services/milestones/transfer_service_spec.rb
+++ b/spec/services/milestones/transfer_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Milestones::TransferService do
let(:new_group) { create(:group) }
let(:old_group) { create(:group) }
let(:project) { create(:project, namespace: old_group) }
- let(:group_milestone) { create(:milestone, group: old_group) }
+ let(:group_milestone) { create(:milestone, :closed, group: old_group) }
let(:group_milestone2) { create(:milestone, group: old_group) }
let(:project_milestone) { create(:milestone, project: project) }
let!(:issue_with_group_milestone) { create(:issue, project: project, milestone: group_milestone) }
@@ -38,6 +38,7 @@ RSpec.describe Milestones::TransferService do
expect(new_milestone).not_to eq(group_milestone)
expect(new_milestone.title).to eq(group_milestone.title)
expect(new_milestone.project_milestone?).to be_truthy
+ expect(new_milestone.state).to eq("closed")
end
context 'when milestone is from an ancestor group' do
@@ -88,6 +89,7 @@ RSpec.describe Milestones::TransferService do
expect(new_milestone).not_to eq(group_milestone)
expect(new_milestone.title).to eq(group_milestone.title)
expect(new_milestone.project_milestone?).to be_truthy
+ expect(new_milestone.state).to eq("closed")
end
it 'does not apply new project milestone to issuables with project milestone' do
diff --git a/spec/services/namespaces/statistics_refresher_service_spec.rb b/spec/services/namespaces/statistics_refresher_service_spec.rb
index d3379e843ec..2d5f9235bd4 100644
--- a/spec/services/namespaces/statistics_refresher_service_spec.rb
+++ b/spec/services/namespaces/statistics_refresher_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Namespaces::StatisticsRefresherService, '#execute' do
let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
let(:projects) { create_list(:project, 5, namespace: group) }
let(:service) { described_class.new }
@@ -23,6 +24,14 @@ RSpec.describe Namespaces::StatisticsRefresherService, '#execute' do
service.execute(group)
end
+
+ context 'when given a subgroup' do
+ it 'does not create statistics for the subgroup' do
+ service.execute(subgroup)
+
+ expect(subgroup.reload.root_storage_statistics).not_to be_present
+ end
+ end
end
context 'with a root storage statistics relation', :sidekiq_might_not_need_inline do
@@ -43,6 +52,16 @@ RSpec.describe Namespaces::StatisticsRefresherService, '#execute' do
service.execute(group)
end
+
+ context 'when given a subgroup' do
+ it "recalculates the root namespace's statistics" do
+ expect(Namespace::RootStorageStatistics)
+ .to receive(:safe_find_or_create_by!).with({ namespace_id: group.id })
+ .and_return(group.root_storage_statistics)
+
+ service.execute(subgroup)
+ end
+ end
end
context 'when something goes wrong' do
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index c25895d2efa..67d8b37f809 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -189,13 +189,13 @@ RSpec.describe Notes::BuildService do
context 'issuable author' do
let(:user) { noteable_author }
- it_behaves_like 'user allowed to set comment as confidential'
+ it_behaves_like 'user not allowed to set comment as confidential'
end
context 'issuable assignee' do
let(:user) { issuable_assignee }
- it_behaves_like 'user allowed to set comment as confidential'
+ it_behaves_like 'user not allowed to set comment as confidential'
end
context 'admin' do
@@ -265,13 +265,13 @@ RSpec.describe Notes::BuildService do
context 'with noteable author' do
let(:user) { note.noteable.author }
- it_behaves_like 'confidential set to `true`'
+ it_behaves_like 'returns `Discussion to reply to cannot be found` error'
end
context 'with noteable assignee' do
let(:user) { issuable_assignee }
- it_behaves_like 'confidential set to `true`'
+ it_behaves_like 'returns `Discussion to reply to cannot be found` error'
end
context 'with guest access' do
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index 989ca7b8df1..05703ac548d 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -245,7 +245,7 @@ RSpec.describe Notes::UpdateService do
context 'for a personal snippet' do
let_it_be(:snippet) { create(:personal_snippet, :public) }
- let(:note) { create(:note, project: nil, noteable: snippet, author: user, note: "Note on a snippet with reference #{issue.to_reference}" ) }
+ let(:note) { create(:note, project: nil, noteable: snippet, author: user, note: "Note on a snippet with reference #{issue.to_reference}") }
it 'does not create todos' do
expect { update_note({ note: "Mentioning user #{user2}" }) }.not_to change { note.todos.count }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 8fbf023cda0..7857bd2263f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -211,6 +211,23 @@ RSpec.describe NotificationService, :mailer do
it_behaves_like 'participating by assignee notification'
end
+ describe '.permitted_actions' do
+ it 'includes public methods' do
+ expect(described_class.permitted_actions).to include(:access_token_created)
+ end
+
+ it 'excludes EXCLUDED_ACTIONS' do
+ described_class::EXCLUDED_ACTIONS.each do |action|
+ expect(described_class.permitted_actions).not_to include(action)
+ end
+ end
+
+ it 'excludes protected and private methods' do
+ expect(described_class.permitted_actions).not_to include(:new_resource_email)
+ expect(described_class.permitted_actions).not_to include(:approve_mr_email)
+ end
+ end
+
describe '#async' do
let(:async) { notification.async }
diff --git a/spec/services/packages/composer/composer_json_service_spec.rb b/spec/services/packages/composer/composer_json_service_spec.rb
index 378016a6ffb..d2187688c4c 100644
--- a/spec/services/packages/composer/composer_json_service_spec.rb
+++ b/spec/services/packages/composer/composer_json_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Packages::Composer::ComposerJsonService do
subject { described_class.new(project, target).execute }
context 'with an existing file' do
- let(:project) { create(:project, :custom_repo, files: { 'composer.json' => json } ) }
+ let(:project) { create(:project, :custom_repo, files: { 'composer.json' => json }) }
context 'with a valid file' do
let(:json) { '{ "name": "package-name"}' }
diff --git a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
index 39c6feb5d12..70c2bbad87a 100644
--- a/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
+++ b/spec/services/packages/maven/metadata/create_versions_xml_service_spec.rb
@@ -65,6 +65,23 @@ RSpec.describe ::Packages::Maven::Metadata::CreateVersionsXmlService do
let(:versions_in_database) { versions_in_xml + additional_versions }
it_behaves_like 'returning an xml with versions in the database'
+
+ context 'with an xml without a release version' do
+ let(:version_release) { nil }
+
+ it_behaves_like 'returning an xml with versions in the database'
+
+ it 'logs a warn with the reason' do
+ expect(Gitlab::AppJsonLogger).to receive(:warn).with(
+ message: 'A malformed metadata file has been encountered',
+ reason: 'Missing release tag',
+ project_id: package.project_id,
+ package_id: package.id
+ )
+
+ subject
+ end
+ end
end
end
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index a3e59913918..ef8cdf2e8ab 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -148,7 +148,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
context 'when file size is faked by setting the attachment length param to a lower size' do
- let(:params) { super().deep_merge!( { _attachments: { "#{package_name}-#{version}.tgz" => { data: encoded_package_data, length: 1 } } }) }
+ let(:params) { super().deep_merge!({ _attachments: { "#{package_name}-#{version}.tgz" => { data: encoded_package_data, length: 1 } } }) }
# TODO (technical debt): Extract the package size calculation outside the service and add separate specs for it.
# Right now we have several contexts here to test the calculation's different scenarios.
@@ -193,7 +193,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
context 'with empty versions' do
- let(:params) { super().merge!({ versions: {} } ) }
+ let(:params) { super().merge!({ versions: {} }) }
it { expect(subject[:http_status]).to eq 400 }
it { expect(subject[:message]).to eq 'Version is empty.' }
diff --git a/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb b/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb
deleted file mode 100644
index 524c224177b..00000000000
--- a/spec/services/packages/rpm/repository_metadata/base_builder_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BaseBuilder do
- describe '#execute' do
- subject { described_class.new(xml: xml, data: data).execute }
-
- let(:xml) { nil }
- let(:data) { {} }
-
- before do
- stub_const("#{described_class}::ROOT_TAG", 'test')
- stub_const("#{described_class}::ROOT_ATTRIBUTES", { foo1: 'bar1', foo2: 'bar2' })
- end
-
- it 'generate valid xml' do
- result = Nokogiri::XML::Document.parse(subject)
-
- expect(result.children.count).to eq(1)
- expect(result.children.first.attributes.count).to eq(2)
- expect(result.children.first.attributes['foo1'].value).to eq('bar1')
- expect(result.children.first.attributes['foo2'].value).to eq('bar2')
- end
-
- context 'when call with parameters' do
- let(:xml) { 'test' }
-
- it 'raise NotImplementedError' do
- expect { subject }.to raise_error NotImplementedError
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb
new file mode 100644
index 00000000000..d93d6ab9fcb
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_service_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXmlService do
+ describe '#execute' do
+ subject { described_class.new(data).execute }
+
+ include_context 'with rpm package data'
+
+ let(:data) { xml_update_params }
+ let(:file_xpath) { "//package/file" }
+
+ it 'adds all file nodes' do
+ result = subject
+
+ expect(result.xpath(file_xpath).count).to eq(data[:files].count)
+ end
+
+ describe 'setting type attribute' do
+ context 'when all files are directories' do
+ let(:dirs) do
+ 3.times.map { generate_directory } # rubocop:disable Performance/TimesMap
+ end
+
+ let(:files) do
+ 5.times.map { FFaker::Filesystem.file_name(dirs.sample) } # rubocop:disable Performance/TimesMap
+ end
+
+ let(:data) do
+ {
+ directories: dirs.map { "#{_1}/" }, # Add trailing slash as in original package
+ files: dirs + files
+ }
+ end
+
+ it 'set dir type attribute for directories only' do
+ result = subject
+
+ result.xpath(file_xpath).each do |tag|
+ if dirs.include?(tag.content)
+ expect(tag.attributes['type']&.value).to eq('dir')
+ else
+ expect(tag.attributes['type']).to be_nil
+ end
+ end
+ end
+ end
+
+ def generate_directory
+ FFaker::Lorem.words(3).join('/')
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb
deleted file mode 100644
index 2feb44c7c1b..00000000000
--- a/spec/services/packages/rpm/repository_metadata/build_filelist_xml_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildFilelistXml do
- describe '#execute' do
- subject { described_class.new.execute }
-
- context "when generate empty xml" do
- let(:expected_xml) do
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <filelists xmlns="http://linux.duke.edu/metadata/filelists" packages="0"/>
- XML
- end
-
- it 'generate expected xml' do
- expect(subject).to eq(expected_xml)
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb
new file mode 100644
index 00000000000..201f9e67ce9
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_other_xml_service_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXmlService do
+ describe '#execute' do
+ subject { described_class.new(data).execute }
+
+ include_context 'with rpm package data'
+
+ let(:data) { xml_update_params }
+ let(:changelog_xpath) { "//package/changelog" }
+
+ it 'adds all changelog nodes' do
+ result = subject
+
+ expect(result.xpath(changelog_xpath).count).to eq(data[:changelogs].count)
+ end
+
+ it 'set required date attribute' do
+ result = subject
+
+ data[:changelogs].each do |changelog|
+ expect(result.at("#{changelog_xpath}[@date=\"#{changelog[:changelogtime]}\"]")).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb
deleted file mode 100644
index 823aa18808a..00000000000
--- a/spec/services/packages/rpm/repository_metadata/build_other_xml_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildOtherXml do
- describe '#execute' do
- subject { described_class.new.execute }
-
- context "when generate empty xml" do
- let(:expected_xml) do
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <otherdata xmlns="http://linux.duke.edu/metadata/other" packages="0"/>
- XML
- end
-
- it 'generate expected xml' do
- expect(subject).to eq(expected_xml)
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb
new file mode 100644
index 00000000000..9bbfa5c9863
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/build_primary_xml_service_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXmlService do
+ describe '#execute' do
+ subject { described_class.new(data).execute }
+
+ include_context 'with rpm package data'
+
+ let(:data) { xml_update_params }
+ let(:required_text_only_attributes) { %i[description summary arch name] }
+
+ it 'adds node with required_text_only_attributes' do
+ result = subject
+
+ required_text_only_attributes.each do |attribute|
+ expect(
+ result.at("//package/#{attribute}").text
+ ).to eq(data[attribute])
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb
deleted file mode 100644
index 147d5862a71..00000000000
--- a/spec/services/packages/rpm/repository_metadata/build_primary_xml_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildPrimaryXml do
- describe '#execute' do
- subject { described_class.new(xml: xml, data: data).execute }
-
- let(:empty_xml) do
- <<~XML
- <?xml version="1.0" encoding="UTF-8"?>
- <metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"/>
- XML
- end
-
- it_behaves_like 'handling rpm xml file'
-
- context 'when updating existing xml' do
- include_context 'with rpm package data'
-
- let(:xml) { empty_xml }
- let(:data) { xml_update_params }
- let(:required_text_only_attributes) { %i[description summary arch name] }
-
- it 'adds node with required_text_only_attributes' do
- result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
-
- required_text_only_attributes.each do |attribute|
- expect(
- result.at("//#{described_class::ROOT_TAG}/package/#{attribute}").text
- ).to eq(data[attribute])
- end
- end
- end
- end
-end
diff --git a/spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb
index 0843a983b7e..cf28301fa2c 100644
--- a/spec/services/packages/rpm/repository_metadata/build_repomd_xml_spec.rb
+++ b/spec/services/packages/rpm/repository_metadata/build_repomd_xml_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXml do
+RSpec.describe Packages::Rpm::RepositoryMetadata::BuildRepomdXmlService do
describe '#execute' do
subject { described_class.new(data).execute }
diff --git a/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb b/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb
new file mode 100644
index 00000000000..e351392ba1c
--- /dev/null
+++ b/spec/services/packages/rpm/repository_metadata/update_xml_service_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Packages::Rpm::RepositoryMetadata::UpdateXmlService do
+ describe '#execute' do
+ subject { described_class.new(filename: filename, xml: xml, data: data).execute }
+
+ let(:xml) { nil }
+ let(:data) { nil }
+
+ shared_examples 'handling not implemented xml filename' do
+ let(:filename) { :not_implemented_yet }
+ let(:empty_xml) { '' }
+
+ it 'raise error' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+
+ shared_context 'with primary xml file data' do
+ let(:filename) { :primary }
+ let(:empty_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"/>
+ XML
+ end
+ end
+
+ shared_context 'with other xml file data' do
+ let(:filename) { :other }
+ let(:empty_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <otherdata xmlns="http://linux.duke.edu/metadata/other" packages="0"/>
+ XML
+ end
+ end
+
+ shared_context 'with filelist xml file data' do
+ let(:filename) { :filelist }
+ let(:empty_xml) do
+ <<~XML
+ <?xml version="1.0" encoding="UTF-8"?>
+ <filelists xmlns="http://linux.duke.edu/metadata/filelists" packages="0"/>
+ XML
+ end
+ end
+
+ context 'when building empty xml' do
+ shared_examples 'generating empty xml' do
+ it 'generate expected xml' do
+ expect(subject).to eq(empty_xml)
+ end
+ end
+
+ it_behaves_like 'handling not implemented xml filename'
+
+ context "for 'primary' xml file" do
+ include_context 'with primary xml file data'
+
+ it_behaves_like 'generating empty xml'
+ end
+
+ context "for 'other' xml file" do
+ include_context 'with other xml file data'
+
+ it_behaves_like 'generating empty xml'
+ end
+
+ context "for 'filelist' xml file" do
+ include_context 'with filelist xml file data'
+
+ it_behaves_like 'generating empty xml'
+ end
+ end
+
+ context 'when updating xml file' do
+ include_context 'with rpm package data'
+
+ let(:xml) { empty_xml }
+ let(:data) { xml_update_params }
+ let(:builder_class) { described_class::BUILDERS[filename] }
+
+ shared_examples 'updating rpm xml file' do
+ context 'when updating existing xml' do
+ shared_examples 'changing root tag attribute' do
+ it "increment previous 'packages' value by 1" do
+ previous_value = Nokogiri::XML(xml).at(builder_class::ROOT_TAG).attributes["packages"].value.to_i
+ new_value = Nokogiri::XML(subject).at(builder_class::ROOT_TAG).attributes["packages"].value.to_i
+
+ expect(previous_value + 1).to eq(new_value)
+ end
+ end
+
+ it 'generate valid xml add expected xml node to existing xml' do
+ # Have one root attribute
+ result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
+ expect(result.children.count).to eq(1)
+
+ # Root node has 1 child with generated node
+ expect(result.xpath("//#{builder_class::ROOT_TAG}/package").count).to eq(1)
+ end
+
+ context 'when empty xml' do
+ it_behaves_like 'changing root tag attribute'
+ end
+
+ context 'when xml has children' do
+ context "when node with given 'pkgid' does not exist yet" do
+ let(:uniq_node_data) do
+ xml_update_params.tap do |data|
+ data[:pkgid] = SecureRandom.uuid
+ end
+ end
+
+ let(:xml) { build_xml_from(uniq_node_data) }
+
+ it 'has children nodes' do
+ existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
+ expect(existing_xml.xpath('//package').count).to eq(1)
+ end
+
+ it_behaves_like 'changing root tag attribute'
+ end
+
+ context "when node with given 'pkgid' already exist" do
+ let(:existing_node_data) do
+ existing_data = data.dup
+ existing_data[:name] = FFaker::Lorem.word
+ existing_data
+ end
+
+ let(:xml) { build_xml_from(existing_node_data) }
+
+ it 'has children nodes' do
+ existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
+ expect(existing_xml.xpath('//package').count).to eq(1)
+ end
+
+ it 'replace existing node with new data' do
+ existing_xml = Nokogiri::XML::Document.parse(xml).remove_namespaces!
+ result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
+ expect(result.xpath('//package').count).to eq(1)
+ expect(result.xpath('//package').first.to_xml).not_to eq(existing_xml.xpath('//package').first.to_xml)
+ end
+ end
+
+ def build_xml_from(data)
+ described_class.new(filename: filename, xml: empty_xml, data: data).execute
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'handling not implemented xml filename'
+
+ context "for 'primary' xml file" do
+ include_context 'with primary xml file data'
+
+ it_behaves_like 'updating rpm xml file'
+ end
+
+ context "for 'other' xml file" do
+ include_context 'with other xml file data'
+
+ it_behaves_like 'updating rpm xml file'
+ end
+
+ context "for 'filelist' xml file" do
+ include_context 'with filelist xml file data'
+
+ it_behaves_like 'updating rpm xml file'
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 65da1976dc2..eb8d94ebfa5 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
- include GitHelpers
-
let(:gitlab_shell) { Gitlab::Shell.new }
let(:project) { create(:project, :legacy_storage, :repository, :wiki_repo, :design_repo) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index b67b4d64c1d..6c7164c5e06 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -126,7 +126,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do
let(:redirect_link) { 'http://external-link' }
before do
- stub_full_request(download_link).to_return(status: 301, body: 'You are being redirected', headers: { 'Location' => redirect_link } )
+ stub_full_request(download_link).to_return(status: 301, body: 'You are being redirected', headers: { 'Location' => redirect_link })
stub_full_request(redirect_link).to_return(body: lfs_content)
end
diff --git a/spec/services/projects/move_users_star_projects_service_spec.rb b/spec/services/projects/move_users_star_projects_service_spec.rb
index 0f766ebd0ec..b580d3d8772 100644
--- a/spec/services/projects/move_users_star_projects_service_spec.rb
+++ b/spec/services/projects/move_users_star_projects_service_spec.rb
@@ -15,6 +15,9 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
end
it 'moves the user\'s stars from one project to another' do
+ project_with_stars.reload
+ target_project.reload
+
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0
@@ -34,6 +37,8 @@ RSpec.describe Projects::MoveUsersStarProjectsService do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_stars) }.to raise_error(StandardError)
+ project_with_stars.reload
+ target_project.reload
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 7bf6dfd0fd8..43d23023d83 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -244,9 +244,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
shared_examples 'process truncated alerts' do
- it 'returns 200 but skips processing and logs a warning', :aggregate_failures do
+ it 'returns 201 but skips processing and logs a warning', :aggregate_failures do
expect(subject).to be_success
- expect(subject.payload[:alerts].size).to eq(max_alerts)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
expect(Gitlab::AppLogger)
.to have_received(:warn)
.with(
@@ -260,9 +261,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
shared_examples 'process all alerts' do
- it 'returns 200 and process alerts without warnings', :aggregate_failures do
+ it 'returns 201 and process alerts without warnings', :aggregate_failures do
expect(subject).to be_success
- expect(subject.payload[:alerts].size).to eq(2)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
expect(Gitlab::AppLogger).not_to have_received(:warn)
end
end
diff --git a/spec/services/protected_branches/api_service_spec.rb b/spec/services/protected_branches/api_service_spec.rb
new file mode 100644
index 00000000000..94484f5a7b9
--- /dev/null
+++ b/spec/services/protected_branches/api_service_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedBranches::ApiService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ it 'creates a protected branch with prefilled defaults' do
+ expect(::ProtectedBranches::CreateService).to receive(:new).with(
+ project, user, hash_including(
+ push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }],
+ merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }]
+ )
+ ).and_call_original
+
+ expect(described_class.new(project, user, { name: 'new name' }).create).to be_valid
+ end
+
+ it 'updates a protected branch without prefilled defaults' do
+ protected_branch = create(:protected_branch, project: project, allow_force_push: true)
+
+ expect(::ProtectedBranches::UpdateService).to receive(:new).with(
+ project, user, hash_including(
+ push_access_levels_attributes: [],
+ merge_access_levels_attributes: []
+ )
+ ).and_call_original
+
+ expect do
+ expect(described_class.new(project, user, { name: 'new name' }).update(protected_branch)).to be_valid
+ end.not_to change { protected_branch.reload.allow_force_push }
+ end
+end
diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb
index 00d1e8b5457..d7a3258160b 100644
--- a/spec/services/protected_branches/cache_service_spec.rb
+++ b/spec/services/protected_branches/cache_service_spec.rb
@@ -111,5 +111,16 @@ RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do
expect(service.fetch('not-found') { true }).to eq(true)
end
end
+
+ describe 'metrics' do
+ it 'records hit ratio metrics' do
+ expect_next_instance_of(Gitlab::Cache::Metrics) do |metrics|
+ expect(metrics).to receive(:increment_cache_miss).once
+ expect(metrics).to receive(:increment_cache_hit).exactly(4).times
+ end
+
+ 5.times { service.fetch('main') { true } }
+ end
+ end
end
# rubocop:enable Style/RedundantFetchBlock
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index a43f3bc55bf..f9c16c84121 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1450,6 +1450,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'estimate command' do
+ let(:content) { '/estimate_time 1h' }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'failed command' do
let(:content) { '/estimate' }
let(:issuable) { issue }
@@ -1470,6 +1475,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'spend command' do
+ let(:content) { '/spend_time 1h' }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'spend command with negative time' do
let(:content) { '/spend -120m' }
let(:issuable) { issue }
@@ -1537,6 +1547,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'remove_estimate command' do
+ let(:content) { '/remove_time_estimate' }
+ let(:issuable) { issue }
+ end
+
it_behaves_like 'remove_time_spent command' do
let(:content) { '/remove_time_spent' }
let(:issuable) { issue }
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
index 8f89696cc55..28f173f1bc7 100644
--- a/spec/services/resource_access_tokens/revoke_service_spec.rb
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -29,35 +29,13 @@ RSpec.describe ResourceAccessTokens::RevokeService do
expect(resource.reload.users).not_to include(resource_bot)
end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal' do
- subject
-
- expect(
- Users::GhostUserMigration.where(user: resource_bot,
- initiator_user: user)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'transfer issuables of bot user to ghost user' do
- issue = create(:issue, author: resource_bot)
-
- subject
-
- expect(issue.reload.author.ghost?).to be true
- end
-
- it 'deletes project bot user' do
- subject
+ it 'initiates user removal' do
+ subject
- expect(User.exists?(resource_bot.id)).to be_falsy
- end
+ expect(
+ Users::GhostUserMigration.where(user: resource_bot,
+ initiator_user: user)
+ ).to be_exists
end
it 'logs the event' do
diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb
index ed234376381..425d5b19907 100644
--- a/spec/services/resource_events/change_milestone_service_spec.rb
+++ b/spec/services/resource_events/change_milestone_service_spec.rb
@@ -14,4 +14,35 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do
let_it_be(:resource) { create(issuable) } # rubocop:disable Rails/SaveBang
end
end
+
+ describe 'events tracking' do
+ let_it_be(:user) { create(:user) }
+
+ let(:resource) { create(resource_type, milestone: timebox, project: timebox.project) }
+
+ subject(:service_instance) { described_class.new(resource, user, old_milestone: nil) }
+
+ context 'when the resource is a work item' do
+ let(:resource_type) { :work_item }
+
+ it 'tracks work item usage data counters' do
+ expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter)
+ .to receive(:track_work_item_milestone_changed_action)
+ .with(author: user)
+
+ service_instance.execute
+ end
+ end
+
+ context 'when the resource is not a work item' do
+ let(:resource_type) { :issue }
+
+ it 'does not track work item usage data counters' do
+ expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter)
+ .not_to receive(:track_work_item_milestone_changed_action)
+
+ service_instance.execute
+ end
+ end
+ end
end
diff --git a/spec/services/resource_events/change_state_service_spec.rb b/spec/services/resource_events/change_state_service_spec.rb
index 255ee9eca57..b679943073c 100644
--- a/spec/services/resource_events/change_state_service_spec.rb
+++ b/spec/services/resource_events/change_state_service_spec.rb
@@ -18,10 +18,11 @@ RSpec.describe ResourceEvents::ChangeStateService do
event = resource.resource_state_events.last
- if resource.is_a?(Issue)
+ case resource
+ when Issue
expect(event.issue).to eq(resource)
expect(event.merge_request).to be_nil
- elsif resource.is_a?(MergeRequest)
+ when MergeRequest
expect(event.issue).to be_nil
expect(event.merge_request).to eq(resource)
end
@@ -91,10 +92,11 @@ RSpec.describe ResourceEvents::ChangeStateService do
end
def expect_event_source(event, source)
- if source.is_a?(MergeRequest)
+ case source
+ when MergeRequest
expect(event.source_commit).to be_nil
expect(event.source_merge_request).to eq(source)
- elsif source.is_a?(Commit)
+ when Commit
expect(event.source_commit).to eq(source.id)
expect(event.source_merge_request).to be_nil
else
diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb
index 152d0700cc1..c9bfa7cb7b4 100644
--- a/spec/services/search/group_service_spec.rb
+++ b/spec/services/search/group_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Search::GroupService do
# These projects shouldn't be found
let!(:outside_project) { create(:project, :public, name: "Outside #{term}") }
- let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}" ) }
+ let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}") }
let!(:other_project) { create(:project, :public, namespace: nested_group, name: term.reverse) }
# These projects should be found
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 5edea13afa4..26def474b88 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -399,159 +399,7 @@ RSpec.describe SearchService do
end
end
- context 'redacting search results' do
- let(:search) { 'anything' }
-
- subject(:result) { search_service.search_objects }
-
- shared_examples "redaction limits N+1 queries" do |limit:|
- it 'does not exceed the query limit' do
- # issuing the query to remove the data loading call
- unredacted_results.to_a
-
- # only the calls from the redaction are left
- query = ActiveRecord::QueryRecorder.new { result }
-
- # these are the project authorization calls, which are not preloaded
- expect(query.count).to be <= limit
- end
- end
-
- def found_blob(project)
- Gitlab::Search::FoundBlob.new(project: project)
- end
-
- def found_wiki_page(project)
- Gitlab::Search::FoundWikiPage.new(found_blob(project))
- end
-
- before do
- expect(search_service)
- .to receive(:search_results)
- .and_return(double('search results', objects: unredacted_results))
- end
-
- def ar_relation(klass, *objects)
- klass.id_in(objects.map(&:id))
- end
-
- def kaminari_array(*objects)
- Kaminari.paginate_array(objects).page(1).per(20)
- end
-
- context 'issues' do
- let(:readable) { create(:issue, project: accessible_project) }
- let(:unreadable) { create(:issue, project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(Issue, readable, unreadable) }
- let(:scope) { 'issues' }
-
- it 'redacts the inaccessible issue' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'notes' do
- let(:readable) { create(:note_on_commit, project: accessible_project) }
- let(:unreadable) { create(:note_on_commit, project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(Note, readable, unreadable) }
- let(:scope) { 'notes' }
-
- it 'redacts the inaccessible note' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'merge_requests' do
- let(:readable) { create(:merge_request, source_project: accessible_project, author: user) }
- let(:unreadable) { create(:merge_request, source_project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(MergeRequest, readable, unreadable) }
- let(:scope) { 'merge_requests' }
-
- it 'redacts the inaccessible merge request' do
- expect(result).to contain_exactly(readable)
- end
-
- context 'with :with_api_entity_associations' do
- let(:unredacted_results) { ar_relation(MergeRequest.with_api_entity_associations, readable, unreadable) }
-
- it_behaves_like "redaction limits N+1 queries", limit: 8
- end
- end
-
- context 'project repository blobs' do
- let(:readable) { found_blob(accessible_project) }
- let(:unreadable) { found_blob(inaccessible_project) }
- let(:unredacted_results) { kaminari_array(readable, unreadable) }
- let(:scope) { 'blobs' }
-
- it 'redacts the inaccessible blob' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'project wiki blobs' do
- let(:readable) { found_wiki_page(accessible_project) }
- let(:unreadable) { found_wiki_page(inaccessible_project) }
- let(:unredacted_results) { kaminari_array(readable, unreadable) }
- let(:scope) { 'wiki_blobs' }
-
- it 'redacts the inaccessible blob' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'project snippets' do
- let(:readable) { create(:project_snippet, project: accessible_project) }
- let(:unreadable) { create(:project_snippet, project: inaccessible_project) }
- let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) }
- let(:scope) { 'snippet_titles' }
-
- it 'redacts the inaccessible snippet' do
- expect(result).to contain_exactly(readable)
- end
-
- context 'with :with_api_entity_associations' do
- it_behaves_like "redaction limits N+1 queries", limit: 14
- end
- end
-
- context 'personal snippets' do
- let(:readable) { create(:personal_snippet, :private, author: user) }
- let(:unreadable) { create(:personal_snippet, :private) }
- let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) }
- let(:scope) { 'snippet_titles' }
-
- it 'redacts the inaccessible snippet' do
- expect(result).to contain_exactly(readable)
- end
-
- context 'with :with_api_entity_associations' do
- it_behaves_like "redaction limits N+1 queries", limit: 4
- end
- end
-
- context 'commits' do
- let(:readable) { accessible_project.commit }
- let(:unreadable) { inaccessible_project.commit }
- let(:unredacted_results) { kaminari_array(readable, unreadable) }
- let(:scope) { 'commits' }
-
- it 'redacts the inaccessible commit' do
- expect(result).to contain_exactly(readable)
- end
- end
-
- context 'users' do
- let(:other_user) { create(:user) }
- let(:unredacted_results) { ar_relation(User, user, other_user) }
- let(:scope) { 'users' }
-
- it 'passes the users through' do
- # Users are always visible to everyone
- expect(result).to contain_exactly(user, other_user)
- end
- end
- end
+ it_behaves_like 'a redacted search results'
end
describe '#valid_request?' do
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 7a004e2915c..9211beb76f8 100644
--- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb
+++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb
@@ -11,9 +11,9 @@ RSpec.describe Security::CiConfiguration::SastParserService do
let(:sast_excluded_paths) { configuration['global'][1] }
let(:sast_pipeline_stage) { configuration['pipeline'][0] }
let(:sast_search_max_depth) { configuration['pipeline'][1] }
- let(:bandit) { configuration['analyzers'][0] }
- let(:brakeman) { configuration['analyzers'][1] }
+ let(:brakeman) { configuration['analyzers'][0] }
let(:sast_brakeman_level) { brakeman['variables'][0] }
+ let(:semgrep) { configuration['analyzers'][1] }
let(:secure_analyzers_prefix) { '$CI_TEMPLATE_REGISTRY_HOST/security-products' }
it 'parses the configuration for SAST' do
@@ -34,7 +34,7 @@ RSpec.describe Security::CiConfiguration::SastParserService do
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)
- expect(bandit['enabled']).to be(true)
+ expect(semgrep['enabled']).to be(true)
expect(sast_brakeman_level['value']).to eql('2')
end
@@ -43,7 +43,7 @@ RSpec.describe Security::CiConfiguration::SastParserService do
allow(project.repository).to receive(:blob_data_at).and_return(gitlab_ci_yml_excluded_analyzers_content)
expect(brakeman['enabled']).to be(false)
- expect(bandit['enabled']).to be(true)
+ expect(semgrep['enabled']).to be(true)
end
end
end
diff --git a/spec/services/security/merge_reports_service_spec.rb b/spec/services/security/merge_reports_service_spec.rb
index e61977297c5..8415ed8a22f 100644
--- a/spec/services/security/merge_reports_service_spec.rb
+++ b/spec/services/security/merge_reports_service_spec.rb
@@ -219,10 +219,10 @@ RSpec.describe Security::MergeReportsService, '#execute' do
let(:finding_id_1) { build(:ci_reports_security_finding, identifiers: [identifier_bandit, identifier_cve], scanner: bandit_scanner, report_type: :sast) }
let(:finding_id_2) { build(:ci_reports_security_finding, identifiers: [identifier_cve], scanner: semgrep_scanner, report_type: :sast) }
- let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast ) }
+ let(:finding_id_3) { build(:ci_reports_security_finding, identifiers: [identifier_semgrep], scanner: semgrep_scanner, report_type: :sast) }
let(:bandit_report) do
- build( :ci_reports_security_report,
+ build(:ci_reports_security_report,
type: :sast,
scanners: [bandit_scanner],
findings: [finding_id_1],
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index b2ccd9dba52..3263e410d3c 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -175,7 +175,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
it 'builds a correct phrase when one reviewer removed from a set' do
expect(build_note([reviewer, reviewer1, reviewer2], [reviewer, reviewer1])).to(
- eq( "removed review request for @#{reviewer2.username}")
+ eq("removed review request for @#{reviewer2.username}")
)
end
@@ -681,7 +681,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
it 'tracks usage' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter)
- .to receive(:track_issue_cloned_action).with(author: author, project: project )
+ .to receive(:track_issue_cloned_action).with(author: author, project: project)
subject
end
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index b1c6623308e..bbf6fe62959 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -27,6 +27,26 @@ RSpec.describe Tags::CreateService do
end
end
+ context 'when tag_name is empty' do
+ it 'returns an error' do
+ response = service.execute('', 'foo', 'Foo')
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:http_status]).to eq(400)
+ expect(response[:message]).to eq('Tag name invalid')
+ end
+ end
+
+ context 'when target is empty' do
+ it 'returns an error' do
+ response = service.execute('v1.1.0', '', 'Foo')
+
+ expect(response[:status]).to eq(:error)
+ expect(response[:http_status]).to eq(400)
+ expect(response[:message]).to eq('Target is empty')
+ end
+ end
+
context 'when tag already exists' do
it 'returns an error' do
expect(repository).to receive(:add_tag)
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 45a8268043f..774a6ddcfb3 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1037,7 +1037,7 @@ RSpec.describe TodoService do
let_it_be(:noteable) { create(:issue, project: project) }
let(:note) { create(:note, project: project, note: mentions, noteable: noteable) }
- let(:addressed_note) { create(:note, project: project, note: "#{directly_addressed}", noteable: noteable) }
+ let(:addressed_note) { create(:note, project: project, note: directly_addressed.to_s, noteable: noteable) }
it 'creates a todo for each valid mentioned user not included in skip_users' do
service.update_note(note, author, skip_users)
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
index 225e7933d79..9d5ed70e9ef 100644
--- a/spec/services/todos/destroy/entity_leave_service_spec.rb
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
let!(:todo_issue_user) { create(:todo, user: user, target: issue, project: project) }
let!(:todo_issue_c_user) { create(:todo, user: user, target: issue_c, project: project) }
let!(:todo_issue_c_user2) { create(:todo, user: user2, target: issue_c, project: project) }
- let(:internal_note) { create(:note, noteable: issue, project: project, confidential: true ) }
+ let(:internal_note) { create(:note, noteable: issue, project: project, confidential: true) }
let!(:todo_for_internal_note) do
create(:todo, user: user, target: issue, project: project, note: internal_note)
end
@@ -28,7 +28,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
set_access(group, user, group_access) if group_access
end
- it "#{params[:method].to_s.humanize(capitalize: false)}" do
+ it params[:method].to_s.humanize(capitalize: false) do
send(method_name)
end
end
@@ -250,7 +250,7 @@ RSpec.describe Todos::Destroy::EntityLeaveService do
let!(:todo_subproject_user2) { create(:todo, user: user2, project: subproject) }
let!(:todo_subpgroup_user2) { create(:todo, user: user2, group: subgroup) }
let!(:todo_parent_group_user) { create(:todo, user: user, group: parent_group) }
- let(:subproject_internal_note) { create(:note, noteable: issue, project: project, confidential: true ) }
+ let(:subproject_internal_note) { create(:note, noteable: issue, project: project, confidential: true) }
let!(:todo_for_internal_subproject_note) do
create(:todo, user: user, target: issue, project: project, note: subproject_internal_note)
end
diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb
index eef31817aa8..98247250a61 100644
--- a/spec/services/topics/merge_service_spec.rb
+++ b/spec/services/topics/merge_service_spec.rb
@@ -5,10 +5,10 @@ require 'spec_helper'
RSpec.describe Topics::MergeService do
let_it_be(:source_topic) { create(:topic, name: 'source_topic') }
let_it_be(:target_topic) { create(:topic, name: 'target_topic') }
- let_it_be(:project_1) { create(:project, :public, topic_list: source_topic.name ) }
- let_it_be(:project_2) { create(:project, :private, topic_list: source_topic.name ) }
- let_it_be(:project_3) { create(:project, :public, topic_list: target_topic.name ) }
- let_it_be(:project_4) { create(:project, :public, topic_list: [source_topic.name, target_topic.name] ) }
+ let_it_be(:project_1) { create(:project, :public, topic_list: source_topic.name) }
+ let_it_be(:project_2) { create(:project, :private, topic_list: source_topic.name) }
+ let_it_be(:project_3) { create(:project, :public, topic_list: target_topic.name) }
+ let_it_be(:project_4) { create(:project, :public, topic_list: [source_topic.name, target_topic.name]) }
subject { described_class.new(source_topic, target_topic).execute }
diff --git a/spec/services/users/approve_service_spec.rb b/spec/services/users/approve_service_spec.rb
index 078dde546c9..34eb5b18ff6 100644
--- a/spec/services/users/approve_service_spec.rb
+++ b/spec/services/users/approve_service_spec.rb
@@ -67,7 +67,7 @@ RSpec.describe Users::ApproveService do
subject
- expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request approved", user: "#{user.username}", email: "#{user.email}", approved_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request approved", user: user.username.to_s, email: user.email.to_s, approved_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
end
it 'emails the user on approval' do
diff --git a/spec/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb
index 79f3cbeb46d..3f9c7ebf067 100644
--- a/spec/services/users/ban_service_spec.rb
+++ b/spec/services/users/ban_service_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Users::BanService do
end
it 'logs ban in application logs' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: "User ban", user: "#{user.username}", email: "#{user.email}", ban_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to receive(:info).with(message: "User ban", user: user.username.to_s, email: user.email.to_s, ban_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
ban_user
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 03e1811c8a5..18ad946b289 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -10,546 +10,250 @@ RSpec.describe Users::DestroyService do
let(:service) { described_class.new(admin) }
let(:gitlab_shell) { Gitlab::Shell.new }
- shared_examples 'pre-migrate clean-up' do
- describe "Deletes a user and all their personal projects", :enable_admin_mode do
- context 'no options are given' do
- it 'will delete the personal project' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
- end
-
- service.execute(user)
- end
+ describe "Initiates user deletion and deletes all their personal projects", :enable_admin_mode do
+ context 'no options are given' do
+ it 'creates GhostUserMigration record to handle migration in a worker' do
+ expect { service.execute(user) }
+ .to(
+ change do
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: admin)
+ .exists?
+ end.from(false).to(true))
end
- context 'personal projects in pending_delete' do
- before do
- project.pending_delete = true
- project.save!
+ it 'will delete the personal project' do
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once.and_return(true)
end
- it 'destroys a personal project in pending_delete' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
- end
-
- service.execute(user)
- end
+ service.execute(user)
end
+ end
- context "solo owned groups present" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
-
- before do
- solo_owned.group_members = [member]
- end
-
- it 'returns the user with attached errors' do
- expect(service.execute(user)).to be(user)
- expect(user.errors.full_messages).to(
- contain_exactly('You must transfer ownership or delete groups before you can remove user'))
- end
-
- it 'does not delete the user, nor the group' do
- service.execute(user)
-
- expect(User.find(user.id)).to eq user
- expect(Group.find(solo_owned.id)).to eq solo_owned
- end
+ context 'personal projects in pending_delete' do
+ before do
+ project.pending_delete = true
+ project.save!
end
- context "deletions with solo owned groups" do
- let(:solo_owned) { create(:group) }
- let(:member) { create(:group_member) }
- let(:user) { member.user }
-
- before do
- solo_owned.group_members = [member]
- service.execute(user, delete_solo_owned_groups: true)
+ it 'destroys a personal project in pending_delete' do
+ expect_next_instance_of(Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).once.and_return(true)
end
- it 'deletes solo owned groups' do
- expect { Group.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ service.execute(user)
end
+ end
- context 'deletions with inherited group owners' do
- let(:group) { create(:group, :nested) }
- let(:user) { create(:user) }
- let(:inherited_owner) { create(:user) }
-
- before do
- group.parent.add_owner(inherited_owner)
- group.add_owner(user)
+ context "solo owned groups present" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
- service.execute(user, delete_solo_owned_groups: true)
- end
-
- it 'does not delete the group' do
- expect(Group.exists?(id: group)).to be_truthy
- end
+ before do
+ solo_owned.group_members = [member]
end
- describe "user personal's repository removal" do
- context 'storages' do
- before do
- perform_enqueued_jobs { service.execute(user) }
- end
-
- context 'legacy storage' do
- let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
-
- it 'removes repository' do
- expect(
- gitlab_shell.repository_exists?(project.repository_storage,
- "#{project.disk_path}.git")
- ).to be_falsey
- end
- end
-
- context 'hashed storage' do
- let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
-
- it 'removes repository' do
- expect(
- gitlab_shell.repository_exists?(project.repository_storage,
- "#{project.disk_path}.git")
- ).to be_falsey
- end
- end
- end
-
- context 'repository removal status is taken into account' do
- it 'raises exception' do
- expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).and_return(false)
- end
-
- expect { service.execute(user) }
- .to raise_error(Users::DestroyService::DestroyError,
- "Project #{project.id} can't be deleted" )
- end
- end
+ it 'returns the user with attached errors' do
+ expect(service.execute(user)).to be(user)
+ expect(user.errors.full_messages).to(
+ contain_exactly('You must transfer ownership or delete groups before you can remove user'))
end
- describe "calls the before/after callbacks" do
- it 'of project_members' do
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
- expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
-
- service.execute(user)
- end
-
- it 'of group_members' do
- group_member = create(:group_member)
- group_member.group.group_members.create!(user: user, access_level: 40)
+ it 'does not delete the user, nor the group' do
+ service.execute(user)
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
- expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
-
- service.execute(user)
- end
+ expect(User.find(user.id)).to eq user
+ expect(Group.find(solo_owned.id)).to eq solo_owned
end
end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- include_examples 'pre-migrate clean-up'
-
- describe "Deletes a user and all their personal projects", :enable_admin_mode do
- context 'no options are given' do
- it 'deletes the user' do
- user_data = service.execute(user)
- expect(user_data['email']).to eq(user.email)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it 'deletes user associations in batches' do
- expect(user).to receive(:destroy_dependent_associations_in_batches)
+ context "deletions with solo owned groups" do
+ let(:solo_owned) { create(:group) }
+ let(:member) { create(:group_member) }
+ let(:user) { member.user }
- service.execute(user)
- end
-
- it 'does not include snippets when deleting in batches' do
- expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] })
-
- service.execute(user)
- end
-
- it 'calls the bulk snippet destroy service for the user personal snippets' do
- repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
- repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
-
- aggregate_failures do
- expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_truthy
- expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_truthy
- end
-
- # Call made when destroying user personal projects
- expect(Snippets::BulkDestroyService).to receive(:new)
- .with(admin, project.snippets).and_call_original
-
- # Call to remove user personal snippets and for
- # project snippets where projects are not user personal
- # ones
- expect(Snippets::BulkDestroyService).to receive(:new)
- .with(admin, user.snippets.only_personal_snippets).and_call_original
-
- service.execute(user)
-
- aggregate_failures do
- expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_falsey
- expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_falsey
- end
- end
-
- it 'calls the bulk snippet destroy service with hard delete option if it is present' do
- # this avoids getting into Projects::DestroyService as it would
- # call Snippets::BulkDestroyService first!
- allow(user).to receive(:personal_projects).and_return([])
-
- expect_next_instance_of(Snippets::BulkDestroyService) do |bulk_destroy_service|
- expect(bulk_destroy_service).to receive(:execute).with({ skip_authorization: true }).and_call_original
- end
-
- service.execute(user, { hard_delete: true })
- end
-
- it 'does not delete project snippets that the user is the author of' do
- repo = create(:project_snippet, :repository, author: user).snippet_repository
- service.execute(user)
- expect(gitlab_shell.repository_exists?(repo.shard_name, repo.disk_path + '.git')).to be_truthy
- expect(User.ghost.snippets).to include(repo.snippet)
- end
-
- context 'when an error is raised deleting snippets' do
- it 'does not delete user' do
- snippet = create(:personal_snippet, :repository, author: user)
-
- bulk_service = double
- allow(Snippets::BulkDestroyService).to receive(:new).and_call_original
- allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service)
- allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
-
- aggregate_failures do
- expect { service.execute(user) }
- .to raise_error(Users::DestroyService::DestroyError, 'foo' )
- expect(snippet.reload).not_to be_nil
- expect(
- gitlab_shell.repository_exists?(snippet.repository_storage,
- snippet.disk_path + '.git')
- ).to be_truthy
- end
- end
- end
+ before do
+ solo_owned.group_members = [member]
+ service.execute(user, delete_solo_owned_groups: true)
end
- context 'projects in pending_delete' do
- before do
- project.pending_delete = true
- project.save!
- end
-
- it 'destroys a project in pending_delete' do
- expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once.and_return(true)
- end
-
- service.execute(user)
-
- expect { Project.find(project.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ it 'deletes solo owned groups' do
+ expect { Group.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
+ end
- context "a deleted user's issues" do
- let(:project) { create(:project) }
-
- before do
- project.add_developer(user)
- end
-
- context "for an issue the user was assigned to" do
- let!(:issue) { create(:issue, project: project, assignees: [user]) }
-
- before do
- service.execute(user)
- end
+ context 'deletions with inherited group owners' do
+ let(:group) { create(:group, :nested) }
+ let(:user) { create(:user) }
+ let(:inherited_owner) { create(:user) }
- it 'does not delete issues the user is assigned to' do
- expect(Issue.find_by_id(issue.id)).to be_present
- end
+ before do
+ group.parent.add_owner(inherited_owner)
+ group.add_owner(user)
- it 'migrates the issue so that it is "Unassigned"' do
- migrated_issue = Issue.find_by_id(issue.id)
-
- expect(migrated_issue.assignees).to be_empty
- end
- end
+ service.execute(user, delete_solo_owned_groups: true)
end
- context "a deleted user's merge_requests" do
- let(:project) { create(:project, :repository) }
+ it 'does not delete the group' do
+ expect(Group.exists?(id: group)).to be_truthy
+ end
+ end
+ describe "user personal's repository removal" do
+ context 'storages' do
before do
- project.add_developer(user)
+ perform_enqueued_jobs { service.execute(user) }
end
- context "for an merge request the user was assigned to" do
- let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
+ context 'legacy storage' do
+ let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
- before do
- service.execute(user)
- end
-
- it 'does not delete merge requests the user is assigned to' do
- expect(MergeRequest.find_by_id(merge_request.id)).to be_present
- end
-
- it 'migrates the merge request so that it is "Unassigned"' do
- migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
-
- expect(migrated_merge_request.assignees).to be_empty
+ it 'removes repository' do
+ expect(
+ gitlab_shell.repository_exists?(project.repository_storage,
+ "#{project.disk_path}.git")
+ ).to be_falsey
end
end
- end
-
- context 'migrating associated records' do
- let!(:issue) { create(:issue, author: user) }
- it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do
- expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
+ context 'hashed storage' do
+ let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
- service.execute(user)
-
- expect(issue.reload.author).to be_ghost
- end
-
- context 'when hard_delete option is given' do
- it 'will not ghost certain records' do
- expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
-
- service.execute(user, hard_delete: true)
-
- expect(Issue.exists?(issue.id)).to be_falsy
+ it 'removes repository' do
+ expect(
+ gitlab_shell.repository_exists?(project.repository_storage,
+ "#{project.disk_path}.git")
+ ).to be_falsey
end
end
end
- end
-
- describe "Deletion permission checks" do
- it 'does not delete the user when user is not an admin' do
- other_user = create(:user)
-
- expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- expect(User.exists?(user.id)).to be(true)
- end
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'allows admins to delete anyone' do
- described_class.new(admin).execute(user)
-
- expect(User.exists?(user.id)).to be(false)
- end
- end
- context 'when admin mode is disabled' do
- it 'disallows admins to delete anyone' do
- expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ context 'repository removal status is taken into account' do
+ it 'raises exception' do
+ expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).and_return(false)
+ end
- expect(User.exists?(user.id)).to be(true)
+ expect { service.execute(user) }
+ .to raise_error(Users::DestroyService::DestroyError,
+ "Project #{project.id} can't be deleted")
end
end
-
- it 'allows users to delete their own account' do
- described_class.new(user).execute(user)
-
- expect(User.exists?(user.id)).to be(false)
- end
-
- it 'allows user to be deleted if skip_authorization: true' do
- other_user = create(:user)
-
- described_class.new(user).execute(other_user, skip_authorization: true)
-
- expect(User.exists?(other_user.id)).to be(false)
- end
end
- context 'batched nullify' do
- let(:other_user) { create(:user) }
+ describe "calls the before/after callbacks" do
+ it 'of project_members' do
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
- # rubocop:disable Layout/LineLength
- def nullify_in_batches_regexp(table, column, user, batch_size: 100)
- %r{^UPDATE "#{table}" SET "#{column}" = NULL WHERE "#{table}"."id" IN \(SELECT "#{table}"."id" FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id} LIMIT #{batch_size}\)}
+ service.execute(user)
end
- def delete_in_batches_regexps(table, column, user, items, batch_size: 1000)
- select_query = %r{^SELECT "#{table}".* FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id}.*ORDER BY "#{table}"."id" ASC LIMIT #{batch_size}}
-
- [select_query] + items.map { |item| %r{^DELETE FROM "#{table}" WHERE "#{table}"."id" = #{item.id}} }
- end
- # rubocop:enable Layout/LineLength
+ it 'of group_members' do
+ group_member = create(:group_member)
+ group_member.group.group_members.create!(user: user, access_level: 40)
- it 'nullifies related associations in batches' do
- expect(other_user).to receive(:nullify_dependent_associations_in_batches).and_call_original
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
- described_class.new(user).execute(other_user, skip_authorization: true)
+ service.execute(user)
end
+ end
- it 'nullifies issues and resource associations', :aggregate_failures do
- issue = create(:issue, closed_by: other_user, updated_by: other_user)
- resource_label_event = create(:resource_label_event, user: other_user)
- resource_state_event = create(:resource_state_event, user: other_user)
- todos = create_list(:todo, 2, project: issue.project, user: other_user, author: other_user, target: issue)
- event = create(:event, project: issue.project, author: other_user)
+ describe 'prometheus metrics', :prometheus do
+ context 'scheduled records' do
+ context 'with a single record' do
+ it 'updates the scheduled records gauge' do
+ service.execute(user)
- query_recorder = ActiveRecord::QueryRecorder.new do
- described_class.new(user).execute(other_user, skip_authorization: true)
+ gauge = Gitlab::Metrics.registry.get(:gitlab_ghost_user_migration_scheduled_records_total)
+ expect(gauge.get).to eq(1)
+ end
end
- issue.reload
- resource_label_event.reload
- resource_state_event.reload
-
- expect(issue.closed_by).to be_nil
- expect(issue.updated_by).to be_nil
- expect(resource_label_event.user).to be_nil
- expect(resource_state_event.user).to be_nil
- expect(other_user.authored_todos).to be_empty
- expect(other_user.todos).to be_empty
- expect(other_user.authored_events).to be_empty
-
- expected_queries = [
- nullify_in_batches_regexp(:issues, :updated_by_id, other_user),
- nullify_in_batches_regexp(:issues, :closed_by_id, other_user),
- nullify_in_batches_regexp(:resource_label_events, :user_id, other_user),
- nullify_in_batches_regexp(:resource_state_events, :user_id, other_user)
- ]
-
- expected_queries += delete_in_batches_regexps(:todos, :user_id, other_user, todos)
- expected_queries += delete_in_batches_regexps(:todos, :author_id, other_user, todos)
- expected_queries += delete_in_batches_regexps(:events, :author_id, other_user, [event])
-
- expect(query_recorder.log).to include(*expected_queries)
- end
+ context 'with approximate count due to large number of records' do
+ it 'updates the scheduled records gauge' do
+ allow(Users::GhostUserMigration)
+ .to(receive_message_chain(:limit, :count).and_return(1001))
+ allow(Users::GhostUserMigration).to(receive(:minimum)).and_return(42)
+ allow(Users::GhostUserMigration).to(receive(:maximum)).and_return(9042)
- it 'nullifies merge request associations', :aggregate_failures do
- merge_request = create(:merge_request, source_project: project, target_project: project,
- assignee: other_user, updated_by: other_user, merge_user: other_user)
- merge_request.metrics.update!(merged_by: other_user, latest_closed_by: other_user)
- merge_request.reviewers = [other_user]
- merge_request.assignees = [other_user]
+ service.execute(user)
- query_recorder = ActiveRecord::QueryRecorder.new do
- described_class.new(user).execute(other_user, skip_authorization: true)
+ gauge = Gitlab::Metrics.registry.get(:gitlab_ghost_user_migration_scheduled_records_total)
+ expect(gauge.get).to eq(9000)
+ end
end
-
- merge_request.reload
-
- expect(merge_request.updated_by).to be_nil
- expect(merge_request.assignee).to be_nil
- expect(merge_request.assignee_id).to be_nil
- expect(merge_request.metrics.merged_by).to be_nil
- expect(merge_request.metrics.latest_closed_by).to be_nil
- expect(merge_request.reviewers).to be_empty
- expect(merge_request.assignees).to be_empty
-
- expected_queries = [
- nullify_in_batches_regexp(:merge_requests, :updated_by_id, other_user),
- nullify_in_batches_regexp(:merge_requests, :assignee_id, other_user),
- nullify_in_batches_regexp(:merge_request_metrics, :merged_by_id, other_user),
- nullify_in_batches_regexp(:merge_request_metrics, :latest_closed_by_id, other_user)
- ]
-
- expected_queries += delete_in_batches_regexps(:merge_request_assignees, :user_id, other_user,
- merge_request.assignees)
- expected_queries += delete_in_batches_regexps(:merge_request_reviewers, :user_id, other_user,
- merge_request.reviewers)
-
- expect(query_recorder.log).to include(*expected_queries)
end
- end
- end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- include_examples 'pre-migrate clean-up'
+ context 'lag' do
+ it 'update the lag gauge', :freeze_time do
+ create(:ghost_user_migration, created_at: 10.minutes.ago)
- describe "Deletes a user and all their personal projects", :enable_admin_mode do
- context 'no options are given' do
- it 'creates GhostUserMigration record to handle migration in a worker' do
- expect { service.execute(user) }
- .to(
- change do
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- .exists?
- end.from(false).to(true))
+ service.execute(user)
+
+ gauge = Gitlab::Metrics.registry.get(:gitlab_ghost_user_migration_lag_seconds)
+ expect(gauge.get).to eq(600)
end
end
end
+ end
- describe "Deletion permission checks" do
- it 'does not delete the user when user is not an admin' do
- other_user = create(:user)
-
- expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
-
- expect(Users::GhostUserMigration).not_to be_exists
- end
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'allows admins to delete anyone' do
- expect { described_class.new(admin).execute(user) }
- .to(
- change do
- Users::GhostUserMigration.where(user: user,
- initiator_user: admin)
- .exists?
- end.from(false).to(true))
- end
- end
+ describe "Deletion permission checks" do
+ it 'does not delete the user when user is not an admin' do
+ other_user = create(:user)
- context 'when admin mode is disabled' do
- it 'disallows admins to delete anyone' do
- expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
- expect(Users::GhostUserMigration).not_to be_exists
- end
- end
+ expect(Users::GhostUserMigration).not_to be_exists
+ end
- it 'allows users to delete their own account' do
- expect { described_class.new(user).execute(user) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'allows admins to delete anyone' do
+ expect { described_class.new(admin).execute(user) }
.to(
change do
Users::GhostUserMigration.where(user: user,
- initiator_user: user)
+ initiator_user: admin)
.exists?
end.from(false).to(true))
end
+ end
- it 'allows user to be deleted if skip_authorization: true' do
- other_user = create(:user)
+ context 'when admin mode is disabled' do
+ it 'disallows admins to delete anyone' do
+ expect { described_class.new(admin).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+
+ expect(Users::GhostUserMigration).not_to be_exists
+ end
+ end
- expect do
- described_class.new(user)
- .execute(other_user, skip_authorization: true)
- end.to(
+ it 'allows users to delete their own account' do
+ expect { described_class.new(user).execute(user) }
+ .to(
change do
- Users::GhostUserMigration.where(user: other_user,
- initiator_user: user )
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: user)
.exists?
end.from(false).to(true))
- end
+ end
+
+ it 'allows user to be deleted if skip_authorization: true' do
+ other_user = create(:user)
+
+ expect do
+ described_class.new(user)
+ .execute(other_user, skip_authorization: true)
+ end.to(
+ change do
+ Users::GhostUserMigration.where(user: other_user,
+ initiator_user: user )
+ .exists?
+ end.from(false).to(true))
end
end
end
diff --git a/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
index 7366b1646b9..107ff82016c 100644
--- a/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_in_batches_service_spec.rb
@@ -27,5 +27,34 @@ RSpec.describe Users::MigrateRecordsToGhostUserInBatchesService do
service.execute
end
+
+ it 'process jobs ordered by the consume_after timestamp' do
+ older_ghost_user_migration = create(:ghost_user_migration, user: create(:user),
+ consume_after: 5.minutes.ago)
+
+ # setup execution tracker to only allow a single job to be processed
+ allow_next_instance_of(::Gitlab::Utils::ExecutionTracker) do |tracker|
+ allow(tracker).to receive(:over_limit?).and_return(false, true)
+ end
+
+ expect(Users::MigrateRecordsToGhostUserService).to(
+ receive(:new).with(older_ghost_user_migration.user,
+ older_ghost_user_migration.initiator_user,
+ any_args)
+ ).and_call_original
+
+ service.execute
+ end
+
+ it 'reschedules job in case of an error', :freeze_time do
+ expect_next_instance_of(Users::MigrateRecordsToGhostUserService) do |service|
+ expect(service).to(receive(:execute)).and_raise(ActiveRecord::QueryCanceled)
+ end
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+
+ expect { service.execute }.to(
+ change { ghost_user_migration.reload.consume_after }
+ .to(30.minutes.from_now))
+ end
end
end
diff --git a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
index 766be51ae13..6082c7bd10e 100644
--- a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
@@ -146,24 +146,106 @@ RSpec.describe Users::MigrateRecordsToGhostUserService do
end
context 'for batched nullify' do
+ # rubocop:disable Layout/LineLength
+ def nullify_in_batches_regexp(table, column, user, batch_size: 100)
+ %r{^UPDATE "#{table}" SET "#{column}" = NULL WHERE "#{table}"."id" IN \(SELECT "#{table}"."id" FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id} LIMIT #{batch_size}\)}
+ end
+
+ def delete_in_batches_regexps(table, column, user, items, batch_size: 1000)
+ select_query = %r{^SELECT "#{table}".* FROM "#{table}" WHERE "#{table}"."#{column}" = #{user.id}.*ORDER BY "#{table}"."id" ASC LIMIT #{batch_size}}
+
+ [select_query] + items.map { |item| %r{^DELETE FROM "#{table}" WHERE "#{table}"."id" = #{item.id}} }
+ end
+ # rubocop:enable Layout/LineLength
+
it 'nullifies related associations in batches' do
expect(user).to receive(:nullify_dependent_associations_in_batches).and_call_original
service.execute
end
- it 'nullifies last_updated_issues, closed_issues, resource_label_events' do
+ it 'nullifies associations marked as `dependent: :nullify` and'\
+ 'destroys the associations marked as `dependent: :destroy`, in batches', :aggregate_failures do
+ # associations to be nullified
issue = create(:issue, closed_by: user, updated_by: user)
resource_label_event = create(:resource_label_event, user: user)
+ resource_state_event = create(:resource_state_event, user: user)
+ created_project = create(:project, creator: user)
- service.execute
+ # associations to be destroyed
+ todos = create_list(:todo, 2, project: issue.project, user: user, author: user, target: issue)
+ event = create(:event, project: issue.project, author: user)
+
+ query_recorder = ActiveRecord::QueryRecorder.new do
+ service.execute
+ end
issue.reload
resource_label_event.reload
+ resource_state_event.reload
+ created_project.reload
expect(issue.closed_by).to be_nil
- expect(issue.updated_by).to be_nil
- expect(resource_label_event.user).to be_nil
+ expect(issue.updated_by_id).to be_nil
+ expect(resource_label_event.user_id).to be_nil
+ expect(resource_state_event.user_id).to be_nil
+ expect(created_project.creator_id).to be_nil
+ expect(user.authored_todos).to be_empty
+ expect(user.todos).to be_empty
+ expect(user.authored_events).to be_empty
+
+ expected_queries = [
+ nullify_in_batches_regexp(:issues, :updated_by_id, user),
+ nullify_in_batches_regexp(:issues, :closed_by_id, user),
+ nullify_in_batches_regexp(:resource_label_events, :user_id, user),
+ nullify_in_batches_regexp(:resource_state_events, :user_id, user),
+ nullify_in_batches_regexp(:projects, :creator_id, user)
+ ]
+
+ expected_queries += delete_in_batches_regexps(:todos, :user_id, user, todos)
+ expected_queries += delete_in_batches_regexps(:todos, :author_id, user, todos)
+ expected_queries += delete_in_batches_regexps(:events, :author_id, user, [event])
+
+ expect(query_recorder.log).to include(*expected_queries)
+ end
+
+ it 'nullifies merge request associations', :aggregate_failures do
+ merge_request = create(:merge_request, source_project: project,
+ target_project: project,
+ assignee: user,
+ updated_by: user,
+ merge_user: user)
+ merge_request.metrics.update!(merged_by: user, latest_closed_by: user)
+ merge_request.reviewers = [user]
+ merge_request.assignees = [user]
+
+ query_recorder = ActiveRecord::QueryRecorder.new do
+ service.execute
+ end
+
+ merge_request.reload
+
+ expect(merge_request.updated_by).to be_nil
+ expect(merge_request.assignee).to be_nil
+ expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.metrics.merged_by).to be_nil
+ expect(merge_request.metrics.latest_closed_by).to be_nil
+ expect(merge_request.reviewers).to be_empty
+ expect(merge_request.assignees).to be_empty
+
+ expected_queries = [
+ nullify_in_batches_regexp(:merge_requests, :updated_by_id, user),
+ nullify_in_batches_regexp(:merge_requests, :assignee_id, user),
+ nullify_in_batches_regexp(:merge_request_metrics, :merged_by_id, user),
+ nullify_in_batches_regexp(:merge_request_metrics, :latest_closed_by_id, user)
+ ]
+
+ expected_queries += delete_in_batches_regexps(:merge_request_assignees, :user_id, user,
+ merge_request.assignees)
+ expected_queries += delete_in_batches_regexps(:merge_request_reviewers, :user_id, user,
+ merge_request.reviewers)
+
+ expect(query_recorder.log).to include(*expected_queries)
end
end
@@ -235,7 +317,7 @@ RSpec.describe Users::MigrateRecordsToGhostUserService do
aggregate_failures do
expect { service.execute }.to(
- raise_error(Users::MigrateRecordsToGhostUserService::DestroyError, 'foo' ))
+ raise_error(Users::MigrateRecordsToGhostUserService::DestroyError, 'foo'))
expect(snippet.reload).not_to be_nil
expect(
gitlab_shell.repository_exists?(snippet.repository_storage,
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
deleted file mode 100644
index 073ebaae5b0..00000000000
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Users::MigrateToGhostUserService do
- let!(:user) { create(:user) }
- let!(:project) { create(:project, :repository) }
- let(:service) { described_class.new(user) }
- let(:always_ghost) { false }
-
- context "migrating a user's associated records to the ghost user" do
- context 'issues' do
- context 'deleted user is present as both author and edited_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:author, :last_edited_by] do
- let(:created_record) do
- create(:issue, project: project, author: user, last_edited_by: user)
- end
- end
- end
-
- context 'deleted user is present only as edited_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:last_edited_by] do
- let(:created_record) { create(:issue, project: project, author: create(:user), last_edited_by: user) }
- end
- end
- end
-
- context 'merge requests' do
- context 'deleted user is present as both author and merge_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:author, :merge_user] do
- let(:created_record) { create(:merge_request, source_project: project, author: user, merge_user: user, target_branch: "first") }
- end
- end
-
- context 'deleted user is present only as both merge_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:merge_user] do
- let(:created_record) { create(:merge_request, source_project: project, merge_user: user, target_branch: "first") }
- end
- end
- end
-
- context 'notes' do
- include_examples "migrating a deleted user's associated records to the ghost user", Note do
- let(:created_record) { create(:note, project: project, author: user) }
- end
- end
-
- context 'abuse reports' do
- include_examples "migrating a deleted user's associated records to the ghost user", AbuseReport do
- let(:created_record) { create(:abuse_report, reporter: user, user: create(:user)) }
- end
- end
-
- context 'award emoji' do
- include_examples "migrating a deleted user's associated records to the ghost user", AwardEmoji, [:user] do
- let(:created_record) { create(:award_emoji, user: user) }
-
- context "when the awardable already has an award emoji of the same name assigned to the ghost user" do
- let(:awardable) { create(:issue) }
- let!(:existing_award_emoji) { create(:award_emoji, user: User.ghost, name: "thumbsup", awardable: awardable) }
- let!(:award_emoji) { create(:award_emoji, user: user, name: "thumbsup", awardable: awardable) }
-
- it "migrates the award emoji regardless" do
- service.execute
-
- migrated_record = AwardEmoji.find_by_id(award_emoji.id)
-
- expect(migrated_record.user).to eq(User.ghost)
- end
-
- it "does not leave the migrated award emoji in an invalid state" do
- service.execute
-
- migrated_record = AwardEmoji.find_by_id(award_emoji.id)
-
- expect(migrated_record).to be_valid
- end
- end
- end
- end
-
- context 'snippets' do
- include_examples "migrating a deleted user's associated records to the ghost user", Snippet do
- let(:created_record) { create(:snippet, project: project, author: user) }
- end
- end
-
- context 'reviews' do
- let!(:user) { create(:user) }
- let(:service) { described_class.new(user) }
-
- include_examples "migrating a deleted user's associated records to the ghost user", Review, [:author] do
- let(:created_record) { create(:review, author: user) }
- end
- end
- end
-end
diff --git a/spec/services/users/reject_service_spec.rb b/spec/services/users/reject_service_spec.rb
index abff6b1e023..37d003c5dac 100644
--- a/spec/services/users/reject_service_spec.rb
+++ b/spec/services/users/reject_service_spec.rb
@@ -35,29 +35,14 @@ RSpec.describe Users::RejectService do
context 'success' do
context 'when the executor user is an admin in admin mode', :enable_admin_mode do
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates user removal', :sidekiq_inline do
- subject
-
- expect(subject[:status]).to eq(:success)
- expect(
- Users::GhostUserMigration.where(user: user,
- initiator_user: current_user)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'deletes the user', :sidekiq_inline do
- subject
+ it 'initiates user removal', :sidekiq_inline do
+ subject
- expect(subject[:status]).to eq(:success)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ expect(subject[:status]).to eq(:success)
+ expect(
+ Users::GhostUserMigration.where(user: user,
+ initiator_user: current_user)
+ ).to be_exists
end
it 'emails the user on rejection' do
@@ -73,7 +58,7 @@ RSpec.describe Users::RejectService do
subject
- expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request rejected", user: "#{user.username}", email: "#{user.email}", rejected_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to have_received(:info).with(message: "User instance access request rejected", user: user.username.to_s, email: user.email.to_s, rejected_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
end
end
end
diff --git a/spec/services/users/unban_service_spec.rb b/spec/services/users/unban_service_spec.rb
index d536baafdcc..3dcb8450e7b 100644
--- a/spec/services/users/unban_service_spec.rb
+++ b/spec/services/users/unban_service_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Users::UnbanService do
end
it 'logs unban in application logs' do
- expect(Gitlab::AppLogger).to receive(:info).with(message: "User unban", user: "#{user.username}", email: "#{user.email}", unban_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ expect(Gitlab::AppLogger).to receive(:info).with(message: "User unban", user: user.username.to_s, email: user.email.to_s, unban_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
unban_user
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 551c3dbcc82..c081b20d95f 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -175,22 +175,6 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
).once
end
- context 'when webhooks_gitlab_instance_header flag is disabled' do
- before do
- stub_feature_flags(webhooks_gitlab_instance_header: false)
- end
-
- it 'excludes the X-Gitlab-Instance header' do
- stub_full_request(project_hook.url, method: :post)
-
- service_instance.execute
-
- expect(WebMock).to have_requested(:post, stubbed_hostname(project_hook.url)).with(
- headers: headers.except('X-Gitlab-Instance')
- ).once
- end
- end
-
context 'when the data is a Gitlab::DataBuilder::Pipeline' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:data) { ::Gitlab::DataBuilder::Pipeline.new(pipeline) }
@@ -245,7 +229,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
it 'does not execute disabled hooks' do
allow(service_instance).to receive(:disabled?).and_return(true)
- expect(service_instance.execute).to eq({ status: :error, message: 'Hook disabled' })
+ expect(service_instance.execute).to have_attributes(status: :error, message: 'Hook disabled')
end
it 'executes and registers the hook with the recursion detection', :aggregate_failures do
@@ -317,7 +301,8 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
project_hook.enable!
stub_full_request(project_hook.url, method: :post).to_raise(exception)
- expect(service_instance.execute).to eq({ status: :error, message: exception.to_s })
+
+ expect(service_instance.execute).to have_attributes(status: :error, message: exception.to_s)
expect { service_instance.execute }.not_to raise_error
end
end
@@ -326,7 +311,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
let_it_be(:project_hook) { create(:project_hook, url: 'http://server.com/my path/') }
it 'handles exceptions' do
- expect(service_instance.execute).to eq(status: :error, message: 'bad URI(is not URI?): "http://server.com/my path/"')
+ expect(service_instance.execute).to have_attributes(
+ status: :error,
+ message: 'bad URI(is not URI?): "http://server.com/my path/"'
+ )
expect { service_instance.execute }.not_to raise_error
end
end
@@ -335,20 +323,31 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state
it 'does not perform the request' do
stub_const("#{described_class}::REQUEST_BODY_SIZE_LIMIT", 10.bytes)
- expect(service_instance.execute).to eq({ status: :error, message: "Gitlab::Json::LimitedEncoder::LimitExceeded" })
+ expect(service_instance.execute).to have_attributes(
+ status: :error,
+ message: 'Gitlab::Json::LimitedEncoder::LimitExceeded'
+ )
end
end
it 'handles 200 status code' do
stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success')
- expect(service_instance.execute).to include({ status: :success, http_status: 200, message: 'Success' })
+ expect(service_instance.execute).to have_attributes(
+ status: :success,
+ payload: { http_status: 200 },
+ message: 'Success'
+ )
end
it 'handles 2xx status codes' do
stub_full_request(project_hook.url, method: :post).to_return(status: 201, body: 'Success')
- expect(service_instance.execute).to include({ status: :success, http_status: 201, message: 'Success' })
+ expect(service_instance.execute).to have_attributes(
+ status: :success,
+ payload: { http_status: 201 },
+ message: 'Success'
+ )
end
context 'execution logging' do
diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb
index c0bcf9b606d..1bd7e15db67 100644
--- a/spec/services/work_items/create_service_spec.rb
+++ b/spec/services/work_items/create_service_spec.rb
@@ -179,16 +179,6 @@ RSpec.describe WorkItems::CreateService do
let(:error_message) { 'only Issue and Incident can be parent of Task.' }
end
end
-
- context 'when hierarchy feature flag is disabled' do
- before do
- stub_feature_flags(work_items_hierarchy: false)
- end
-
- it_behaves_like 'fails creating work item and returns errors' do
- let(:error_message) { '`work_items_hierarchy` feature flag disabled for this project' }
- end
- end
end
context 'when user cannot admin parent link' do
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 1761d1104dd..68efb4c220b 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -311,6 +311,34 @@ RSpec.describe WorkItems::UpdateService do
end
end
end
+
+ context 'for milestone widget' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+
+ let(:widget_params) { { milestone_widget: { milestone_id: milestone.id } } }
+
+ context 'when milestone is updated' do
+ it "triggers 'issuableMilestoneUpdated'" do
+ expect(work_item.milestone).to eq(nil)
+ expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(work_item).and_call_original
+
+ update_work_item
+ end
+ end
+
+ context 'when milestone remains unchanged' do
+ before do
+ update_work_item
+ end
+
+ it "does not trigger 'issuableMilestoneUpdated'" do
+ expect(work_item.milestone).to eq(milestone)
+ expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated)
+
+ update_work_item
+ end
+ end
+ end
end
describe 'label updates' do
diff --git a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
index 9a425d5308c..1b8c4c5f15f 100644
--- a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
+++ b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb
@@ -42,18 +42,6 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
let_it_be(:child_work_item3) { create(:work_item, :task, project: project) }
let_it_be(:child_work_item4) { create(:work_item, :task, project: project) }
- context 'when work_items_hierarchy feature flag is disabled' do
- let(:params) { { children: [child_work_item4] } }
-
- before do
- stub_feature_flags(work_items_hierarchy: false)
- end
-
- it_behaves_like 'raises a WidgetError' do
- let(:message) { '`work_items_hierarchy` feature flag disabled for this project' }
- end
- end
-
context 'when user has insufficient permissions to link work items' do
let(:params) { { children: [child_work_item4] } }
@@ -105,16 +93,6 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do
let(:params) { { parent: parent_work_item } }
- context 'when work_items_hierarchy feature flag is disabled' do
- before do
- stub_feature_flags(work_items_hierarchy: false)
- end
-
- it_behaves_like 'raises a WidgetError' do
- let(:message) { '`work_items_hierarchy` feature flag disabled for this project' }
- end
- end
-
context 'when user has insufficient permissions to link work items' do
it_behaves_like 'raises a WidgetError' do
let(:message) { not_found_error }
diff --git a/spec/services/work_items/widgets/milestone_service/create_service_spec.rb b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
new file mode 100644
index 00000000000..3f90784b703
--- /dev/null
+++ b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::MilestoneService::CreateService do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:project_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:guest) { create(:user) }
+
+ let(:current_user) { guest }
+ let(:work_item) { build(:work_item, project: project, updated_at: 1.day.ago) }
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
+ let(:service) { described_class.new(widget: widget, current_user: current_user) }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ describe '#before_create_callback' do
+ it_behaves_like "setting work item's milestone" do
+ subject(:execute_callback) do
+ service.before_create_callback(params: params)
+ end
+ end
+ end
+end
diff --git a/spec/services/work_items/widgets/milestone_service/update_service_spec.rb b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
new file mode 100644
index 00000000000..f3a7fc156b9
--- /dev/null
+++ b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::Widgets::MilestoneService::UpdateService do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:project_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+
+ let(:work_item) { create(:work_item, project: project, updated_at: 1.day.ago) }
+ let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
+ let(:service) { described_class.new(widget: widget, current_user: current_user) }
+
+ before do
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+ end
+
+ describe '#before_update_callback' do
+ context 'when current user is not allowed to set work item metadata' do
+ let(:current_user) { guest }
+ let(:params) { { milestone_id: group_milestone.id } }
+
+ it "does not set the work item's milestone" do
+ expect { service.before_update_callback(params: params) }
+ .to not_change(work_item, :milestone)
+ end
+ end
+
+ context "when current user is allowed to set work item metadata" do
+ let(:current_user) { reporter }
+
+ it_behaves_like "setting work item's milestone" do
+ subject(:execute_callback) do
+ service.before_update_callback(params: params)
+ end
+ end
+
+ context 'when unsetting a milestone' do
+ let(:params) { { milestone_id: nil } }
+
+ before do
+ work_item.update!(milestone: project_milestone)
+ end
+
+ it "sets the work item's milestone" do
+ expect { service.before_update_callback(params: params) }
+ .to change(work_item, :milestone)
+ .from(project_milestone)
+ .to(nil)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/x509_certificate_revoke_service_spec.rb b/spec/services/x509_certificate_revoke_service_spec.rb
index adad3281c13..ff5d2dc058b 100644
--- a/spec/services/x509_certificate_revoke_service_spec.rb
+++ b/spec/services/x509_certificate_revoke_service_spec.rb
@@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe X509CertificateRevokeService do
describe '#execute' do
let(:service) { described_class.new }
- let!(:x509_signature_1) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified ) }
- let!(:x509_signature_2) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified ) }
+ let!(:x509_signature_1) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified) }
+ let!(:x509_signature_2) { create(:x509_commit_signature, x509_certificate: x509_certificate, verification_status: :verified) }
context 'for revoked certificates' do
- let(:x509_certificate) { create(:x509_certificate, certificate_status: :revoked ) }
+ let(:x509_certificate) { create(:x509_certificate, certificate_status: :revoked) }
it 'update all commit signatures' do
expect do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 8a1fa486bde..8e73073e68b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -65,7 +65,6 @@ require_relative('../jh/spec/spec_helper') if Gitlab.jh?
# Requires helpers, and shared contexts/examples first since they're used in other support files
# Load these first since they may be required by other helpers
-require Rails.root.join("spec/support/helpers/git_helpers.rb")
require Rails.root.join("spec/support/helpers/stub_requests.rb")
# Then the rest
@@ -145,7 +144,7 @@ RSpec.configure do |config|
config.include NextInstanceOf
config.include TestEnv
config.include FileReadHelpers
- config.include Database::MultipleDatabases
+ config.include Database::MultipleDatabasesHelpers
config.include Database::WithoutCheckConstraint
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
@@ -256,10 +255,6 @@ RSpec.configure do |config|
# The survey popover can block the diffs causing specs to fail
stub_feature_flags(mr_experience_survey: false)
- # Merge request widget GraphQL requests are disabled in the tests
- # for now whilst we migrate as much as we can over the GraphQL
- # stub_feature_flags(merge_request_widget_graphql: false)
-
# Using FortiAuthenticator as OTP provider is disabled by default in
# tests, until we introduce it in user settings
stub_feature_flags(forti_authenticator: false)
@@ -311,6 +306,10 @@ RSpec.configure do |config|
# See https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor
stub_feature_flags(legacy_merge_request_state_check_for_merged_result_pipelines: false)
+ # Disable the `vue_issues_dashboard` feature flag in specs as we migrate the issues
+ # dashboard page to Vue. https://gitlab.com/gitlab-org/gitlab/-/issues/379025
+ stub_feature_flags(vue_issues_dashboard: false)
+
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags
diff --git a/spec/support/database/query_recorder.rb b/spec/support/database/query_recorder.rb
new file mode 100644
index 00000000000..1050120e528
--- /dev/null
+++ b/spec/support/database/query_recorder.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ # Truncate the query_recorder log file before starting the suite
+ config.before(:suite) do
+ log_path = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder::LOG_FILE)
+ File.write(log_path, '') if File.exist?(log_path)
+ end
+end
diff --git a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
index e02bf66507a..e9a13f7bf63 100644
--- a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
+++ b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
@@ -10,8 +10,8 @@ RSpec.shared_examples 'a correct instrumented metric value' do |params|
end
before do
- if described_class.respond_to?(:relation) && described_class.relation.respond_to?(:connection)
- allow(described_class.relation.connection).to receive(:transaction_open?).and_return(false)
+ if metric.respond_to?(:relation, true) && metric.send(:relation).respond_to?(:connection)
+ allow(metric.send(:relation).connection).to receive(:transaction_open?).and_return(false)
end
end
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
index 840f948e377..b9752577c76 100644
--- a/spec/support/google_api/cloud_platform_helpers.rb
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -157,7 +157,7 @@ module GoogleApi
def cloud_platform_projects_billing_info_body(project_id, billing_enabled)
{
"name": "projects/#{project_id}/billingInfo",
- "projectId": "#{project_id}",
+ "projectId": project_id.to_s,
"billingAccountName": "account-name",
"billingEnabled": billing_enabled
}
diff --git a/spec/support/helpers/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb
deleted file mode 100644
index e29e12a15f6..00000000000
--- a/spec/support/helpers/bare_repo_operations.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'zlib'
-
-class BareRepoOperations
- include Gitlab::Popen
-
- def initialize(path_to_repo)
- @path_to_repo = path_to_repo
- end
-
- def commit_tree(tree_id, msg, parent: Gitlab::Git::EMPTY_TREE_ID)
- commit_tree_args = ['commit-tree', tree_id, '-m', msg]
- commit_tree_args += ['-p', parent] unless parent == Gitlab::Git::EMPTY_TREE_ID
- commit_id = execute(commit_tree_args)
-
- commit_id[0]
- end
-
- private
-
- def execute(args, allow_failure: false)
- output, status = popen(base_args + args, nil) do |stdin|
- yield stdin if block_given?
- end
-
- unless status == 0
- if allow_failure
- return []
- else
- raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
- end
- end
-
- output.split("\n")
- end
-
- def base_args
- [
- Gitlab.config.git.bin_path,
- "--git-dir=#{@path_to_repo}"
- ]
- end
-end
diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb
index 2e9b6f748cd..2cdd242ac22 100644
--- a/spec/support/helpers/ci/template_helpers.rb
+++ b/spec/support/helpers/ci/template_helpers.rb
@@ -5,6 +5,51 @@ module Ci
def template_registry_host
'registry.gitlab.com'
end
+
+ def public_image_exist?(registry, repository, image)
+ public_image_manifest(registry, repository, image).present?
+ end
+
+ def public_image_manifest(registry, repository, reference)
+ token = public_image_repository_token(registry, repository)
+
+ response = with_net_connect_allowed do
+ Gitlab::HTTP.get(image_manifest_url(registry, repository, reference),
+ headers: { 'Authorization' => "Bearer #{token}" })
+ end
+
+ return unless response.success?
+
+ Gitlab::Json.parse(response.body)
+ end
+
+ def public_image_repository_token(registry, repository)
+ @public_image_repository_tokens ||= {}
+ @public_image_repository_tokens[[registry, repository]] ||=
+ begin
+ response = with_net_connect_allowed do
+ Gitlab::HTTP.get(image_manifest_url(registry, repository, 'latest'))
+ end
+
+ return unless response.unauthorized?
+
+ www_authenticate = response.headers['www-authenticate']
+ return unless www_authenticate
+
+ realm, service, scope = www_authenticate.split(',').map { |s| s[/\w+="(.*)"/, 1] }
+ token_response = with_net_connect_allowed do
+ Gitlab::HTTP.get(realm, query: { service: service, scope: scope })
+ end
+
+ return unless token_response.success?
+
+ token_response['token']
+ end
+ end
+
+ def image_manifest_url(registry, repository, reference)
+ "#{registry}/v2/#{repository}/manifests/#{reference}"
+ end
end
end
diff --git a/spec/support/helpers/content_security_policy_helpers.rb b/spec/support/helpers/content_security_policy_helpers.rb
index c9f15e65c74..230075ead70 100644
--- a/spec/support/helpers/content_security_policy_helpers.rb
+++ b/spec/support/helpers/content_security_policy_helpers.rb
@@ -1,20 +1,14 @@
# frozen_string_literal: true
module ContentSecurityPolicyHelpers
- # Expecting 2 calls to current_content_security_policy by default, once for
- # the call that's being tested and once for the call in ApplicationController
- def setup_csp_for_controller(controller_class, times = 2)
+ # Expecting 2 calls to current_content_security_policy by default:
+ # 1. call that's being tested
+ # 2. call in ApplicationController
+ def setup_csp_for_controller(controller_class, csp = ActionDispatch::ContentSecurityPolicy.new, times: 2)
expect_next_instance_of(controller_class) do |controller|
- expect(controller).to receive(:current_content_security_policy)
- .and_return(ActionDispatch::ContentSecurityPolicy.new).exactly(times).times
- end
- end
-
- # Expecting 2 calls to current_content_security_policy by default, once for
- # the call that's being tested and once for the call in ApplicationController
- def setup_existing_csp_for_controller(controller_class, csp, times = 2)
- expect_next_instance_of(controller_class) do |controller|
- expect(controller).to receive(:current_content_security_policy).and_return(csp).exactly(times).times
+ expect(controller)
+ .to receive(:current_content_security_policy).exactly(times).times
+ .and_return(csp)
end
end
end
diff --git a/spec/support/database/multiple_databases.rb b/spec/support/helpers/database/multiple_databases_helpers.rb
index b6341c2caec..16f5168ca29 100644
--- a/spec/support/database/multiple_databases.rb
+++ b/spec/support/helpers/database/multiple_databases_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Database
- module MultipleDatabases
+ module MultipleDatabasesHelpers
def skip_if_multiple_databases_not_setup
skip 'Skipping because multiple databases not set up' unless Gitlab::Database.has_config?(:ci)
end
@@ -52,17 +52,17 @@ module Database
#
# rubocop:disable Database/MultipleDatabases
def with_reestablished_active_record_base(reconnect: true)
- connection_classes = ActiveRecord::Base.connection_handler.connection_pool_names.map(&:constantize).to_h do |klass|
- [klass, klass.connection_db_config]
- end
+ connection_classes = ActiveRecord::Base
+ .connection_handler
+ .connection_pool_names
+ .map(&:constantize)
+ .index_with(&:connection_db_config)
original_handler = ActiveRecord::Base.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
ActiveRecord::Base.connection_handler = new_handler
- if reconnect
- connection_classes.each { |klass, db_config| klass.establish_connection(db_config) }
- end
+ connection_classes.each { |klass, db_config| klass.establish_connection(db_config) } if reconnect
yield
ensure
@@ -95,9 +95,12 @@ module Database
module ActiveRecordBaseEstablishConnection
def establish_connection(*args)
# rubocop:disable Database/MultipleDatabases
- if connected? && connection&.transaction_open? && ActiveRecord::Base.connection_handler == ActiveRecord::Base.default_connection_handler
- raise "Cannot re-establish '#{self}.establish_connection' within an open transaction (#{connection&.open_transactions.to_i}). " \
- "Use `with_reestablished_active_record_base` instead or add `:reestablished_active_record_base` to rspec context."
+ if connected? &&
+ connection&.transaction_open? &&
+ ActiveRecord::Base.connection_handler == ActiveRecord::Base.default_connection_handler
+ raise "Cannot re-establish '#{self}.establish_connection' within an open transaction " \
+ "(#{connection&.open_transactions.to_i}). Use `with_reestablished_active_record_base` " \
+ "instead or add `:reestablished_active_record_base` to rspec context."
end
# rubocop:enable Database/MultipleDatabases
@@ -106,56 +109,4 @@ module Database
end
end
-RSpec.configure do |config|
- # Ensure database versions are memoized to prevent query counts from
- # being affected by version checks. Note that
- # Gitlab::Database.check_postgres_version_and_print_warning is called
- # at startup, but that generates its own
- # `Gitlab::Database::Reflection` so the result is not memoized by
- # callers of `ApplicationRecord.database.version`, such as
- # `Gitlab::Database::AsWithMaterialized.materialized_supported?`.
- # TODO This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/325639 is completed.
- [ApplicationRecord, ::Ci::ApplicationRecord].each { |record| record.database.version }
-
- config.around(:each, :reestablished_active_record_base) do |example|
- with_reestablished_active_record_base(reconnect: example.metadata.fetch(:reconnect, true)) do
- example.run
- end
- end
-
- config.around(:each, :add_ci_connection) do |example|
- with_added_ci_connection do
- example.run
- end
- end
-
- config.append_after(:context, :migration) do
- recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables
- end
-
- config.around(:each, :migration) do |example|
- self.class.use_transactional_tests = false
-
- migration_schema = example.metadata[:migration]
- migration_schema = :gitlab_main if migration_schema == true
- base_model = Gitlab::Database.schemas_to_base_models.fetch(migration_schema).first
-
- # Migration require an `ActiveRecord::Base` to point to desired database
- if base_model != ActiveRecord::Base
- with_reestablished_active_record_base do
- reconfigure_db_connection(
- model: ActiveRecord::Base,
- config_model: base_model
- )
-
- example.run
- end
- else
- example.run
- end
-
- self.class.use_transactional_tests = true
- end
-end
-
ActiveRecord::Base.singleton_class.prepend(::Database::ActiveRecordBaseEstablishConnection) # rubocop:disable Database/MultipleDatabases
diff --git a/spec/support/helpers/features/access_token_helpers.rb b/spec/support/helpers/features/access_token_helpers.rb
new file mode 100644
index 00000000000..f4bdb70c160
--- /dev/null
+++ b/spec/support/helpers/features/access_token_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+module Spec
+ module Support
+ module Helpers
+ module AccessTokenHelpers
+ def active_access_tokens
+ find("[data-testid='active-tokens']")
+ end
+
+ def created_access_token
+ within('[data-testid=access-token-section]') do
+ find('[data-testid=toggle-visibility-button]').click
+ find_field('new-access-token').value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb
index 9cce9c4882d..a24b99bbe61 100644
--- a/spec/support/helpers/features/releases_helpers.rb
+++ b/spec/support/helpers/features/releases_helpers.rb
@@ -39,7 +39,7 @@ module Spec
wait_for_all_requests
- click_button("#{branch_name}")
+ click_button(branch_name.to_s)
end
end
diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
index ca844b33ba8..7beed9c7755 100644
--- a/spec/support/helpers/filter_spec_helper.rb
+++ b/spec/support/helpers/filter_spec_helper.rb
@@ -90,10 +90,11 @@ module FilterSpecHelper
#
# Returns a String
def invalidate_reference(reference)
- if reference =~ /\A(.+)?[^\d]\d+\z/
+ case reference
+ when /\A(.+)?[^\d]\d+\z/
# Integer-based reference with optional project prefix
reference.gsub(/\d+\z/) { |i| i.to_i + 10_000 }
- elsif reference =~ /\A(.+@)?(\h{7,40}\z)/
+ when /\A(.+@)?(\h{7,40}\z)/
# SHA-based reference with optional prefix
reference.gsub(/\h{7,40}\z/) { |v| v.reverse }
else
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 93122ca3d0c..677cea7b804 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -254,6 +254,10 @@ module FilteredSearchHelpers
expect(page).to have_css '.gl-filtered-search-token', text: "Assignee = #{value}"
end
+ def expect_unioned_assignee_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Assignee is one of #{value}"
+ end
+
def expect_author_token(value)
expect(page).to have_css '.gl-filtered-search-token', text: "Author = #{value}"
end
diff --git a/spec/support/helpers/full_name_helper.rb b/spec/support/helpers/full_name_helper.rb
new file mode 100644
index 00000000000..a41c0da74d4
--- /dev/null
+++ b/spec/support/helpers/full_name_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module FullNameHelper
+ def full_name(first_name, last_name)
+ "#{first_name} #{last_name}"
+ end
+end
+
+FullNameHelper.prepend_mod
diff --git a/spec/support/helpers/git_helpers.rb b/spec/support/helpers/git_helpers.rb
deleted file mode 100644
index 72bba419116..00000000000
--- a/spec/support/helpers/git_helpers.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module GitHelpers
- def rugged_repo(repository)
- path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(TestEnv.repos_path, repository.disk_path + '.git')
- end
-
- Rugged::Repository.new(path)
- end
-end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index b2fc6ae3286..bd0efc96bd8 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -378,7 +378,7 @@ module GraphqlHelpers
def field_with_params(name, attributes = {})
namerized = GraphqlHelpers.fieldnamerize(name.to_s)
- return "#{namerized}" if attributes.blank?
+ return namerized.to_s if attributes.blank?
field_params = if attributes.is_a?(Hash)
"(#{attributes_to_graphql(attributes)})"
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 912e7d24b25..72524453f34 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -142,6 +142,29 @@ module KubernetesHelpers
WebMock.stub_request(method, ingresses_url).to_return(response)
end
+ def stub_server_min_version_failed_request
+ WebMock.stub_request(:get, service.api_url + '/version').to_return(
+ status: [500, "Internal Server Error"],
+ body: {}.to_json)
+ end
+
+ def stub_server_min_version(min_version)
+ response = kube_response({
+ "major": "1", # not used, just added here to be a bit more realistic purposes
+ "minor": min_version.to_s
+ })
+
+ WebMock.stub_request( :get, service.api_url + '/version')
+ .with(
+ headers: {
+ 'Accept' => '*/*',
+ 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
+ 'Authorization' => 'Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ 'User-Agent' => 'Ruby'
+ })
+ .to_return(response)
+ end
+
def stub_kubeclient_knative_services(options = {})
namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
@@ -537,7 +560,7 @@ module KubernetesHelpers
},
"spec" => {
"containers" => [
- { "name" => "#{container_name}" },
+ { "name" => container_name.to_s },
{ "name" => "#{container_name}-1" }
]
},
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index b44552d6479..e1ed3ffacec 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -90,7 +90,11 @@ module NavbarStructureHelper
_('Kubernetes'),
new_nav_item: {
nav_item: _('Observability'),
- nav_sub_items: []
+ nav_sub_items: [
+ _('Dashboards'),
+ _('Explore'),
+ _('Manage Dashboards')
+ ]
}
)
end
diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb
index b9796ebbe62..370dedabd9b 100644
--- a/spec/support/helpers/reference_parser_helpers.rb
+++ b/spec/support/helpers/reference_parser_helpers.rb
@@ -12,14 +12,14 @@ module ReferenceParserHelpers
end
RSpec.shared_examples 'no project N+1 queries' do
- it 'avoids N+1 queries in #nodes_visible_to_user' do
+ it 'avoids N+1 queries in #nodes_visible_to_user', :use_sql_query_cache do
context = Banzai::RenderContext.new(project, user)
request = lambda do |links|
described_class.new(context).nodes_visible_to_user(user, links)
end
- control = ActiveRecord::QueryRecorder.new { request.call(control_links) }
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { request.call(control_links) }
create(:group_member, group: project.group) if project.group
create(:project_member, project: project)
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index 581ef07752e..7d0f8c09933 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -33,13 +33,13 @@ module SearchHelpers
end
def select_search_scope(scope)
- page.within '.search-filter' do
+ page.within '[data-testid="search-filter"]' do
click_link scope
end
end
def has_search_scope?(scope)
- page.within '.search-filter' do
+ page.within '[data-testid="search-filter"]' do
has_link?(scope)
end
end
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index f41457d2420..24c768258a1 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -38,6 +38,10 @@ module StubConfiguration
allow(Rails.application.routes).to receive(:default_url_options).and_return(url_options)
end
+ def stub_dependency_proxy_setting(messages)
+ allow(Gitlab.config.dependency_proxy).to receive_messages(to_settings(messages))
+ end
+
def stub_gravatar_setting(messages)
allow(Gitlab.config.gravatar).to receive_messages(to_settings(messages))
end
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index f1654e55b7e..e301e29afc2 100644
--- a/spec/support/helpers/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
@@ -37,6 +37,10 @@ module StubFeatureFlags
# Enable `ci_live_trace` feature flag only on the specified projects.
def stub_feature_flags(features)
features.each do |feature_name, actors|
+ unless Feature::Definition.get(feature_name)
+ ActiveSupport::Deprecation.warn "Invalid Feature Flag #{feature_name} stubbed"
+ end
+
# Remove feature flag overwrite
feature = Feature.get(feature_name) # rubocop:disable Gitlab/AvoidFeatureGet
feature.remove
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index c58353558df..e1b461cf37e 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -11,6 +11,8 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
'signed-commits' => 'c7794c1',
+ 'gpg-signed' => '8a852d5',
+ 'x509-signed' => 'a4df3c8',
'not-merged-branch' => 'b83d6e3',
'branch-merged' => '498214d',
'empty-branch' => '7efb185',
@@ -43,7 +45,7 @@ module TestEnv
'video' => '8879059',
'crlf-diff' => '5938907',
'conflict-start' => '824be60',
- 'conflict-resolvable' => '1450cd6',
+ 'conflict-resolvable' => '1450cd639e0bc6721eb02800169e464f212cde06',
'conflict-binary-file' => '259a6fb',
'conflict-contains-conflict-markers' => '78a3086',
'conflict-missing-side' => 'eb227b3',
@@ -282,15 +284,30 @@ module TestEnv
unless File.directory?(repo_path)
start = Time.now
system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
- system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} remote remove origin))
puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n"
end
- set_repo_refs(repo_path, refs)
+ create_bundle = !File.file?(repo_bundle_path)
- unless File.file?(repo_bundle_path)
+ unless set_repo_refs(repo_path, refs)
+ # Prefer not to fetch over the network. Only fetch when we have failed to
+ # set all the required local branches. This would happen when a new
+ # branch is added to BRANCH_SHA, in which case we want to update
+ # everything.
+ unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
+ raise 'Could not fetch test seed repository.'
+ end
+
+ unless set_repo_refs(repo_path, refs)
+ raise "Could not update test seed repository, please delete #{repo_path} and try again"
+ end
+
+ create_bundle = true
+ end
+
+ if create_bundle
start = Time.now
- system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --all))
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --exclude refs/remotes/* --all))
puts "==> #{repo_bundle_path} generated in #{Time.now - start} seconds...\n"
end
end
@@ -392,20 +409,13 @@ module TestEnv
end
def set_repo_refs(repo_path, branch_sha)
- instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
- update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
- reset = proc do
- Dir.chdir(repo_path) do
- IO.popen(update_refs, "w") { |io| io.write(instructions) }
- $?.success?
+ IO.popen(%W[#{Gitlab.config.git.bin_path} -C #{repo_path} update-ref --stdin -z], "w") do |io|
+ branch_sha.each do |branch, sha|
+ io.write("update refs/heads/#{branch}\x00#{sha}\x00\x00")
end
end
- # Try to reset without fetching to avoid using the network.
- unless reset.call
- raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
- raise "Could not update test seed repository, please delete #{repo_path} and try again" unless reset.call
- end
+ $?.success?
end
def component_timed_setup(component, install_dir:, version:, task:, fresh_install: true, task_args: [])
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index b4f0cbd8527..92a946db337 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -67,24 +67,12 @@ module UsageDataHelpers
projects_with_repositories_enabled
projects_with_error_tracking_enabled
projects_with_enabled_alert_integrations
- projects_with_expiration_policy_enabled
- projects_with_expiration_policy_enabled_with_keep_n_unset
- projects_with_expiration_policy_enabled_with_keep_n_set_to_1
- projects_with_expiration_policy_enabled_with_keep_n_set_to_5
- projects_with_expiration_policy_enabled_with_keep_n_set_to_10
- projects_with_expiration_policy_enabled_with_keep_n_set_to_25
- projects_with_expiration_policy_enabled_with_keep_n_set_to_50
projects_with_expiration_policy_enabled_with_older_than_unset
projects_with_expiration_policy_enabled_with_older_than_set_to_7d
projects_with_expiration_policy_enabled_with_older_than_set_to_14d
projects_with_expiration_policy_enabled_with_older_than_set_to_30d
projects_with_expiration_policy_enabled_with_older_than_set_to_60d
projects_with_expiration_policy_enabled_with_older_than_set_to_90d
- projects_with_expiration_policy_enabled_with_cadence_set_to_1d
- projects_with_expiration_policy_enabled_with_cadence_set_to_7d
- projects_with_expiration_policy_enabled_with_cadence_set_to_14d
- projects_with_expiration_policy_enabled_with_cadence_set_to_1month
- projects_with_expiration_policy_enabled_with_cadence_set_to_3month
projects_with_terraform_reports
projects_with_terraform_states
pages_domains
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index bfcaf9552b3..6d7658b7c33 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -333,7 +333,7 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do
or_fewer_msg = "or fewer" if @or_fewer
threshold_msg = "(+/- #{threshold})" unless threshold == 0
- ["#{expected_count}", or_fewer_msg, threshold_msg].compact.join(' ')
+ [expected_count.to_s, or_fewer_msg, threshold_msg].compact.join(' ')
end
def skip_cached
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index db7d4269945..155a6dba52c 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -169,7 +169,11 @@ RSpec::Matchers.define :have_graphql_type do |expected, opts = {}|
include GraphQLTypeHelpers
match do |object|
- expect(object.type).to eq(nullified(expected, opts[:null]))
+ if object.type.list?
+ expect(object.type.unwrap).to eq(nullified(expected, opts[:null]))
+ else
+ expect(object.type).to eq(nullified(expected, opts[:null]))
+ end
end
failure_message do |object|
diff --git a/spec/support/migration.rb b/spec/support/migration.rb
index 490aa836d74..2a69630a29a 100644
--- a/spec/support/migration.rb
+++ b/spec/support/migration.rb
@@ -16,14 +16,42 @@ RSpec.configure do |config|
schema_migrate_down!
end
+ config.after(:context, :migration) do
+ Gitlab::CurrentSettings.clear_in_memory_application_settings!
+ end
+
+ config.append_after(:context, :migration) do
+ recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables
+ end
+
+ config.around(:each, :migration) do |example|
+ self.class.use_transactional_tests = false
+
+ migration_schema = example.metadata[:migration]
+ migration_schema = :gitlab_main if migration_schema == true
+ base_model = Gitlab::Database.schemas_to_base_models.fetch(migration_schema).first
+
+ # Migration require an `ActiveRecord::Base` to point to desired database
+ if base_model != ActiveRecord::Base
+ with_reestablished_active_record_base do
+ reconfigure_db_connection(
+ model: ActiveRecord::Base,
+ config_model: base_model
+ )
+
+ example.run
+ end
+ else
+ example.run
+ end
+
+ self.class.use_transactional_tests = true
+ end
+
# Each example may call `migrate!`, so we must ensure we are migrated down every time
config.before(:each, :migration) do
use_fake_application_settings
schema_migrate_down!
end
-
- config.after(:context, :migration) do
- Gitlab::CurrentSettings.clear_in_memory_application_settings!
- end
end
diff --git a/spec/support/models/partitionable_check.rb b/spec/support/models/ci/partitioning_testing/cascade_check.rb
index 2c09c1b3408..f553a47ef4f 100644
--- a/spec/support/models/partitionable_check.rb
+++ b/spec/support/models/ci/partitioning_testing/cascade_check.rb
@@ -10,37 +10,18 @@ module PartitioningTesting
def check_partition_cascade_value
raise 'Partition value not found' unless partition_scope_value
- raise 'Default value detected' if partition_id == 100
return if partition_id == partition_scope_value
raise "partition_id was expected to equal #{partition_scope_value} but it was #{partition_id}."
end
end
-
- module DefaultPartitionValue
- extend ActiveSupport::Concern
-
- class_methods do
- def current_partition_value
- current = super
-
- if current == 100
- 54321
- else
- current
- end
- end
- end
- end
end
Ci::Partitionable::Testing::PARTITIONABLE_MODELS.each do |klass|
+ next if klass == 'Ci::Pipeline'
+
model = klass.safe_constantize
- if klass == 'Ci::Pipeline'
- model.prepend(PartitioningTesting::DefaultPartitionValue)
- else
- model.include(PartitioningTesting::CascadeCheck)
- end
+ model.include(PartitioningTesting::CascadeCheck)
end
diff --git a/spec/support/models/ci/partitioning_testing/partition_identifiers.rb b/spec/support/models/ci/partitioning_testing/partition_identifiers.rb
new file mode 100644
index 00000000000..aa091095fb6
--- /dev/null
+++ b/spec/support/models/ci/partitioning_testing/partition_identifiers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ module PartitioningTesting
+ module PartitionIdentifiers
+ module_function
+
+ def ci_testing_partition_id
+ 99999
+ end
+ end
+ end
+end
diff --git a/spec/support/models/ci/partitioning_testing/rspec_hooks.rb b/spec/support/models/ci/partitioning_testing/rspec_hooks.rb
new file mode 100644
index 00000000000..39b15ba8721
--- /dev/null
+++ b/spec/support/models/ci/partitioning_testing/rspec_hooks.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.include Ci::PartitioningTesting::PartitionIdentifiers
+
+ config.around(:each, :ci_partitionable) do |example|
+ Ci::PartitioningTesting::SchemaHelpers.with_routing_tables do
+ example.run
+ end
+ end
+
+ config.before(:all) do
+ Ci::PartitioningTesting::SchemaHelpers.setup
+ end
+
+ config.after(:all) do
+ Ci::PartitioningTesting::SchemaHelpers.teardown
+ end
+end
diff --git a/spec/support/models/ci/partitioning_testing/schema_helpers.rb b/spec/support/models/ci/partitioning_testing/schema_helpers.rb
new file mode 100644
index 00000000000..712178710da
--- /dev/null
+++ b/spec/support/models/ci/partitioning_testing/schema_helpers.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module Ci
+ module PartitioningTesting
+ module SchemaHelpers
+ DEFAULT_PARTITION = 100
+
+ module_function
+
+ def with_routing_tables
+ Ci::BuildMetadata.table_name = :p_ci_builds_metadata
+ yield
+ ensure
+ Ci::BuildMetadata.table_name = :ci_builds_metadata
+ end
+
+ # We're dropping the default values here to ensure that the application code
+ # populates the `partition_id` value and it's not falling back on the
+ # database default one. We should be able to clean this up after
+ # partitioning the tables and substituting the routing table in the model:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/377822
+ #
+ def setup(connection: Ci::ApplicationRecord.connection)
+ each_partitionable_table do |table_name|
+ change_column_default(table_name, from: DEFAULT_PARTITION, to: nil, connection: connection)
+ change_column_default("p_#{table_name}", from: DEFAULT_PARTITION, to: nil, connection: connection)
+ create_test_partition(table_name, connection: connection)
+ end
+ end
+
+ def teardown(connection: Ci::ApplicationRecord.connection)
+ each_partitionable_table do |table_name|
+ drop_test_partition(table_name, connection: connection)
+ change_column_default(table_name, from: nil, to: DEFAULT_PARTITION, connection: connection)
+ change_column_default("p_#{table_name}", from: nil, to: DEFAULT_PARTITION, connection: connection)
+ end
+ end
+
+ def each_partitionable_table
+ ::Ci::Partitionable::Testing::PARTITIONABLE_MODELS.each do |klass|
+ model = klass.safe_constantize
+ table_name = model.table_name.delete_prefix('p_')
+
+ yield(table_name)
+
+ model.reset_column_information if model.connected?
+ end
+ end
+
+ def change_column_default(table_name, from:, to:, connection:)
+ return unless table_available?(table_name, connection: connection)
+
+ connection.change_column_default(table_name, :partition_id, from: from, to: to)
+ end
+
+ def create_test_partition(table_name, connection:)
+ return unless table_available?("p_#{table_name}", connection: connection)
+
+ drop_test_partition(table_name, connection: connection)
+
+ connection.execute(<<~SQL)
+ CREATE TABLE #{full_partition_name(table_name)}
+ PARTITION OF p_#{table_name}
+ FOR VALUES IN (#{PartitioningTesting::PartitionIdentifiers.ci_testing_partition_id});
+ SQL
+ end
+
+ def drop_test_partition(table_name, connection:)
+ return unless table_available?(table_name, connection: connection)
+
+ connection.execute(<<~SQL)
+ DROP TABLE IF EXISTS #{full_partition_name(table_name)};
+ SQL
+ end
+
+ def table_available?(table_name, connection:)
+ connection.table_exists?(table_name) &&
+ connection.column_exists?(table_name, :partition_id)
+ end
+
+ def full_partition_name(table_name)
+ "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_#{table_name}_partition"
+ end
+ end
+ end
+end
diff --git a/spec/support/multiple_databases.rb b/spec/support/multiple_databases.rb
new file mode 100644
index 00000000000..616cf00269c
--- /dev/null
+++ b/spec/support/multiple_databases.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ # Ensure database versions are memoized to prevent query counts from
+ # being affected by version checks. Note that
+ # Gitlab::Database.check_postgres_version_and_print_warning is called
+ # at startup, but that generates its own
+ # `Gitlab::Database::Reflection` so the result is not memoized by
+ # callers of `ApplicationRecord.database.version`, such as
+ # `Gitlab::Database::AsWithMaterialized.materialized_supported?`.
+ # TODO This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/325639 is completed.
+ [ApplicationRecord, ::Ci::ApplicationRecord].each { |record| record.database.version }
+
+ config.around(:each, :reestablished_active_record_base) do |example|
+ with_reestablished_active_record_base(reconnect: example.metadata.fetch(:reconnect, true)) do
+ example.run
+ end
+ end
+
+ config.around(:each, :add_ci_connection) do |example|
+ with_added_ci_connection do
+ example.run
+ end
+ end
+end
diff --git a/spec/support/rate_limiter.rb b/spec/support/rate_limiter.rb
new file mode 100644
index 00000000000..525d593c293
--- /dev/null
+++ b/spec/support/rate_limiter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before(:each, :disable_rate_limiter) do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
+ end
+end
diff --git a/spec/support/redis.rb b/spec/support/redis.rb
index d00d6562966..6d313c8aa16 100644
--- a/spec/support/redis.rb
+++ b/spec/support/redis.rb
@@ -19,4 +19,10 @@ RSpec.configure do |config|
public_send("redis_#{underscored_name}_cleanup!")
end
end
+
+ config.before(:suite) do
+ Gitlab::Redis::ALL_CLASSES.each do |instance_class|
+ instance_class.with(&:flushdb)
+ end
+ end
end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 6795d2f6d2a..71dfc3fd5a3 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -11,6 +11,9 @@ require_relative "helpers/fast_rails_root"
RSpec::Expectations.configuration.on_potential_false_positives = :raise
RSpec.configure do |config|
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/379686
+ config.threadsafe = false
+
# Re-run failures locally with `--only-failures`
config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt')
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 30220b04fa2..67b7023f1ff 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -997,7 +997,6 @@
- './ee/spec/graphql/types/vulnerability_severity_enum_spec.rb'
- './ee/spec/graphql/types/vulnerability_sort_enum_spec.rb'
- './ee/spec/graphql/types/vulnerability_state_enum_spec.rb'
-- './ee/spec/graphql/types/vulnerability_type_spec.rb'
- './ee/spec/graphql/types/vulnerable_dependency_type_spec.rb'
- './ee/spec/graphql/types/vulnerable_kubernetes_resource_type_spec.rb'
- './ee/spec/graphql/types/vulnerable_package_type_spec.rb'
@@ -1870,7 +1869,6 @@
- './ee/spec/models/ee/ci/secure_file_spec.rb'
- './ee/spec/models/ee/clusters/agent_spec.rb'
- './ee/spec/models/ee/description_version_spec.rb'
-- './ee/spec/models/ee/event_collection_spec.rb'
- './ee/spec/models/ee/event_spec.rb'
- './ee/spec/models/ee/gpg_key_spec.rb'
- './ee/spec/models/ee/group_group_link_spec.rb'
@@ -3223,7 +3221,6 @@
- './ee/spec/services/security/security_orchestration_policies/sync_opened_merge_requests_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/sync_open_merge_requests_head_pipeline_service_spec.rb'
- './ee/spec/services/security/security_orchestration_policies/validate_policy_service_spec.rb'
-- './ee/spec/services/security/store_findings_metadata_service_spec.rb'
- './ee/spec/services/security/store_grouped_scans_service_spec.rb'
- './ee/spec/services/security/store_scan_service_spec.rb'
- './ee/spec/services/security/store_scans_service_spec.rb'
@@ -3379,7 +3376,6 @@
- './ee/spec/views/registrations/welcome/continuous_onboarding_getting_started.html.haml_spec.rb'
- './ee/spec/views/registrations/welcome/show.html.haml_spec.rb'
- './ee/spec/views/search/_category.html.haml_spec.rb'
-- './ee/spec/views/shared/access_tokens/_table.html.haml_spec.rb'
- './ee/spec/views/shared/billings/_billing_plan_actions.html.haml_spec.rb'
- './ee/spec/views/shared/billings/_billing_plan.html.haml_spec.rb'
- './ee/spec/views/shared/billings/_billing_plans.html.haml_spec.rb'
@@ -4091,7 +4087,6 @@
- './spec/features/incidents/user_creates_new_incident_spec.rb'
- './spec/features/incidents/user_filters_incidents_by_status_spec.rb'
- './spec/features/incidents/user_searches_incidents_spec.rb'
-- './spec/features/incidents/user_views_incident_spec.rb'
- './spec/features/invites_spec.rb'
- './spec/features/issuables/issuable_list_spec.rb'
- './spec/features/issuables/markdown_references/internal_references_spec.rb'
@@ -5873,14 +5868,6 @@
- './spec/lib/error_tracking/collector/payload_validator_spec.rb'
- './spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb'
- './spec/lib/error_tracking/collector/sentry_request_parser_spec.rb'
-- './spec/lib/error_tracking/sentry_client/api_urls_spec.rb'
-- './spec/lib/error_tracking/sentry_client/event_spec.rb'
-- './spec/lib/error_tracking/sentry_client/issue_link_spec.rb'
-- './spec/lib/error_tracking/sentry_client/issue_spec.rb'
-- './spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb'
-- './spec/lib/error_tracking/sentry_client/projects_spec.rb'
-- './spec/lib/error_tracking/sentry_client/repo_spec.rb'
-- './spec/lib/error_tracking/sentry_client_spec.rb'
- './spec/lib/error_tracking/stacktrace_builder_spec.rb'
- './spec/lib/event_filter_spec.rb'
- './spec/lib/expand_variables_spec.rb'
@@ -8400,7 +8387,6 @@
- './spec/models/error_tracking/error_event_spec.rb'
- './spec/models/error_tracking/error_spec.rb'
- './spec/models/error_tracking/project_error_tracking_setting_spec.rb'
-- './spec/models/event_collection_spec.rb'
- './spec/models/event_spec.rb'
- './spec/models/experiment_spec.rb'
- './spec/models/experiment_subject_spec.rb'
@@ -9464,16 +9450,6 @@
- './spec/routing/projects/security/configuration_controller_routing_spec.rb'
- './spec/routing/routing_spec.rb'
- './spec/routing/uploads_routing_spec.rb'
-- './spec/scripts/changed-feature-flags_spec.rb'
-- './spec/scripts/determine-qa-tests_spec.rb'
-- './spec/scripts/failed_tests_spec.rb'
-- './spec/scripts/lib/glfm/parse_examples_spec.rb'
-- './spec/scripts/lib/glfm/shared_spec.rb'
-- './spec/scripts/lib/glfm/update_example_snapshots_spec.rb'
-- './spec/scripts/lib/glfm/update_specification_spec.rb'
-- './spec/scripts/pipeline_test_report_builder_spec.rb'
-- './spec/scripts/setup/find_jh_branch_spec.rb'
-- './spec/scripts/trigger-build_spec.rb'
- './spec/serializers/accessibility_error_entity_spec.rb'
- './spec/serializers/accessibility_reports_comparer_entity_spec.rb'
- './spec/serializers/accessibility_reports_comparer_serializer_spec.rb'
@@ -10082,7 +10058,6 @@
- './spec/services/members/request_access_service_spec.rb'
- './spec/services/members/standard_member_builder_spec.rb'
- './spec/services/members/unassign_issuables_service_spec.rb'
-- './spec/services/members/update_service_spec.rb'
- './spec/services/merge_requests/add_context_service_spec.rb'
- './spec/services/merge_requests/add_spent_time_service_spec.rb'
- './spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
@@ -10768,7 +10743,6 @@
- './spec/views/registrations/welcome/show.html.haml_spec.rb'
- './spec/views/search/_results.html.haml_spec.rb'
- './spec/views/search/show.html.haml_spec.rb'
-- './spec/views/shared/access_tokens/_table.html.haml_spec.rb'
- './spec/views/shared/deploy_tokens/_form.html.haml_spec.rb'
- './spec/views/shared/groups/_dropdown.html.haml_spec.rb'
- './spec/views/shared/issuable/_sidebar.html.haml_spec.rb'
@@ -10922,7 +10896,6 @@
- './spec/workers/environments/canary_ingress/update_worker_spec.rb'
- './spec/workers/error_tracking_issue_link_worker_spec.rb'
- './spec/workers/every_sidekiq_worker_spec.rb'
-- './spec/workers/experiments/record_conversion_event_worker_spec.rb'
- './spec/workers/expire_build_artifacts_worker_spec.rb'
- './spec/workers/export_csv_worker_spec.rb'
- './spec/workers/external_service_reactive_caching_worker_spec.rb'
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
index 94061b140f4..b85c3904127 100644
--- a/spec/support/services/issuable_update_service_shared_examples.rb
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -6,18 +6,48 @@ RSpec.shared_examples 'issuable update service' do
end
context 'changing state' do
- before do
- expect(project).to receive(:execute_hooks).once
- end
+ let(:hook_event) { :"#{closed_issuable.class.name.underscore.to_sym}_hooks" }
context 'to reopened' do
- it 'executes hooks only once' do
+ let(:expected_payload) do
+ include(
+ changes: include(
+ state_id: { current: 1, previous: 2 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ ),
+ object_attributes: include(
+ state: 'opened',
+ action: 'reopen'
+ )
+ )
+ end
+
+ it 'executes hooks' do
+ expect(project).to receive(:execute_hooks).with(expected_payload, hook_event)
+ expect(project).to receive(:execute_integrations).with(expected_payload, hook_event)
+
described_class.new(project: project, current_user: user, params: { state_event: 'reopen' }).execute(closed_issuable)
end
end
context 'to closed' do
- it 'executes hooks only once' do
+ let(:expected_payload) do
+ include(
+ changes: include(
+ state_id: { current: 2, previous: 1 },
+ updated_at: { current: kind_of(Time), previous: kind_of(Time) }
+ ),
+ object_attributes: include(
+ state: 'closed',
+ action: 'close'
+ )
+ )
+ end
+
+ it 'executes hooks' do
+ expect(project).to receive(:execute_hooks).with(expected_payload, hook_event)
+ expect(project).to receive(:execute_integrations).with(expected_payload, hook_event)
+
described_class.new(project: project, current_user: user, params: { state_event: 'close' }).execute(open_issuable)
end
end
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index 1e291a90163..ae98ce689e3 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -57,7 +57,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
context "race conditions" do
context "when #{record_class_name} migration fails and is rolled back" do
before do
- expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
+ allow_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::StatementTimeout)
end
@@ -68,6 +68,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos
end
it "doesn't unblock a previously-blocked user" do
+ expect(user.starred_projects).to receive(:update_all).and_call_original
user.block
expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout)
diff --git a/spec/support/shared_contexts/container_repositories_shared_context.rb b/spec/support/shared_contexts/container_repositories_shared_context.rb
index a74b09d38bd..c3040a77517 100644
--- a/spec/support/shared_contexts/container_repositories_shared_context.rb
+++ b/spec/support/shared_contexts/container_repositories_shared_context.rb
@@ -14,7 +14,6 @@ RSpec.shared_context 'importable repositories' do
before do
stub_application_setting(container_registry_import_created_before: 1.day.ago)
stub_feature_flags(
- container_registry_phase_2_deny_list: false,
container_registry_migration_limit_gitlab_org: false,
container_registry_migration_phase2_all_plans: false
)
diff --git a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
index aa857cfdb70..1480b5f98e7 100644
--- a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb
@@ -17,6 +17,6 @@ RSpec.shared_context 'runners resolver setup' do
end
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) }
- let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: 'mnopqr', description: 'subgroup runner', contacted_at: 1.second.ago) }
+ let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: '123456', description: 'subgroup runner', contacted_at: 1.second.ago) }
let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) }
end
diff --git a/spec/support/shared_contexts/jobs/handling_retried_jobs_shared_context.rb b/spec/support/shared_contexts/jobs/handling_retried_jobs_shared_context.rb
new file mode 100644
index 00000000000..428eff77373
--- /dev/null
+++ b/spec/support/shared_contexts/jobs/handling_retried_jobs_shared_context.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when handling retried jobs' do |url|
+ let(:set_name) { 'retry' }
+ # Account for Sidekiq retry jitter
+ # https://github.com/mperham/sidekiq/blob/3575ccb44c688dd08bfbfd937696260b12c622fb/lib/sidekiq/job_retry.rb#L217
+ let(:schedule_jitter) { 10 }
+
+ # Try to mimic as closely as possible what Sidekiq will actually
+ # do to retry a job.
+ def retry_in(klass, time, args = 0)
+ message = Gitlab::Json.generate(
+ 'class' => klass.name,
+ 'args' => [args],
+ 'retry' => true
+ )
+
+ allow(klass).to receive(:sidekiq_retry_in_block).and_return(proc { time.to_i })
+
+ begin
+ Sidekiq::JobRetry.new(Sidekiq).local(klass, message, klass.queue) { raise 'boom' }
+ rescue Sidekiq::JobRetry::Skip
+ # Sidekiq scheduled the retry
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
new file mode 100644
index 00000000000..263cf9f5e19
--- /dev/null
+++ b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'oj'
+
+def parameterized_test_matrix(invalid: false)
+ test_cases_path = File.join(
+ File.expand_path(__dir__), '..', '..', '..', '..', 'fixtures', 'lib', 'sbom', 'package-url-test-cases.json')
+ test_cases = Gitlab::Json.parse(File.read(test_cases_path))
+
+ test_cases.filter { _1.delete('is_invalid') == invalid }.each_with_object({}) do |test_case, memo|
+ description = test_case.delete('description')
+ memo[description] = test_case.symbolize_keys
+ end
+end
+
+RSpec.shared_context 'with valid purl examples' do
+ where do
+ parameterized_test_matrix(invalid: false)
+ end
+end
+
+RSpec.shared_context 'with invalid purl examples' do
+ where do
+ parameterized_test_matrix(invalid: true)
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 064e40287be..af35a5ff068 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -67,8 +67,8 @@ RSpec.shared_context 'project navbar structure' do
{
nav_item: _('Deployments'),
nav_sub_items: [
- _('Feature Flags'),
_('Environments'),
+ _('Feature Flags'),
_('Releases')
]
},
@@ -85,8 +85,7 @@ RSpec.shared_context 'project navbar structure' do
_('Metrics'),
_('Error Tracking'),
_('Alerts'),
- _('Incidents'),
- _('Product Analytics')
+ _('Incidents')
]
},
{
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index bb1b794c2b6..a6226fe903b 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -76,6 +76,7 @@ RSpec.shared_context 'GroupPolicy context' do
register_group_runners
read_billing
edit_billing
+ admin_member_access_request
]
end
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 fc7255a4a20..6e2caa853f8 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -68,7 +68,7 @@ RSpec.shared_context 'ProjectPolicy context' do
admin_project admin_project_member admin_snippet admin_terraform_state
admin_wiki create_deploy_token destroy_deploy_token
push_to_delete_protected_branch read_deploy_token update_snippet
- destroy_upload
+ destroy_upload admin_member_access_request rename_project
]
end
@@ -83,7 +83,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_owner_permissions) do
%i[
archive_project change_namespace change_visibility_level destroy_issue
- destroy_merge_request manage_owners remove_fork_project remove_project rename_project
+ destroy_merge_request manage_owners remove_fork_project remove_project
set_issue_created_at set_issue_iid set_issue_updated_at
set_note_created_at
]
diff --git a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
index b18ce14eba6..d9ea7bc7f82 100644
--- a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
@@ -126,6 +126,63 @@ RSpec.shared_context 'ProjectPolicyTable context' do
end
# This table is based on permission_table_for_guest_feature_access,
+ # but takes into account note confidentiality. It is required on the context
+ # to have one regular note and one confidential note.
+ #
+ # project_level, :feature_access_level, :membership, :admin_mode, :expected_count
+ def permission_table_for_notes_feature_access
+ :public | :enabled | :admin | true | 2
+ :public | :enabled | :admin | false | 1
+ :public | :enabled | :reporter | nil | 2
+ :public | :enabled | :guest | nil | 1
+ :public | :enabled | :non_member | nil | 1
+ :public | :enabled | :anonymous | nil | 1
+
+ :public | :private | :admin | true | 2
+ :public | :private | :admin | false | 0
+ :public | :private | :reporter | nil | 2
+ :public | :private | :guest | nil | 1
+ :public | :private | :non_member | nil | 0
+ :public | :private | :anonymous | nil | 0
+
+ :public | :disabled | :reporter | nil | 0
+ :public | :disabled | :guest | nil | 0
+ :public | :disabled | :non_member | nil | 0
+ :public | :disabled | :anonymous | nil | 0
+
+ :internal | :enabled | :admin | true | 2
+ :internal | :enabled | :admin | false | 1
+ :internal | :enabled | :reporter | nil | 2
+ :internal | :enabled | :guest | nil | 1
+ :internal | :enabled | :non_member | nil | 1
+ :internal | :enabled | :anonymous | nil | 0
+
+ :internal | :private | :admin | true | 2
+ :internal | :private | :admin | false | 0
+ :internal | :private | :reporter | nil | 2
+ :internal | :private | :guest | nil | 1
+ :internal | :private | :non_member | nil | 0
+ :internal | :private | :anonymous | nil | 0
+
+ :internal | :disabled | :reporter | nil | 0
+ :internal | :disabled | :guest | nil | 0
+ :internal | :disabled | :non_member | nil | 0
+ :internal | :disabled | :anonymous | nil | 0
+
+ :private | :private | :admin | true | 2
+ :private | :private | :admin | false | 0
+ :private | :private | :reporter | nil | 2
+ :private | :private | :guest | nil | 1
+ :private | :private | :non_member | nil | 0
+ :private | :private | :anonymous | nil | 0
+
+ :private | :disabled | :reporter | nil | 0
+ :private | :disabled | :guest | nil | 0
+ :private | :disabled | :non_member | nil | 0
+ :private | :disabled | :anonymous | nil | 0
+ end
+
+ # This table is based on permission_table_for_guest_feature_access,
# but with a slight twist.
# Some features can be hidden away to GUEST, when project is private.
# (see ProjectFeature::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT)
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index fa048b76e18..7df4b7635d3 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -3,6 +3,7 @@
RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
+ stub_feature_flags(apollo_boards: false)
parent.add_maintainer(user)
login_as(user)
@@ -121,6 +122,7 @@ RSpec.shared_examples 'multiple issue boards' do
context 'unauthorized user' do
before do
+ stub_feature_flags(apollo_boards: false)
visit boards_path
wait_for_requests
end
diff --git a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb
index cd4432af4ed..a9edf18d562 100644
--- a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb
+++ b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb
@@ -35,7 +35,7 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do
it 'does not import wiki' do
expect(subject).to receive(:source_wiki_exists?).and_return(false)
- expect(parent.wiki).not_to receive(:ensure_repository)
+ expect(parent.wiki).not_to receive(:create_wiki_repository)
expect(parent.wiki.repository).not_to receive(:ensure_repository)
expect { subject.run }.not_to raise_error
@@ -75,7 +75,7 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do
describe 'unsuccessful response' do
shared_examples 'does not raise an error' do
it 'does not raise an error' do
- expect(parent.wiki).not_to receive(:ensure_repository)
+ expect(parent.wiki).not_to receive(:create_wiki_repository)
expect(parent.wiki.repository).not_to receive(:ensure_repository)
expect { subject.run }.not_to raise_error
diff --git a/spec/support/shared_examples/ci/retryable_shared_examples.rb b/spec/support/shared_examples/ci/retryable_shared_examples.rb
new file mode 100644
index 00000000000..4622dbe4e31
--- /dev/null
+++ b/spec/support/shared_examples/ci/retryable_shared_examples.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a retryable job' do
+ describe '#enqueue_immediately?' do
+ it 'defaults to false' do
+ expect(subject.enqueue_immediately?).to eq(false)
+ end
+ end
+
+ describe '#set_enqueue_immediately!' do
+ it 'changes #enqueue_immediately? to true' do
+ expect { subject.set_enqueue_immediately! }
+ .to change(subject, :enqueue_immediately?).from(false).to(true)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/preferred_language_switcher_shared_examples.rb b/spec/support/shared_examples/controllers/preferred_language_switcher_shared_examples.rb
new file mode 100644
index 00000000000..74456e62eb8
--- /dev/null
+++ b/spec/support/shared_examples/controllers/preferred_language_switcher_shared_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'switches to user preferred language' do |msg_id_example|
+ context 'with preferred_language in cookies' do
+ render_views
+ let(:user_preferred_language) { 'zh_CN' }
+
+ subject { get :new }
+
+ before do
+ cookies['preferred_language'] = user_preferred_language
+ end
+
+ it 'renders new template with cookies preferred language' do
+ expect(subject).to render_template(:new)
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expected_text = Gitlab::I18n.with_locale(user_preferred_language) { _(msg_id_example) }
+ expect(response.body).to include(expected_text)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
index cd255abd7a8..32a7b32ac72 100644
--- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -9,13 +9,7 @@ RSpec.shared_examples 'resource access tokens missing access rights' do
end
RSpec.shared_examples 'resource access tokens creation' do |resource_type|
- def active_resource_access_tokens
- find("[data-testid='active-tokens']")
- end
-
- def created_resource_access_token
- find_field('new-access-token').value
- end
+ include Spec::Support::Helpers::AccessTokenHelpers
it 'allows creation of an access token', :aggregate_failures do
name = 'My access token'
@@ -34,12 +28,12 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type|
click_on "Create #{resource_type} access token"
- expect(active_resource_access_tokens).to have_text(name)
- expect(active_resource_access_tokens).to have_text('in')
- expect(active_resource_access_tokens).to have_text('read_api')
- expect(active_resource_access_tokens).to have_text('read_repository')
- expect(active_resource_access_tokens).to have_text('Guest')
- expect(created_resource_access_token).not_to be_empty
+ expect(active_access_tokens).to have_text(name)
+ expect(active_access_tokens).to have_text('in')
+ expect(active_access_tokens).to have_text('read_api')
+ expect(active_access_tokens).to have_text('read_repository')
+ expect(active_access_tokens).to have_text('Guest')
+ expect(created_access_token).to match(/[\w-]{20}/)
end
end
@@ -105,14 +99,14 @@ RSpec.shared_examples 'resource access tokens creation disallowed' do |error_mes
end
RSpec.shared_examples 'active resource access tokens' do
- def active_resource_access_tokens
+ def active_access_tokens
find("[data-testid='active-tokens']")
end
it 'shows active access tokens' do
visit resource_settings_access_tokens_path
- expect(active_resource_access_tokens).to have_text(resource_access_token.name)
+ expect(active_access_tokens).to have_text(resource_access_token.name)
end
context 'when User#time_display_relative is false' do
@@ -123,13 +117,13 @@ RSpec.shared_examples 'active resource access tokens' do
it 'shows absolute times for expires_at' do
visit resource_settings_access_tokens_path
- expect(active_resource_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
+ expect(active_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d'))
end
end
end
RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_text|
- def active_resource_access_tokens
+ def active_access_tokens
find("[data-testid='active-tokens']")
end
@@ -137,14 +131,14 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
visit resource_settings_access_tokens_path
accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
- expect(active_resource_access_tokens).to have_text(no_active_tokens_text)
+ expect(active_access_tokens).to have_text(no_active_tokens_text)
end
it 'removes expired tokens from active section' do
resource_access_token.update!(expires_at: 5.days.ago)
visit resource_settings_access_tokens_path
- expect(active_resource_access_tokens).to have_text(no_active_tokens_text)
+ expect(active_access_tokens).to have_text(no_active_tokens_text)
end
context 'when resource access token creation is not allowed' do
@@ -156,7 +150,7 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
visit resource_settings_access_tokens_path
accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
- expect(active_resource_access_tokens).to have_text(no_active_tokens_text)
+ expect(active_access_tokens).to have_text(no_active_tokens_text)
end
end
end
diff --git a/spec/support/shared_examples/features/confidential_notes_shared_examples.rb b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb
new file mode 100644
index 00000000000..289da025af6
--- /dev/null
+++ b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.shared_examples 'confidential notes on issuables' do
+ include Spec::Support::Helpers::Features::NotesHelpers
+
+ context 'when user does not have permissions' do
+ it 'does not show confidential note checkbox' do
+ issuable_parent.add_guest(user)
+ sign_in(user)
+ visit(issuable_path)
+
+ page.within('.new-note') do
+ expect(page).not_to have_selector('[data-testid="internal-note-checkbox"]')
+ end
+ end
+ end
+
+ context 'when user has permissions' do
+ it 'creates confidential note' do
+ issuable_parent.add_reporter(user)
+ sign_in(user)
+ visit(issuable_path)
+
+ find('[data-testid="internal-note-checkbox"]').click
+ add_note('Confidential note')
+
+ page.within('.note-header') do
+ expect(page).to have_selector('[data-testid="internal-note-indicator"]')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 7863548e7f3..f01e3c88dad 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -313,5 +313,21 @@ RSpec.shared_examples 'edits content using the content editor' do
expect(page).not_to have_css(suggestions_dropdown)
end
+
+ it 'scrolls selected item into view when navigating with keyboard' do
+ type_in_content_editor ':'
+
+ expect(find(suggestions_dropdown)).to have_text('hundred points symbol')
+
+ expect(dropdown_scroll_top).to be 0
+
+ send_keys :arrow_up
+
+ expect(dropdown_scroll_top).to be > 100
+ end
+
+ def dropdown_scroll_top
+ evaluate_script("document.querySelector('#{suggestions_dropdown} .gl-new-dropdown-inner').scrollTop")
+ end
end
end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 456175e7113..2cfe353d5d7 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -21,15 +21,10 @@ RSpec.shared_examples 'a creatable merge request' do
expect(page).to have_content user.name
end
- click_button 'Milestone'
- page.within '.issue-milestone' do
- click_link milestone.title
- end
-
+ click_button 'Select milestone'
+ click_button milestone.title
expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
- page.within '.js-milestone-select' do
- expect(page).to have_content milestone.title
- end
+ expect(page).to have_button milestone.title
click_button 'Labels'
page.within '.dropdown-menu-labels' do
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index 2fff4137934..ea6d1655694 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -20,14 +20,10 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_content user.name
end
- click_button 'Milestone'
- page.within '.issue-milestone' do
- click_link milestone.title
- end
+ click_button 'Select milestone'
+ click_button milestone.title
expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
- page.within '.js-milestone-select' do
- expect(page).to have_content milestone.title
- end
+ expect(page).to have_button milestone.title
click_button 'Labels'
page.within '.dropdown-menu-labels' do
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 7aad5e2de80..f09cf0613a1 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
def package_table_row(index)
- page.all("#{packages_table_selector} > [data-testid=\"package-row\"]")[index].text
+ page.all("#{packages_table_selector} [data-testid=\"package-row\"]")[index].text
end
end
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index 1d4af944187..a7bc19da45f 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'shows and resets runner registration token' do
- include Spec::Support::Helpers::ModalHelpers
include Spec::Support::Helpers::Features::RunnersHelpers
+ include Spec::Support::Helpers::ModalHelpers
before do
click_on dropdown_text
@@ -146,6 +146,23 @@ RSpec.shared_examples 'pauses, resumes and deletes a runner' do
end
end
+RSpec.shared_examples 'deletes runners in bulk' do
+ describe 'when selecting all for deletion', :js do
+ before do
+ check s_('Runners|Select all')
+ click_button s_('Runners|Delete selected')
+
+ within_modal do
+ click_on "Permanently delete #{runner_count} runners"
+ end
+
+ wait_for_requests
+ end
+
+ it_behaves_like 'shows no runners registered'
+ end
+end
+
RSpec.shared_examples 'filters by tag' do
it 'shows correct runner when tag matches' do
expect(page).to have_content found_runner
diff --git a/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb
new file mode 100644
index 00000000000..4d242d0e719
--- /dev/null
+++ b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb
@@ -0,0 +1,304 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a redacted search results' do
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:accessible_group) { create(:group, :private) }
+ let_it_be(:accessible_project) { create(:project, :repository, :private, name: 'accessible_project') }
+
+ let_it_be(:group_member) { create(:group_member, group: accessible_group, user: user) }
+
+ let_it_be(:inaccessible_group) { create(:group, :private) }
+ let_it_be(:inaccessible_project) { create(:project, :repository, :private, name: 'inaccessible_project') }
+
+ let(:search) { 'anything' }
+
+ subject(:result) { search_service.search_objects }
+
+ def found_blob(project)
+ Gitlab::Search::FoundBlob.new(project: project)
+ end
+
+ def found_wiki_page(project)
+ Gitlab::Search::FoundWikiPage.new(found_blob(project))
+ end
+
+ def ar_relation(klass, *objects)
+ klass.id_in(objects.map(&:id))
+ end
+
+ def kaminari_array(*objects)
+ Kaminari.paginate_array(objects).page(1).per(20)
+ end
+
+ before do
+ accessible_project.add_maintainer(user)
+
+ allow(search_service)
+ .to receive_message_chain(:search_results, :objects)
+ .and_return(unredacted_results)
+ end
+
+ context 'for issues' do
+ let(:readable) { create(:issue, project: accessible_project) }
+ let(:unreadable) { create(:issue, project: inaccessible_project) }
+ let(:unredacted_results) { ar_relation(Issue, readable, unreadable) }
+ let(:scope) { 'issues' }
+
+ it 'redacts the inaccessible issue' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Issue', id: unreadable.id, ability: :read_issue }
+ ])))
+
+ expect(result).to contain_exactly(readable)
+ end
+ end
+
+ context 'for notes' do
+ let(:readable_merge_request) do
+ create(:merge_request_with_diffs, target_project: accessible_project, source_project: accessible_project)
+ end
+
+ let(:readable_note_on_commit) { create(:note_on_commit, project: accessible_project) }
+ let(:readable_diff_note) { create(:diff_note_on_commit, project: accessible_project) }
+ let(:readable_note_on_mr) do
+ create(:discussion_note_on_merge_request, noteable: readable_merge_request, project: accessible_project)
+ end
+
+ let(:readable_diff_note_on_mr) do
+ create(:diff_note_on_merge_request, noteable: readable_merge_request, project: accessible_project)
+ end
+
+ let(:readable_note_on_project_snippet) do
+ create(:note_on_project_snippet, noteable: readable_merge_request, project: accessible_project)
+ end
+
+ let(:unreadable_merge_request) do
+ create(:merge_request_with_diffs, target_project: inaccessible_project, source_project: inaccessible_project)
+ end
+
+ let(:unreadable_note_on_commit) { create(:note_on_commit, project: inaccessible_project) }
+ let(:unreadable_diff_note) { create(:diff_note_on_commit, project: inaccessible_project) }
+ let(:unreadable_note_on_mr) do
+ create(:discussion_note_on_merge_request, noteable: unreadable_merge_request, project: inaccessible_project)
+ end
+
+ let(:unreadable_note_on_project_snippet) do
+ create(:note_on_project_snippet, noteable: unreadable_merge_request, project: inaccessible_project)
+ end
+
+ let(:unredacted_results) do
+ ar_relation(Note,
+ readable_note_on_commit,
+ readable_diff_note,
+ readable_note_on_mr,
+ readable_diff_note_on_mr,
+ readable_note_on_project_snippet,
+ unreadable_note_on_commit,
+ unreadable_diff_note,
+ unreadable_note_on_mr,
+ unreadable_note_on_project_snippet)
+ end
+
+ let(:scope) { 'notes' }
+
+ it 'redacts the inaccessible notes' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Note', id: unreadable_note_on_commit.id, ability: :read_note },
+ { class_name: 'DiffNote', id: unreadable_diff_note.id, ability: :read_note },
+ { class_name: 'DiscussionNote', id: unreadable_note_on_mr.id, ability: :read_note },
+ { class_name: 'Note', id: unreadable_note_on_project_snippet.id, ability: :read_note }
+ ])))
+
+ expect(result).to contain_exactly(readable_note_on_commit,
+ readable_diff_note,
+ readable_note_on_mr,
+ readable_diff_note_on_mr,
+ readable_note_on_project_snippet)
+ end
+ end
+
+ context 'for merge_requests' do
+ let(:readable) { create(:merge_request, source_project: accessible_project) }
+ let(:unreadable) { create(:merge_request, source_project: inaccessible_project) }
+ let(:unredacted_results) { ar_relation(MergeRequest, readable, unreadable) }
+ let(:scope) { 'merge_requests' }
+
+ it 'redacts the inaccessible merge request' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'MergeRequest', id: unreadable.id, ability: :read_merge_request }
+ ])))
+
+ expect(result).to contain_exactly(readable)
+ end
+
+ context 'with :with_api_entity_associations' do
+ let(:unredacted_results) { ar_relation(MergeRequest.with_api_entity_associations, readable, unreadable) }
+
+ it_behaves_like "redaction limits N+1 queries", limit: 8
+ end
+ end
+
+ context 'for blobs' do
+ let(:readable) { found_blob(accessible_project) }
+ let(:unreadable) { found_blob(inaccessible_project) }
+ let(:unredacted_results) { kaminari_array(readable, unreadable) }
+ let(:scope) { 'blobs' }
+
+ it 'redacts the inaccessible blob' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Gitlab::Search::FoundBlob', id: unreadable.id, ability: :read_blob }
+ ])))
+
+ expect(result).to contain_exactly(readable)
+ end
+ end
+
+ context 'for wiki blobs' do
+ let(:readable) { found_wiki_page(accessible_project) }
+ let(:unreadable) { found_wiki_page(inaccessible_project) }
+ let(:unredacted_results) { kaminari_array(readable, unreadable) }
+ let(:scope) { 'wiki_blobs' }
+
+ it 'redacts the inaccessible blob' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Gitlab::Search::FoundWikiPage', id: unreadable.id, ability: :read_wiki_page }
+ ])))
+
+ expect(result).to contain_exactly(readable)
+ end
+ end
+
+ context 'for project snippets' do
+ let(:readable) { create(:project_snippet, project: accessible_project) }
+ let(:unreadable) { create(:project_snippet, project: inaccessible_project) }
+ let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) }
+ let(:scope) { 'snippet_titles' }
+
+ it 'redacts the inaccessible snippet' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'ProjectSnippet', id: unreadable.id, ability: :read_snippet }
+ ])))
+
+ expect(result).to contain_exactly(readable)
+ end
+
+ context 'with :with_api_entity_associations' do
+ it_behaves_like "redaction limits N+1 queries", limit: 14
+ end
+ end
+
+ context 'for personal snippets' do
+ let(:readable) { create(:personal_snippet, :private, author: user) }
+ let(:unreadable) { create(:personal_snippet, :private) }
+ let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) }
+ let(:scope) { 'snippet_titles' }
+
+ it 'redacts the inaccessible snippet' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'PersonalSnippet', id: unreadable.id, ability: :read_snippet }
+ ])))
+
+ expect(result).to contain_exactly(readable)
+ end
+
+ context 'with :with_api_entity_associations' do
+ it_behaves_like "redaction limits N+1 queries", limit: 4
+ end
+ end
+
+ context 'for commits' do
+ let(:readable) { accessible_project.commit }
+ let(:unreadable) { inaccessible_project.commit }
+ let(:unredacted_results) { kaminari_array(readable, unreadable) }
+ let(:scope) { 'commits' }
+
+ it 'redacts the inaccessible commit' do
+ expect(search_service.send(:logger))
+ .to receive(:error)
+ .with(hash_including(
+ message: "redacted_search_results",
+ current_user_id: user.id,
+ query: search,
+ filtered: array_including(
+ [
+ { class_name: 'Commit', id: unreadable.id, ability: :read_commit }
+ ])))
+
+ expect(result).to contain_exactly(readable)
+ end
+ end
+
+ context 'for users' do
+ let(:other_user) { create(:user) }
+ let(:unredacted_results) { ar_relation(User, user, other_user) }
+ let(:scope) { 'users' }
+
+ it 'passes the users through' do
+ # Users are always visible to everyone
+ expect(result).to contain_exactly(user, other_user)
+ end
+ end
+end
+
+RSpec.shared_examples "redaction limits N+1 queries" do |limit:|
+ it 'does not exceed the query limit' do
+ # issuing the query to remove the data loading call
+ unredacted_results.to_a
+
+ # only the calls from the redaction are left
+ query = ActiveRecord::QueryRecorder.new { result }
+
+ # these are the project authorization calls, which are not preloaded
+ expect(query.count).to be <= limit
+ end
+end
diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
index 84dc2b20ddc..cc74c977064 100644
--- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
+++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
RSpec.shared_examples 'search timeouts' do |scope|
+ let(:additional_params) { {} }
+
context 'when search times out' do
before do
- stub_feature_flags(search_page_vertical_nav: false)
allow_next_instance_of(SearchService) do |service|
allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled)
end
- visit(search_path(search: 'test', scope: scope))
+ visit(search_path(search: 'test', scope: scope, **additional_params))
end
it 'renders timeout information' do
- # expect(page).to have_content('This endpoint has been requested too many times.')
expect(page).to have_content('Your search timed out')
end
it 'sets tab count to 0' do
- expect(page.find('.search-filter .active')).to have_text('0')
+ expect(page.find('[data-testid="search-filter"] .active')).to have_text('0')
end
end
end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index d1e5046a39e..f0b72cfaee3 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -31,8 +31,8 @@ RSpec.shared_examples 'variable list' do |is_admin|
wait_for_requests
page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Protected"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key')
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Protected'))
end
end
@@ -46,26 +46,26 @@ RSpec.shared_examples 'variable list' do |is_admin|
wait_for_requests
page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key')
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).not_to have_content(s_('CiVariables|Masked'))
end
end
it 'reveals and hides variables' do
page.within('[data-testid="ci-variable-table"]') do
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
- expect(page).to have_content('*' * 17)
+ expect(page).to have_content('*' * 5)
click_button('Reveal value')
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
expect(first('.js-ci-variable-row td[data-label="Value"]').text).to eq(variable.value)
- expect(page).not_to have_content('*' * 17)
+ expect(page).not_to have_content('*' * 5)
click_button('Hide value')
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
- expect(page).to have_content('*' * 17)
+ expect(page).to have_content('*' * 5)
end
end
@@ -116,7 +116,8 @@ RSpec.shared_examples 'variable list' do |is_admin|
wait_for_requests
page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Protected'))
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).not_to have_content(s_('CiVariables|Masked'))
end
end
@@ -144,7 +145,7 @@ RSpec.shared_examples 'variable list' do |is_admin|
end
page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Masked'))
end
end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index f62c9c00006..8b3a344a841 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -585,7 +585,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
context 'when full-text search is disabled' do
- let(:search_term) { 'somet' }
+ let(:search_term) { 'ometh' }
before do
stub_feature_flags(issues_full_text_search: false)
diff --git a/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb
new file mode 100644
index 00000000000..fbfd1af2601
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'timeline event mutation responds with validation error' do |error_message:|
+ it 'responds with a validation error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to match_array([error_message])
+ end
+end
diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
index 0aa3bf8944f..bdd4dbfe209 100644
--- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
@@ -78,7 +78,7 @@ RSpec.shared_examples 'a Note mutation when there are rate limit validation erro
context 'when the user is in the allowlist' do
before do
- stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"])
+ stub_application_setting(notes_create_limit_allowlist: [current_user.username.to_s])
end
it_behaves_like 'a Note mutation that creates a Note'
diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
index 3017f62a7c9..aadc061f175 100644
--- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'group and projects packages resolver' do
end
%w[CREATED_DESC NAME_DESC VERSION_DESC TYPE_ASC].each do |order|
- context "#{order}" do
+ context order.to_s do
let(:args) { { sort: order } }
it { is_expected.to eq([maven_package, conan_package]) }
@@ -33,7 +33,7 @@ RSpec.shared_examples 'group and projects packages resolver' do
end
%w[CREATED_ASC NAME_ASC VERSION_ASC TYPE_DESC].each do |order|
- context "#{order}" do
+ context order.to_s do
let(:args) { { sort: order } }
it { is_expected.to eq([conan_package, maven_package]) }
diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
index 2d7da9f9f00..67576a18c80 100644
--- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -86,4 +86,25 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
eq("`alpha` and `deprecated` arguments cannot be passed at the same time")
)
end
+
+ describe 'visible?' do
+ let(:ctx) { {} }
+
+ it 'defaults to true' do
+ expect(subject).to be_visible(ctx)
+ end
+
+ context 'when subject is deprecated' do
+ let(:arguments) { { deprecated: { milestone: '1.10', reason: :renamed } } }
+
+ it 'defaults to true' do
+ expect(subject(arguments)).to be_visible(ctx)
+ end
+
+ it 'returns false if `remove_deprecated` is true in context' do
+ ctx = { remove_deprecated: true }
+ expect(subject(arguments)).not_to be_visible(ctx)
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
index 2e00abe2f8e..6cdd7954b5f 100644
--- a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
+++ b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb
@@ -129,6 +129,7 @@ RSpec.shared_examples_for 'collection cache helper' do
before do
allow(::Gitlab::Metrics::WebTransaction).to receive(:current).and_return(transaction)
allow(transaction).to receive(:increment)
+ allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(any_args).and_call_original
allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id)
end
diff --git a/spec/support/shared_examples/lib/email/email_shared_examples.rb b/spec/support/shared_examples/lib/email/email_shared_examples.rb
new file mode 100644
index 00000000000..26655a71fec
--- /dev/null
+++ b/spec/support/shared_examples/lib/email/email_shared_examples.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+# Set the particular setting as a key-value pair
+# Setting method is different depending on klass and must be defined in the calling spec
+def stub_email_setting(key_value_pairs)
+ case setting_name
+ when :incoming_email
+ stub_incoming_email_setting(key_value_pairs)
+ when :service_desk_email
+ stub_service_desk_email_setting(key_value_pairs)
+ end
+end
+
+RSpec.shared_examples_for 'enabled? method for email' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.enabled? }
+
+ where(:value, :address, :result) do
+ false | nil | false
+ false | 'replies+%{key}@example.com' | false
+ true | nil | false
+ true | 'replies+%{key}@example.com' | true
+ end
+
+ with_them do
+ before do
+ stub_email_setting(enabled: value)
+ stub_email_setting(address: address)
+ end
+
+ it { is_expected.to eq result }
+ end
+end
+
+RSpec.shared_examples_for 'supports_wildcard? method for email' do
+ subject { described_class.supports_wildcard? }
+
+ before do
+ stub_incoming_email_setting(address: value)
+ end
+
+ context 'when address contains the wildcard placeholder' do
+ let(:value) { 'replies+%{key}@example.com' }
+
+ it 'confirms that wildcard is supported' do
+ expect(subject).to be_truthy
+ end
+ end
+
+ context "when address doesn't contain the wildcard placeholder" do
+ let(:value) { 'replies@example.com' }
+
+ it 'returns that wildcard is not supported' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when address is nil' do
+ let(:value) { nil }
+
+ it 'returns that wildcard is not supported' do
+ expect(subject).to be_falsey
+ end
+ end
+end
+
+RSpec.shared_examples_for 'unsubscribe_address method for email' do
+ before do
+ stub_incoming_email_setting(address: 'replies+%{key}@example.com')
+ end
+
+ it 'returns the address with interpolated reply key and unsubscribe suffix' do
+ expect(described_class.unsubscribe_address('key'))
+ .to eq("replies+key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}@example.com")
+ end
+end
+
+RSpec.shared_examples_for 'key_from_fallback_message_id method for email' do
+ it 'returns reply key' do
+ expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
+ end
+end
+
+RSpec.shared_examples_for 'supports_issue_creation? method for email' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.supports_issue_creation? }
+
+ where(:enabled_value, :supports_wildcard_value, :result) do
+ false | false | false
+ false | true | false
+ true | false | false
+ true | true | true
+ end
+
+ with_them do
+ before do
+ allow(described_class).to receive(:enabled?).and_return(enabled_value)
+ allow(described_class).to receive(:supports_wildcard?).and_return(supports_wildcard_value)
+ end
+
+ it { is_expected.to eq result }
+ end
+end
+
+RSpec.shared_examples_for 'reply_address method for email' do
+ before do
+ stub_incoming_email_setting(address: "replies+%{key}@example.com")
+ end
+
+ it "returns the address with an interpolated reply key" do
+ expect(described_class.reply_address("key")).to eq("replies+key@example.com")
+ end
+end
+
+RSpec.shared_examples_for 'scan_fallback_references method for email' do
+ let(:references) do
+ '<issue_1@localhost>' \
+ ' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' \
+ ',<exchange@microsoft.com>'
+ end
+
+ it 'returns reply key' do
+ expect(described_class.scan_fallback_references(references))
+ .to eq(%w[issue_1@localhost
+ reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost
+ exchange@microsoft.com])
+ end
+end
+
+RSpec.shared_examples_for 'common email methods' do
+ it_behaves_like 'enabled? method for email'
+ it_behaves_like 'supports_wildcard? method for email'
+ it_behaves_like 'key_from_fallback_message_id method for email'
+ it_behaves_like 'supports_issue_creation? method for email'
+ it_behaves_like 'reply_address method for email'
+ it_behaves_like 'unsubscribe_address method for email'
+ it_behaves_like 'scan_fallback_references method for email'
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
index d14216ec5ff..22b4f9f583c 100644
--- a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
@@ -3,9 +3,6 @@
RSpec.shared_context 'reconfigures connection stack' do |db_config_name|
before do
skip_if_multiple_databases_not_setup
-
- # Due to lib/gitlab/database/load_balancing/configuration.rb:92 requiring RequestStore
- # we cannot use stub_feature_flags(force_no_sharing_primary_model: true)
Gitlab::Database.database_base_models.each do |_, model_class|
allow(model_class.load_balancer.configuration).to receive(:use_dedicated_connection?).and_return(true)
end
diff --git a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
index db2f2f2d0f0..e97b9ad969f 100644
--- a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
@@ -15,6 +15,16 @@ RSpec.shared_examples 'subscribes to event' do
it_behaves_like 'an idempotent worker'
end
+RSpec.shared_examples 'ignores the published event' do
+ include AfterNextHelpers
+
+ it 'does not consume the published event', :sidekiq_inline do
+ expect_next(described_class).not_to receive(:handle_event)
+
+ ::Gitlab::EventStore.publish(event)
+ end
+end
+
def consume_event(subscriber:, event:)
subscriber.new.perform(event.class.name, event.data)
end
diff --git a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb
deleted file mode 100644
index fdca326dbea..00000000000
--- a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'tracks assignment and records the subject' do |experiment, subject_type|
- before do
- stub_experiments(experiment => true)
- end
-
- 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
- expect(Experiment).to receive(:add_subject).with(experiment.to_s, variant: anything, subject: subject)
-
- action
- end
-end
diff --git a/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb
new file mode 100644
index 00000000000..f26b9a4a7bd
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+def raw_repo_without_container(repository)
+ Gitlab::Git::Repository.new(repository.shard,
+ "#{repository.disk_path}.git",
+ repository.repo_type.identifier_for_container(repository.container),
+ repository.container.full_path)
+end
+
+RSpec.shared_examples 'Gitaly feature flag actors are inferred from repository' do
+ it 'captures correct actors' do
+ service.repository_actor = repository
+
+ expect(service.repository_actor.flipper_id).to eql(repository.flipper_id)
+
+ if expected_project.nil?
+ expect(service.project_actor).to be(nil)
+ else
+ expect(service.project_actor.flipper_id).to eql(expected_project.flipper_id)
+ end
+
+ if expected_group.nil?
+ expect(service.group_actor).to be(nil)
+ else
+ expect(service.group_actor.flipper_id).to eql(expected_group.flipper_id)
+ end
+ end
+
+ it 'does not issues SQL queries after the first invocation' do
+ service.repository_actor = repository
+
+ service.repository_actor
+ service.project_actor
+ service.group_actor
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ 3.times do
+ service.repository_actor
+ service.project_actor
+ service.group_actor
+ end
+ end
+
+ expect(recorder.count).to be(0)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb
index 4b4a7f4ce9d..a2a4c57d62e 100644
--- a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb
@@ -52,7 +52,7 @@ RSpec.shared_examples 'acts as branch pipeline' do |jobs|
context 'when branch pipeline' do
let(:pipeline_branch) { default_branch }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) }
- let(:pipeline) { service.execute!(:push).payload }
+ let(:pipeline) { service.execute(:push).payload }
it 'includes a job' do
expect(pipeline.builds.pluck(:name)).to match_array(jobs)
diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
index 71b32005c55..e0b411e1e2a 100644
--- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
@@ -78,8 +78,8 @@ end
# Expects to following variables:
# - subject
# - sentry_api_response
-# - sentry_url, token - only if enabled_by_default: false
-RSpec.shared_examples 'Sentry API response size limit' do |enabled_by_default: false|
+# - sentry_url, token
+RSpec.shared_examples 'Sentry API response size limit' do
let(:invalid_deep_size) { instance_double(Gitlab::Utils::DeepSize, valid?: false) }
before do
@@ -89,35 +89,8 @@ RSpec.shared_examples 'Sentry API response size limit' do |enabled_by_default: f
.and_return(invalid_deep_size)
end
- if enabled_by_default
- it 'raises an exception when response is too large' do
- expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError,
- 'Sentry API response is too big. Limit is 1 MB.')
- end
- else
- context 'when guarded by feature flag' do
- let(:client) do
- ErrorTracking::SentryClient.new(sentry_url, token, validate_size_guarded_by_feature_flag: feature_flag)
- end
-
- context 'with feature flag enabled' do
- let(:feature_flag) { true }
-
- it 'raises an exception when response is too large' do
- expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError,
- 'Sentry API response is too big. Limit is 1 MB.')
- end
- end
-
- context 'with feature flag disabled' do
- let(:feature_flag) { false }
-
- it 'does not check the limit and thus not raise' do
- expect { subject }.not_to raise_error
-
- expect(Gitlab::Utils::DeepSize).not_to have_received(:new)
- end
- end
- end
+ it 'raises an exception when response is too large' do
+ expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError,
+ 'Sentry API response is too big. Limit is 1 MB.')
end
end
diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb
index 20ed380fb18..919311adc96 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -44,12 +44,12 @@ end
RSpec.shared_examples 'an email with X-GitLab headers containing IDs' do
it 'has X-GitLab-*-ID header' do
- is_expected.to have_header "X-GitLab-#{model.class.name}-ID", "#{model.id}"
+ is_expected.to have_header "X-GitLab-#{model.class.name}-ID", model.id.to_s
end
it 'has X-GitLab-*-IID header if model has iid defined' do
if model.respond_to?(:iid)
- is_expected.to have_header "X-GitLab-#{model.class.name}-IID", "#{model.iid}"
+ is_expected.to have_header "X-GitLab-#{model.class.name}-IID", model.iid.to_s
else
expect(subject.header["X-GitLab-#{model.class.name}-IID"]).to eq nil
end
diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
index 3f187a7e9e4..ef4b08c7865 100644
--- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
@@ -37,7 +37,8 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event)
- expected = if db_role == :primary
+ expected = case db_role
+ when :primary
transform_hash(expected_payload_defaults, {
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
@@ -53,7 +54,7 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
"db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
})
- elsif db_role == :replica
+ when :replica
transform_hash(expected_payload_defaults, {
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
new file mode 100644
index 00000000000..f0581333b28
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :repository, :wiki_repo) }
+ let_it_be(:integration) { create(factory, branches_to_be_notified: 'all', project: project) }
+
+ before do
+ stub_request(:post, integration.webhook)
+ end
+
+ it 'uses only known events', :aggregate_failures do
+ described_class::SUPPORTED_EVENTS_FOR_USAGE_LOG.each do |action|
+ expect(
+ Gitlab::UsageDataCounters::HLLRedisCounter.known_event?("i_ecosystem_slack_service_#{action}_notification")
+ ).to be true
+ end
+ end
+
+ context 'when hook data includes a user object' do
+ let_it_be(:user) { create_default(:user) }
+
+ shared_examples 'increases the usage data counter' do |event_name|
+ subject(:execute) { integration.execute(data) }
+
+ it 'increases the usage data counter' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event).with(event_name, values: user.id).and_call_original
+
+ execute
+ end
+
+ it_behaves_like 'Snowplow event tracking' do
+ let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
+ let(:category) { described_class.to_s }
+ let(:action) { 'perform_integrations_action' }
+ let(:namespace) { project.namespace }
+ let(:label) { 'redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly' }
+ let(:property) { event_name }
+ end
+ end
+
+ context 'when event is not supported for usage log' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
+
+ it 'does not increase the usage data counter' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to receive(:track_event).with('i_ecosystem_slack_service_pipeline_notification', values: user.id)
+
+ integration.execute(data)
+ end
+ end
+
+ context 'for issue notification' do
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ let(:data) { issue.to_hook_data(user) }
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_issue_notification'
+ end
+
+ context 'for push notification' do
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_push_notification'
+ end
+
+ context 'for deployment notification' do
+ let_it_be(:deployment) { create(:deployment, project: project, user: user) }
+
+ let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current) }
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification'
+ end
+
+ context 'for wiki_page notification' do
+ let_it_be(:wiki_page) do
+ create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page')
+ end
+
+ let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
+
+ before do
+ # Skip this method that is not relevant to this test to prevent having
+ # to update project which is frozen
+ allow(project.wiki).to receive(:after_wiki_activity)
+ end
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_wiki_page_notification'
+ end
+
+ context 'for merge_request notification' do
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:data) { merge_request.to_hook_data(user) }
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_merge_request_notification'
+ end
+
+ context 'for note notification' 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 'increases the usage data counter', 'i_ecosystem_slack_service_note_notification'
+ end
+
+ context 'for tag_push notification' do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b' } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { 'refs/tags/v1.1.0' }
+ let(:data) do
+ Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data)
+ end
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_tag_push_notification'
+ end
+
+ context 'for confidential note notification' do
+ let_it_be(:confidential_issue_note) do
+ create(:note_on_issue, project: project, note: 'issue note', confidential: true)
+ end
+
+ let(:data) { Gitlab::DataBuilder::Note.build(confidential_issue_note, user) }
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_note_notification'
+ end
+
+ context 'for confidential issue notification' do
+ let_it_be(:issue) { create(:issue, project: project, confidential: true) }
+
+ let(:data) { issue.to_hook_data(user) }
+
+ it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_issue_notification'
+ end
+ end
+
+ context 'when hook data does not include a user' do
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(create(:ci_pipeline, project: project)) }
+
+ it 'does not increase the usage data counter' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ integration.execute(data)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
index 7512a9f2855..974fc8f402a 100644
--- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
@@ -152,7 +152,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'issue events' do
- let_it_be(:issue) { create(:issue) }
+ let_it_be(:issue) { create(:issue, project: project) }
let(:data) { issue.to_hook_data(user) }
@@ -192,7 +192,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'merge request events' do
- let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
let(:data) { merge_request.to_hook_data(user) }
@@ -210,7 +210,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'wiki page events' do
- let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') }
+ let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, project: project, message: 'user created page: Awesome wiki_page') }
let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
@@ -228,7 +228,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'deployment events' do
- let_it_be(:deployment) { create(:deployment) }
+ let_it_be(:deployment) { create(:deployment, project: project) }
let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, 'created', Time.current) }
@@ -275,8 +275,8 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
describe 'Push events' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, creator: user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
before do
allow(chat_integration).to receive_messages(
@@ -327,7 +327,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch' do
- before do
+ before(:all) do
create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch')
end
@@ -369,7 +369,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch with protected branches defined using wildcards' do
- before do
+ before(:all) do
create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable')
end
@@ -450,8 +450,8 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
describe 'Note events' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, creator: user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
before do
allow(chat_integration).to receive_messages(
@@ -519,8 +519,8 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
describe 'Pipeline events' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, creator: user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
let(:pipeline) do
create(:ci_pipeline,
project: project, status: status,
@@ -582,7 +582,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch' do
- before do
+ before(:all) do
create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch')
end
@@ -612,7 +612,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch with protected branches defined usin wildcards' do
- before do
+ before(:all) do
create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable')
end
@@ -673,7 +673,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, :repository, creator: user) }
- let(:deployment) do
+ let_it_be(:deployment) do
create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch)
end
@@ -692,11 +692,11 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
it_behaves_like "triggered #{integration_name} integration", event_type: "deployment"
context 'on a protected branch' do
- before do
+ before(:all) do
create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch')
end
- let(:deployment) do
+ let_it_be(:deployment) do
create(:deployment, :success, project: project, sha: project.commit.sha, ref: 'a-protected-branch')
end
diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
index 3d393e6dcb5..c6d6e00c781 100644
--- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'includes Limitable concern' do
describe '#exceeds_limits?' do
- let(:plan_limits) { create(:plan_limits, :default_plan) }
+ let_it_be_with_reload(:plan_limits) { create(:plan_limits, :default_plan) }
context 'without plan limits configured' do
it { expect(subject.exceeds_limits?).to eq false }
@@ -26,7 +26,7 @@ RSpec.shared_examples 'includes Limitable concern' do
end
describe 'validations' do
- let(:plan_limits) { create(:plan_limits, :default_plan) }
+ let_it_be_with_reload(:plan_limits) { create(:plan_limits, :default_plan) }
it { is_expected.to be_a(Limitable) }
diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
index 174b8609337..ac34ee32c6d 100644
--- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb
@@ -7,6 +7,11 @@ RSpec.shared_examples 'ttl_expirable' do
it_behaves_like 'having unique enum values'
+ describe 'default values', :freeze_time do
+ it { expect(described_class.new.read_at).to be_like_time(Time.zone.now) }
+ it { expect(described_class.new(read_at: 1.day.ago).read_at).to be_like_time(1.day.ago) }
+ end
+
describe 'validations' do
it { is_expected.to validate_presence_of(:status) }
end
@@ -38,7 +43,7 @@ RSpec.shared_examples 'ttl_expirable' do
end
end
- describe '#read', :freeze_time do
+ describe '#read!', :freeze_time do
let_it_be(:old_read_at) { 1.day.ago }
let_it_be(:item1) { create(class_symbol, read_at: old_read_at) }
diff --git a/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb
new file mode 100644
index 00000000000..08fab45e41b
--- /dev/null
+++ b/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples Integrations::BaseCi do
+ describe 'default values' do
+ it { expect(subject.category).to eq(:ci) }
+ end
+end
diff --git a/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb
new file mode 100644
index 00000000000..5d7e7633a23
--- /dev/null
+++ b/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples Integrations::BaseMonitoring do
+ describe 'default values' do
+ it { expect(subject.category).to eq(:monitoring) }
+ end
+end
diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
index e35ac9c0d0d..7dfdd24177e 100644
--- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb
@@ -6,6 +6,10 @@ RSpec.shared_examples Integrations::BaseSlashCommands do
it { is_expected.to have_many :chat_names }
end
+ describe 'default values' do
+ it { expect(subject.category).to eq(:chat) }
+ end
+
describe '#valid_token?' do
subject { described_class.new }
diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
index 31ec25249d7..a764d47d7c0 100644
--- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
+++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb
@@ -38,7 +38,7 @@ RSpec.shared_examples Integrations::HasWebHook do
end
describe '#url_variables' do
- it 'returns a string' do
+ it 'returns a hash' do
expect(integration.url_variables).to be_a(Hash)
end
end
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 23026167b19..5be0f6349ea 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
@@ -199,7 +199,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
expect(component_file)
.to receive(:update_column)
- .with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL)
+ .with('file_store', ::Packages::PackageFileUploader::Store::LOCAL)
.and_call_original
expect { subject }.to change { component_file.size }.from(nil).to(74)
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index b1aa90449e1..7e69a6663d5 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -161,9 +161,10 @@ RSpec.shared_examples 'wiki model' do
let(:wiki_pages) { subject.list_pages }
before do
- subject.create_page('index', 'This is an index')
+ # The order is intentional
subject.create_page('index2', 'This is an index2')
- subject.create_page('an index3', 'This is an index3')
+ subject.create_page('index', 'This is an index')
+ subject.create_page('index3', 'This is an index3')
end
it 'returns an array of WikiPage instances' do
@@ -183,13 +184,47 @@ RSpec.shared_examples 'wiki model' do
context 'with limit option' do
it 'returns limited set of pages' do
- expect(subject.list_pages(limit: 1).count).to eq(1)
+ expect(
+ subject.list_pages(limit: 1).map(&:title)
+ ).to eql(%w[index])
+ end
+
+ it 'returns all set of pages if limit is more than the total pages' do
+ expect(subject.list_pages(limit: 4).count).to eq(3)
+ end
+
+ it 'returns all set of pages if limit is 0' do
+ expect(subject.list_pages(limit: 0).count).to eq(3)
+ end
+ end
+
+ context 'with offset option' do
+ it 'returns offset-ed set of pages' do
+ expect(
+ subject.list_pages(offset: 1).map(&:title)
+ ).to eq(%w[index2 index3])
+
+ expect(
+ subject.list_pages(offset: 2).map(&:title)
+ ).to eq(["index3"])
+ expect(subject.list_pages(offset: 3).count).to eq(0)
+ expect(subject.list_pages(offset: 4).count).to eq(0)
+ end
+
+ it 'returns all set of pages if offset is 0' do
+ expect(subject.list_pages(offset: 0).count).to eq(3)
+ end
+
+ it 'can combines with limit' do
+ expect(
+ subject.list_pages(offset: 1, limit: 1).map(&:title)
+ ).to eq(["index2"])
end
end
context 'with sorting options' do
it 'returns pages sorted by title by default' do
- pages = ['an index3', 'index', 'index2']
+ pages = %w[index index2 index3]
expect(subject.list_pages.map(&:title)).to eq(pages)
expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse)
@@ -200,24 +235,14 @@ RSpec.shared_examples 'wiki model' do
let(:pages) { subject.list_pages(load_content: true) }
it 'loads WikiPage content' do
- expect(pages.first.content).to eq('This is an index3')
- expect(pages.second.content).to eq('This is an index')
- expect(pages.third.content).to eq('This is an index2')
+ expect(pages.first.content).to eq('This is an index')
+ expect(pages.second.content).to eq('This is an index2')
+ expect(pages.third.content).to eq('This is an index3')
end
end
end
- context 'list pages with legacy wiki rpcs' do
- before do
- stub_feature_flags(wiki_list_page_with_normal_repository_rpcs: false)
- end
-
- it_behaves_like 'wiki model #list_pages'
- end
-
- context 'list pages with normal repository rpcs' do
- it_behaves_like 'wiki model #list_pages'
- end
+ it_behaves_like 'wiki model #list_pages'
end
describe '#sidebar_entries' do
@@ -821,29 +846,6 @@ RSpec.shared_examples 'wiki model' do
end
end
- describe '#ensure_repository' do
- context 'if the repository exists' do
- it 'does not create the repository' do
- expect(subject.repository.exists?).to eq(true)
- expect(subject.repository.raw).not_to receive(:create_repository)
-
- subject.ensure_repository
- end
- end
-
- context 'if the repository does not exist' do
- let(:wiki_container) { wiki_container_without_repo }
-
- it 'creates the repository' do
- expect(subject.repository.exists?).to eq(false)
-
- subject.ensure_repository
-
- expect(subject.repository.exists?).to eq(true)
- end
- end
- end
-
describe '#hook_attrs' do
it 'returns a hash with values' do
expect(subject.hook_attrs).to be_a Hash
diff --git a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
index e725de8ad31..f5431b29ee2 100644
--- a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
@@ -12,49 +12,60 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do
project.add_maintainer(user3)
end
- context 'when feature flag is turned on' do
- context "when the number of users of issuable does exceed the limit" do
- before do
- stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2)
+ context "when the number of users of issuable does exceed the limit" do
+ before do
+ stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2)
+ end
+
+ it 'will not add more than the allowed number of users' do
+ allow_next_instance_of(update_service) do |service|
+ expect(service).not_to receive(:execute)
end
- it 'will not add more than the allowed number of users' do
- allow_next_instance_of(update_service) do |service|
- expect(service).not_to receive(:execute)
- end
+ note = described_class.new(project, user, opts.merge(
+ note: note_text,
+ noteable_type: noteable_type,
+ noteable_id: issuable.id,
+ confidential: false
+ )).execute
- note = described_class.new(project, user, opts.merge(
- note: note_text,
- noteable_type: noteable_type,
- noteable_id: issuable.id,
- confidential: false
- )).execute
+ expect(note.errors[:validation]).to match_array([validation_message])
+ end
+ end
- expect(note.errors[:validation]).to match_array([validation_message])
- end
+ context "when the number of users does not exceed the limit" do
+ before do
+ stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 6)
end
- context "when the number of users does not exceed the limit" do
- before do
- stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 6)
+ it 'calls execute and does not return an error' do
+ allow_next_instance_of(update_service) do |service|
+ expect(service).to receive(:execute).and_call_original
end
- it 'calls execute and does not return an error' do
- allow_next_instance_of(update_service) do |service|
- expect(service).to receive(:execute).and_call_original
- end
-
- note = described_class.new(project, user, opts.merge(
- note: note_text,
- noteable_type: noteable_type,
- noteable_id: issuable.id,
- confidential: false
- )).execute
+ note = described_class.new(project, user, opts.merge(
+ note: note_text,
+ noteable_type: noteable_type,
+ noteable_id: issuable.id,
+ confidential: false
+ )).execute
- expect(note.errors[:validation]).to be_empty
- end
+ expect(note.errors[:validation]).to be_empty
end
end
+end
+
+RSpec.shared_examples 'does not exceed the issuable size limit with ff off' do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ project.add_maintainer(user1)
+ project.add_maintainer(user2)
+ project.add_maintainer(user3)
+ end
context 'when feature flag is off' do
before do
diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
index 59e641e2af6..2170025824f 100644
--- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
+++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb
@@ -1,21 +1,98 @@
# frozen_string_literal: true
RSpec.shared_examples 'GET resource access tokens available' do
- let_it_be(:active_resource_access_token) { create(:personal_access_token, user: bot_user) }
+ let_it_be(:active_resource_access_token) { create(:personal_access_token, user: access_token_user) }
- it 'retrieves active resource access tokens' do
- subject
+ it 'retrieves active access tokens' do
+ get_access_tokens
- token_entities = assigns(:active_resource_access_tokens)
+ token_entities = assigns(:active_access_tokens)
expect(token_entities.length).to eq(1)
expect(token_entities[0][:name]).to eq(active_resource_access_token.name)
end
it 'lists all available scopes' do
- subject
+ get_access_tokens
expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes)
end
+
+ it 'returns for json response' do
+ get_access_tokens_json
+
+ expect(json_response.count).to eq(1)
+ end
+end
+
+RSpec.shared_examples 'GET access tokens are paginated and ordered' do
+ before do
+ create(:personal_access_token, user: access_token_user)
+ end
+
+ context "when multiple access tokens are returned" do
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ create(:personal_access_token, user: access_token_user)
+ end
+
+ it "returns paginated response", :aggregate_failures do
+ get_access_tokens_with_page
+ expect(assigns(:active_access_tokens).count).to eq(1)
+
+ expect_header('X-Per-Page', '1')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '2')
+ expect_header('X-Total', '2')
+ end
+ end
+
+ context "when access_token_pagination feature flag is disabled" do
+ before do
+ stub_feature_flags(access_token_pagination: false)
+ create(:personal_access_token, user: access_token_user)
+ end
+
+ it "returns all tokens in system" do
+ get_access_tokens_with_page
+ expect(assigns(:active_access_tokens).count).to eq(2)
+ end
+ end
+
+ context "when tokens returned are ordered" do
+ let(:expires_1_day_from_now) { 1.day.from_now.to_date }
+ let(:expires_2_day_from_now) { 2.days.from_now.to_date }
+
+ before do
+ create(:personal_access_token, user: access_token_user, name: "Token1", expires_at: expires_1_day_from_now)
+ create(:personal_access_token, user: access_token_user, name: "Token2", expires_at: expires_2_day_from_now)
+ end
+
+ it "orders token list ascending on expires_at" do
+ get_access_tokens
+
+ first_token = assigns(:active_access_tokens).first.as_json
+ expect(first_token['name']).to eq("Token1")
+ expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
+ end
+
+ it "orders tokens on id in case token has same expires_at" do
+ create(:personal_access_token, user: access_token_user, name: "Token3", expires_at: expires_1_day_from_now)
+
+ get_access_tokens
+
+ first_token = assigns(:active_access_tokens).first.as_json
+ expect(first_token['name']).to eq("Token3")
+ expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
+
+ second_token = assigns(:active_access_tokens).second.as_json
+ expect(second_token['name']).to eq("Token1")
+ expect(second_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
+ end
+ end
+
+ def expect_header(header_name, header_val)
+ expect(response.headers[header_name]).to eq(header_val)
+ end
end
RSpec.shared_examples 'POST resource access tokens available' do
@@ -83,7 +160,7 @@ end
RSpec.shared_examples 'PUT resource access tokens available' do
it 'calls delete user worker' do
- expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true)
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, access_token_user.id, skip_authorization: true)
subject
end
@@ -91,34 +168,12 @@ RSpec.shared_examples 'PUT resource access tokens available' do
it 'removes membership of bot user' do
subject
- expect(resource.reload.bots).not_to include(bot_user)
+ expect(resource.reload.bots).not_to include(access_token_user)
end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'creates GhostUserMigration records to handle migration in a worker' do
- expect { subject }.to(
- change { Users::GhostUserMigration.count }.from(0).to(1))
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'converts issuables of the bot user to ghost user' do
- issue = create(:issue, author: bot_user)
-
- subject
-
- expect(issue.reload.author.ghost?).to be true
- end
-
- it 'deletes project bot user' do
- subject
-
- expect(User.exists?(bot_user.id)).to be_falsy
- end
+ it 'creates GhostUserMigration records to handle migration in a worker' do
+ expect { subject }.to(
+ change { Users::GhostUserMigration.count }.from(0).to(1))
end
context 'when unsuccessful' do
diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
index 32562aef8d2..f577e2ad323 100644
--- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'with cross-reference system notes' do
new_merge_request.project.add_developer(user)
hidden_merge_request = create(:merge_request)
- new_cross_reference = "test commit #{hidden_merge_request.project.commit}"
+ new_cross_reference = "test commit #{hidden_merge_request.project.commit.to_reference(project)}"
new_note = create(:system_note, noteable: merge_request, project: project, note: new_cross_reference)
create(:system_note_metadata, note: new_note, action: 'cross_reference')
end
diff --git a/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb
index 22805cf7aed..bb492425fd7 100644
--- a/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb
@@ -1,13 +1,15 @@
# frozen_string_literal: true
# Requires `query(params)` , `user`, `issuable_data` and `issuable` bindings
-RSpec.shared_examples 'query with a search term' do
+RSpec.shared_examples 'query with a search term' do |fields = [:DESCRIPTION]|
+ let(:search_term) { 'bar' }
+ let(:ids) { graphql_dig_at(issuable_data, :node, :id) }
+
it 'returns only matching issuables' do
- filter_params = { search: 'bar', in: [:DESCRIPTION] }
+ filter_params = { search: search_term, in: fields }
graphql_query = query(filter_params)
post_graphql(graphql_query, current_user: user)
- ids = graphql_dig_at(issuable_data, :node, :id)
expect(ids).to contain_exactly(issuable.to_global_id.to_s)
end
diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
new file mode 100644
index 00000000000..5469fd80a4f
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'graphql issue list request spec' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_query
+ end
+ end
+
+ describe 'filters' do
+ context 'when filtering by assignees' do
+ context 'when both assignee_username filters are provided' do
+ let(:issue_filter_params) do
+ { assignee_username: current_user.username, assignee_usernames: [current_user.username] }
+ end
+
+ it 'returns a mutually exclusive param error' do
+ post_query
+
+ expect_graphql_errors_to_include(
+ 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.'
+ )
+ end
+ end
+
+ context 'when filtering by a negated argument' do
+ let(:issue_filter_params) { { not: { assignee_usernames: [current_user.username] } } }
+
+ it 'returns correctly filtered issues' do
+ post_query
+
+ expect(issue_ids).to match_array(expected_negated_assignee_issues.map { |i| i.to_gid.to_s })
+ end
+ end
+ end
+
+ context 'when filtering by unioned arguments' do
+ let(:issue_filter_params) { { or: { assignee_usernames: [current_user.username, another_user.username] } } }
+
+ it 'returns correctly filtered issues' do
+ post_query
+
+ expect(issue_ids).to match_array(expected_unioned_assignee_issues.map { |i| i.to_gid.to_s })
+ end
+
+ context 'when argument is blank' do
+ let(:issue_filter_params) { { or: {} } }
+
+ it 'does not raise an error' do
+ post_query
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ it 'returns an error' do
+ stub_feature_flags(or_issuable_queries: false)
+
+ post_query
+
+ expect_graphql_errors_to_include(
+ "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled."
+ )
+ end
+ end
+ end
+
+ context 'when filtering by a blank negated argument' do
+ let(:issue_filter_params) { { not: {} } }
+
+ it 'does not raise an error' do
+ post_query
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when filtering by reaction emoji' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:value, :issue_list) do
+ 'thumbsup' | lazy { voted_issues }
+ 'ANY' | lazy { voted_issues }
+ 'any' | lazy { voted_issues }
+ 'AnY' | lazy { voted_issues }
+ 'NONE' | lazy { no_award_issues }
+ 'thumbsdown' | lazy { [] }
+ end
+
+ with_them do
+ let(:issue_filter_params) { { my_reaction_emoji: value } }
+ let(:gids) { to_gid_list(issue_list) }
+
+ it 'returns correctly filtered issues' do
+ post_query
+
+ expect(issue_ids).to match_array(gids)
+ end
+ end
+ end
+
+ context 'when filtering by search' do
+ it_behaves_like 'query with a search term', [:TITLE] do
+ let(:search_term) { search_title_term }
+ let(:issuable_data) { issues_data }
+ let(:user) { current_user }
+ let(:issuable) { title_search_issue }
+ let(:ids) { issue_ids }
+ end
+ end
+ end
+
+ describe 'sorting and pagination' do
+ context 'when sorting by severity' do
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :SEVERITY_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_severity_sorted_asc) }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :SEVERITY_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_severity_sorted_asc.reverse) }
+ end
+ end
+ end
+
+ context 'when sorting by priority' do
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :PRIORITY_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_priority_sorted_asc) }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :PRIORITY_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { to_gid_list(expected_priority_sorted_desc) }
+ end
+ end
+ end
+ end
+
+ it 'includes a web_url' do
+ post_query
+
+ expect(issues_data[0]['webUrl']).to be_present
+ end
+
+ it 'includes discussion locked' do
+ post_query
+
+ expect(issues_data).to contain_exactly(
+ *locked_discussion_issues.map { |i| hash_including('id' => i.to_gid.to_s, 'discussionLocked' => true) },
+ *unlocked_discussion_issues.map { |i| hash_including('id' => i.to_gid.to_s, 'discussionLocked' => false) }
+ )
+ end
+
+ def to_gid_list(instance_list)
+ instance_list.map { |instance| instance.to_gid.to_s }
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index 1b609915f32..fb4aacfd7a9 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -114,7 +114,7 @@ RSpec.shared_examples 'group and project packages query' do
end
[:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC, :CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order|
- context "#{order}" do
+ context order.to_s do
let(:sorted_packages) { packages_order_map.fetch(order) }
it_behaves_like 'sorted paginated query' do
diff --git a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
new file mode 100644
index 00000000000..54cc13fac94
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'perform graphql requests for AccessLevel type objects' do |access_level_kind|
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+ let_it_be(:variables) { { path: project.full_path } }
+
+ let(:fields) { all_graphql_fields_for("#{access_level_kind.to_s.classify}AccessLevel", max_depth: 2) }
+ let(:access_levels) { protected_branch.public_send("#{access_level_kind}_access_levels") }
+ let(:access_levels_count) { access_levels.size }
+ let(:maintainer_access_level) { access_levels.for_role.first }
+ let(:maintainer_access_level_data) { access_levels_data.first }
+ let(:access_levels_data) do
+ graphql_data_at('project',
+ 'branchRules',
+ 'nodes',
+ 0,
+ 'branchProtection',
+ "#{access_level_kind.to_s.camelize(:lower)}AccessLevels",
+ 'nodes')
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!) {
+ project(fullPath: $path) {
+ branchRules(first: 1) {
+ nodes {
+ branchProtection {
+ #{access_level_kind.to_s.camelize(:lower)}AccessLevels {
+ nodes {
+ #{fields}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ context 'when request AccessLevel type objects as a guest user' do
+ let_it_be(:protected_branch) { create(:protected_branch, project: project) }
+
+ before do
+ project.add_guest(current_user)
+
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(access_levels_data).not_to be_present }
+ end
+
+ context 'when request AccessLevel type objects as a maintainer' do
+ let_it_be(:protected_branch) do
+ create(:protected_branch, "maintainers_can_#{access_level_kind}", project: project)
+ end
+
+ before do
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all the access level attributes' do
+ expect(maintainer_access_level_data['accessLevel']).to eq(maintainer_access_level.access_level)
+ expect(maintainer_access_level_data['accessLevelDescription']).to eq(maintainer_access_level.humanize)
+ expect(maintainer_access_level_data.dig('group', 'name')).to be_nil
+ expect(maintainer_access_level_data.dig('user', 'name')).to be_nil
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/issues_shared_examples.rb b/spec/support/shared_examples/requests/api/issues_shared_examples.rb
index 991dbced02d..6328fb9cd8a 100644
--- a/spec/support/shared_examples/requests/api/issues_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/issues_shared_examples.rb
@@ -37,7 +37,7 @@ RSpec.shared_examples 'labeled issues with labels and label_name params' do
context 'negation' do
context 'array of labeled issues when all labels match with negation' do
- let(:params) { { labels: "#{label.title},#{label_b.title}", not: { labels: "#{label_c.title}" } } }
+ let(:params) { { labels: "#{label.title},#{label_b.title}", not: { labels: label_c.title.to_s } } }
it_behaves_like 'returns negated label names'
end
diff --git a/spec/support/shared_examples/requests/api/members_shared_examples.rb b/spec/support/shared_examples/requests/api/members_shared_examples.rb
index fce75c29971..9136f60eb93 100644
--- a/spec/support/shared_examples/requests/api/members_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/members_shared_examples.rb
@@ -11,3 +11,11 @@ RSpec.shared_examples 'a 404 response when source is private' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+RSpec.shared_examples 'a 403 response when user does not have rights to manage members of a specific access level' do
+ it 'returns 403' do
+ route
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
index fa111ca5811..d749479544d 100644
--- a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb
@@ -5,6 +5,7 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition|
context 'multiple issue boards' do
before do
+ stub_feature_flags(apollo_boards: false)
board_parent.add_reporter(user)
stub_licensed_features(multiple_group_issue_boards: true)
end
diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
index 8479493911b..11f9565989f 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -179,7 +179,8 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
end
- if parent_type == 'projects'
+ case parent_type
+ when 'projects'
context 'by a project owner' do
let(:user) { project.first_owner }
@@ -211,7 +212,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
expect(Time.parse(json_response['updated_at'])).to be_like_time(creation_time)
end
end
- elsif parent_type == 'groups'
+ when 'groups'
context 'by a group owner' do
it 'sets the creation time on the new note' do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
@@ -288,7 +289,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
it 'allows user in allow-list to create notes' do
- stub_application_setting(notes_create_limit_allowlist: ["#{user.username}"])
+ stub_application_setting(notes_create_limit_allowlist: [user.username.to_s])
subject
expect(response).to have_gitlab_http_status(:created)
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 11e19d8d067..a9b44015206 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
@@ -221,6 +221,27 @@ RSpec.shared_examples 'rejects PyPI access with unknown group id' do
end
end
+RSpec.shared_examples 'allow access for everyone with public package_registry_access_level' do
+ context 'with private project but public access to package registry' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ end
+
+ context 'as non-member user' do
+ let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'as anonymous' do
+ let(:headers) { {} }
+
+ it_behaves_like 'returning response status', :success
+ end
+ end
+end
+
RSpec.shared_examples 'pypi simple API endpoint' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
index 544a0ed8fdd..bdff2c65691 100644
--- a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb
@@ -63,9 +63,9 @@ RSpec.shared_examples 'redirects to version download' do |user_type, status, add
it 'returns a valid response' do
subject
- expect(request.url).to include 'module-1/system/download'
+ expect(request.url).to include "#{package.name}/download"
expect(response.headers).to include 'Location'
- expect(response.headers['Location']).to include 'module-1/system/1.0.1/download'
+ expect(response.headers['Location']).to include "#{package.name}/1.0.1/download"
end
end
end
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index 571cb7dc03d..b46ace1824a 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -72,8 +72,8 @@ RSpec.shared_examples 'processes one firing and one resolved prometheus alerts'
.and change(Note, :count).by(1)
expect(subject).to be_success
- expect(subject.payload[:alerts]).to all(be_a_kind_of(AlertManagement::Alert))
- expect(subject.payload[:alerts].size).to eq(1)
+ expect(subject.payload).to eq({})
+ expect(subject.http_status).to eq(:created)
end
it_behaves_like 'processes incident issues'
diff --git a/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb b/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb
deleted file mode 100644
index c9520852a5b..00000000000
--- a/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'handling rpm xml file' do
- include_context 'with rpm package data'
-
- let(:xml) { nil }
- let(:data) { {} }
-
- context 'when generate empty xml' do
- it 'generate expected xml' do
- expect(subject).to eq(empty_xml)
- end
- end
-
- context 'when updating existing xml' do
- let(:xml) { empty_xml }
- let(:data) { xml_update_params }
-
- shared_examples 'changing root tag attribute' do
- it "increment previous 'packages' value by 1" do
- previous_value = Nokogiri::XML(xml).at(described_class::ROOT_TAG).attributes["packages"].value.to_i
- new_value = Nokogiri::XML(subject).at(described_class::ROOT_TAG).attributes["packages"].value.to_i
-
- expect(previous_value + 1).to eq(new_value)
- end
- end
-
- it 'generate valid xml add expected xml node to existing xml' do
- # Have one root attribute
- result = Nokogiri::XML::Document.parse(subject).remove_namespaces!
- expect(result.children.count).to eq(1)
-
- # Root node has 1 child with generated node
- expect(result.xpath("//#{described_class::ROOT_TAG}/package").count).to eq(1)
- end
-
- context 'when empty xml' do
- it_behaves_like 'changing root tag attribute'
- end
-
- context 'when xml has children' do
- let(:xml) { described_class.new(xml: empty_xml, data: data).execute }
-
- it 'has children nodes' do
- result = Nokogiri::XML::Document.parse(xml).remove_namespaces!
- expect(result.children.count).to be > 0
- end
-
- it_behaves_like 'changing root tag attribute'
- end
- end
-end
diff --git a/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb
new file mode 100644
index 00000000000..c38ca6a3bf0
--- /dev/null
+++ b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'listing issuable discussions' do |user_role, internal_discussion_count, total_discussions_count|
+ before_all do
+ create_notes(issuable, "some user comment")
+ end
+
+ context 'when user cannot read issue' do
+ it "returns no notes" do
+ expect(discussions_service.execute).to be_empty
+ end
+ end
+
+ context 'when user can read issuable' do
+ before do
+ group.add_developer(current_user)
+ end
+
+ context 'with paginated results' do
+ let(:finder_params_for_issuable) { { per_page: 2 } }
+ let(:next_page_cursor) { { cursor: discussions_service.paginator.cursor_for_next_page } }
+
+ it "returns next page notes" do
+ next_page_discussions_service = described_class.new(current_user, issuable,
+ finder_params_for_issuable.merge(next_page_cursor))
+ discussions = next_page_discussions_service.execute
+
+ expect(discussions.count).to eq(2)
+ expect(discussions.first.notes.map(&:note)).to match_array(["added #{label.to_reference} label"])
+ expect(discussions.second.notes.map(&:note)).to match_array(["removed #{label.to_reference} label"])
+ end
+ end
+
+ # confidential notes are currently available only on issues and epics
+ context 'and cannot read confidential notes' do
+ before do
+ group.add_member(current_user, user_role)
+ end
+
+ it "returns non confidential notes" do
+ discussions = discussions_service.execute
+
+ non_conf_discussion_count = total_discussions_count - internal_discussion_count
+ expect(discussions.count).to eq(non_conf_discussion_count)
+ expect(discussions.count { |disc| disc.notes.any?(&:confidential) }).to eq(0)
+ expect(discussions.count { |disc| !disc.notes.any?(&:confidential) }).to eq(non_conf_discussion_count)
+ end
+ end
+
+ # confidential notes are currently available only on issues and epics
+ context 'and can read confidential notes' do
+ it "returns all notes" do
+ discussions = discussions_service.execute
+
+ expect(discussions.count).to eq(total_discussions_count)
+ expect(discussions.count { |disc| disc.notes.any?(&:confidential) }).to eq(internal_discussion_count)
+ non_conf_discussion_count = total_discussions_count - internal_discussion_count
+ expect(discussions.count { |disc| !disc.notes.any?(&:confidential) }).to eq(non_conf_discussion_count)
+ end
+ end
+
+ context 'and system notes only' do
+ let(:finder_params_for_issuable) { { notes_filter: UserPreference::NOTES_FILTERS[:only_activity] } }
+
+ it "returns system notes" do
+ discussions = discussions_service.execute
+
+ expect(discussions.count { |disc| disc.notes.any?(&:system) }).to be > 0
+ expect(discussions.count { |disc| !disc.notes.any?(&:system) }).to eq(0)
+ end
+ end
+
+ context 'and user comments only' do
+ let(:finder_params_for_issuable) { { notes_filter: UserPreference::NOTES_FILTERS[:only_comments] } }
+
+ it "returns user comments" do
+ discussions = discussions_service.execute
+
+ expect(discussions.count { |disc| disc.notes.any?(&:system) }).to eq(0)
+ expect(discussions.count { |disc| !disc.notes.any?(&:system) }).to be > 0
+ end
+ end
+ end
+end
+
+def create_notes(issuable, note_body)
+ assoc_name = issuable.to_ability_name
+
+ create(:note, system: true, project: issuable.project, noteable: issuable)
+
+ first_discussion = create(:discussion_note_on_issue, noteable: issuable, project: issuable.project, note: note_body)
+ create(:note,
+ discussion_id: first_discussion.discussion_id, noteable: issuable,
+ project: issuable.project, note: "reply on #{note_body}")
+
+ create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'add')
+ create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'remove')
+
+ unless issuable.is_a?(Epic)
+ create(:resource_milestone_event, "#{assoc_name}": issuable, milestone: milestone, action: 'add')
+ create(:resource_milestone_event, "#{assoc_name}": issuable, milestone: milestone, action: 'remove')
+ end
+
+ # confidential notes are currently available only on issues and epics
+ return unless issuable.is_a?(Issue) || issuable.is_a?(Epic)
+
+ first_internal_discussion = create(:discussion_note_on_issue, :confidential,
+ noteable: issuable, project: issuable.project, note: "confidential #{note_body}")
+ create(:note, :confidential,
+ discussion_id: first_internal_discussion.discussion_id, noteable: issuable,
+ project: issuable.project, note: "reply on confidential #{note_body}")
+end
diff --git a/spec/support/shared_examples/services/merge_status_updated_trigger_shared_examples.rb b/spec/support/shared_examples/services/merge_status_updated_trigger_shared_examples.rb
new file mode 100644
index 00000000000..97e3b0a44a7
--- /dev/null
+++ b/spec/support/shared_examples/services/merge_status_updated_trigger_shared_examples.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do
+ specify do
+ expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(merge_request)
+
+ action
+ end
+end
+
+RSpec.shared_examples 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do
+ specify do
+ expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated)
+
+ action
+ end
+end
diff --git a/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb
index 09820593cdb..46a1f4b6598 100644
--- a/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb
@@ -20,7 +20,7 @@ RSpec.shared_examples_for 'dismissing user callout' do |model|
old_time = 1.day.ago
new_time = Time.current
attributes = params.merge(dismissed_at: old_time, user: user)
- existing_callout = create("#{model.name.split('::').last.underscore}".to_sym, attributes)
+ existing_callout = create(model.name.split('::').last.underscore.to_s.to_sym, attributes)
expect { execute }.to change { existing_callout.reload.dismissed_at }.from(old_time).to(new_time)
end
diff --git a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
new file mode 100644
index 00000000000..ac17915c15a
--- /dev/null
+++ b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "setting work item's milestone" do
+ context "when 'milestone' param does not exist" do
+ let(:params) { {} }
+
+ it "does not set the work item's milestone" do
+ expect { execute_callback }.to not_change(work_item, :milestone)
+ end
+ end
+
+ context "when 'milestone' is not in the work item's project's hierarchy" do
+ let(:another_group_milestone) { create(:milestone, group: create(:group)) }
+ let(:params) { { milestone_id: another_group_milestone.id } }
+
+ it "does not set the work item's milestone" do
+ expect { execute_callback }.to not_change(work_item, :milestone)
+ end
+ end
+
+ context 'when assigning a group milestone' do
+ let(:params) { { milestone_id: group_milestone.id } }
+
+ it "sets the work item's milestone" do
+ expect { execute_callback }
+ .to change(work_item, :milestone)
+ .from(nil)
+ .to(group_milestone)
+ end
+ end
+
+ context 'when assigning a project milestone' do
+ let(:params) { { milestone_id: project_milestone.id } }
+
+ it "sets the work item's milestone" do
+ expect { execute_callback }
+ .to change(work_item, :milestone)
+ .from(nil)
+ .to(project_milestone)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
index 3c977e62a10..af56f8ffac7 100644
--- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -26,9 +26,10 @@ RSpec.shared_examples "migrates" do |to_store:, from_store: nil|
expect(subject).to be_an(CarrierWave::Uploader::Base)
expect(subject).to be_a(ObjectStorage::Concern)
- if from == described_class::Store::REMOTE
+ case from
+ when described_class::Store::REMOTE
expect(subject.file).to be_a(CarrierWave::Storage::Fog::File)
- elsif from == described_class::Store::LOCAL
+ when described_class::Store::LOCAL
expect(subject.file).to be_a(CarrierWave::SanitizedFile)
else
raise 'Unexpected file type'
diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
index 3ba5f080a01..0be55fd2a3e 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
@@ -137,8 +137,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
let(:lease_timeout) { 15.minutes }
let(:lease_key) { described_class.name.demodulize.underscore }
let(:interval_variance) { described_class::INTERVAL_VARIANCE }
+ let(:migration_id) { 123 }
let(:migration) do
- build(:batched_background_migration, :active, interval: job_interval, table_name: table_name)
+ build(
+ :batched_background_migration, :active,
+ id: migration_id, interval: job_interval, table_name: table_name
+ )
end
before do
@@ -150,45 +154,6 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
allow(migration).to receive(:reload)
end
- context 'when the reloaded migration is no longer active' do
- it 'does not run the migration' do
- expect_to_obtain_exclusive_lease(lease_key, timeout: lease_timeout)
-
- expect(migration).to receive(:reload)
- expect(migration).to receive(:active?).and_return(false)
-
- expect(worker).not_to receive(:run_active_migration)
-
- worker.perform
- end
- end
-
- context 'when the interval has not elapsed' do
- it 'does not run the migration' do
- expect_to_obtain_exclusive_lease(lease_key, timeout: lease_timeout)
-
- expect(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(false)
-
- expect(worker).not_to receive(:run_active_migration)
-
- worker.perform
- end
- end
-
- context 'when the reloaded migration is still active and the interval has elapsed' do
- it 'runs the migration' do
- expect_to_obtain_exclusive_lease(lease_key, timeout: lease_timeout)
-
- expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |instance|
- expect(instance).to receive(:run_migration_job).with(migration)
- end
-
- expect(worker).to receive(:run_active_migration).and_call_original
-
- worker.perform
- end
- end
-
context 'when the calculated timeout is less than the minimum allowed' do
let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT }
let(:job_interval) { 2.minutes }
@@ -196,8 +161,8 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
it 'sets the lease timeout to the minimum value' do
expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout)
- expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |instance|
- expect(instance).to receive(:run_migration_job).with(migration)
+ expect_next_instance_of(Database::BatchedBackgroundMigration::ExecutionWorker) do |worker|
+ expect(worker).to receive(:perform).with(tracking_database, migration_id)
end
expect(worker).to receive(:run_active_migration).and_call_original
@@ -217,10 +182,13 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
expect { worker.perform }.to raise_error(RuntimeError, 'I broke')
end
- it 'receives the correct connection' do
+ it 'delegetes the execution to ExecutionWorker' do
base_model = Gitlab::Database.database_base_models[tracking_database]
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield
+ expect_next_instance_of(Database::BatchedBackgroundMigration::ExecutionWorker) do |worker|
+ expect(worker).to receive(:perform).with(tracking_database, migration_id)
+ end
worker.perform
end
@@ -236,10 +204,10 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
let(:migration_class) do
Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
job_arguments :matching_status
+ operation_name :update_all
def perform
each_sub_batch(
- operation_name: :update_all,
batching_scope: -> (relation) { relation.where(status: matching_status) }
) do |sub_batch|
sub_batch.update_all(some_column: 0)
diff --git a/spec/support/sidekiq_middleware.rb b/spec/support/sidekiq_middleware.rb
index cbd6163d46b..73f43487d7c 100644
--- a/spec/support/sidekiq_middleware.rb
+++ b/spec/support/sidekiq_middleware.rb
@@ -6,6 +6,15 @@ require 'sidekiq/testing'
module SidekiqMiddleware
def with_sidekiq_server_middleware(&block)
Sidekiq::Testing.server_middleware.clear
+
+ if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.7')
+ raise 'New version of sidekiq detected, please remove this line'
+ end
+
+ # This line is a workaround for a Sidekiq bug that is already fixed in v7.0.0
+ # https://github.com/mperham/sidekiq/commit/1b83a152786ed382f07fff12d2608534f1e3c922
+ Sidekiq::Testing.server_middleware.instance_variable_set(:@config, Sidekiq)
+
Sidekiq::Testing.server_middleware(&block)
ensure
Sidekiq::Testing.server_middleware.clear
diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb
index f952f7f0985..b9bd3f82f65 100644
--- a/spec/support/webmock.rb
+++ b/spec/support/webmock.rb
@@ -15,6 +15,13 @@ def webmock_allowed_hosts
end.compact.uniq
end
+def with_net_connect_allowed
+ WebMock.allow_net_connect!
+ yield
+ensure
+ webmock_enable!
+end
+
# This prevents Selenium/WebMock from spawning thousands of connections
# while waiting for an element to appear via Capybara's find:
# https://github.com/teamcapybara/capybara/issues/2322#issuecomment-619321520
diff --git a/spec/support_specs/database/multiple_databases_spec.rb b/spec/support_specs/database/multiple_databases_helpers_spec.rb
index 0b019462077..eb0e980d376 100644
--- a/spec/support_specs/database/multiple_databases_spec.rb
+++ b/spec/support_specs/database/multiple_databases_helpers_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Database::MultipleDatabases' do
+RSpec.describe 'Database::MultipleDatabasesHelpers' do
let(:query) do
<<~SQL
WITH cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (SELECT 1) SELECT 1;
@@ -72,7 +72,8 @@ RSpec.describe 'Database::MultipleDatabases' do
context 'when reconnect is false' do
it 'does raise exception' do
with_reestablished_active_record_base(reconnect: false) do
- expect { ApplicationRecord.connection.execute("SELECT 1") }.to raise_error(ActiveRecord::ConnectionNotEstablished)
+ expect { ApplicationRecord.connection.execute("SELECT 1") }
+ .to raise_error(ActiveRecord::ConnectionNotEstablished)
end
end
end
diff --git a/spec/support_specs/graphql/arguments_spec.rb b/spec/support_specs/graphql/arguments_spec.rb
index 925af1ab79c..925f8c15d68 100644
--- a/spec/support_specs/graphql/arguments_spec.rb
+++ b/spec/support_specs/graphql/arguments_spec.rb
@@ -6,14 +6,14 @@ RSpec.describe Graphql::Arguments do
it 'returns a blank string if the arguments are blank' do
args = described_class.new({})
- expect("#{args}").to be_blank
+ expect(args.to_s).to be_blank
end
it 'returns a serialized arguments if the arguments are not blank' do
units = described_class.new({ temp: :CELSIUS, time: :MINUTES })
args = described_class.new({ temp: 180, time: 45, units: units })
- expect("#{args}").to eq('temp: 180, time: 45, units: {temp: CELSIUS, time: MINUTES}')
+ expect(args.to_s).to eq('temp: 180, time: 45, units: {temp: CELSIUS, time: MINUTES}')
end
it 'supports merge with +' do
diff --git a/spec/tasks/gitlab/sidekiq_rake_spec.rb b/spec/tasks/gitlab/sidekiq_rake_spec.rb
index 75f904389e2..0e5111c90a1 100644
--- a/spec/tasks/gitlab/sidekiq_rake_spec.rb
+++ b/spec/tasks/gitlab/sidekiq_rake_spec.rb
@@ -9,7 +9,10 @@ RSpec.describe 'sidekiq.rake', :aggregate_failures, :silence_stdout do
stub_warn_user_is_not_gitlab
end
+ let(:migrator) { ::Gitlab::SidekiqMigrateJobs.new(mappings, logger: Logger.new($stdout)) }
+
shared_examples 'migration rake task' do
+ let(:mappings) { {} }
it 'runs the migrator with a mapping of workers to queues' do
test_routes = [
['urgency=high', 'default'],
@@ -17,20 +20,17 @@ RSpec.describe 'sidekiq.rake', :aggregate_failures, :silence_stdout do
]
test_router = ::Gitlab::SidekiqConfig::WorkerRouter.new(test_routes)
- migrator = ::Gitlab::SidekiqMigrateJobs.new(sidekiq_set, logger: Logger.new($stdout))
allow(::Gitlab::SidekiqConfig::WorkerRouter)
.to receive(:global).and_return(test_router)
expect(::Gitlab::SidekiqMigrateJobs)
- .to receive(:new).with(sidekiq_set, logger: an_instance_of(Logger)).and_return(migrator)
+ .to receive(:new).with(a_hash_including('PostReceive' => 'default',
+ 'MergeWorker' => 'default',
+ 'DeleteDiffFilesWorker' => 'delete_diff_files'),
+ logger: an_instance_of(Logger)).and_return(migrator)
- expect(migrator)
- .to receive(:execute)
- .with(a_hash_including('PostReceive' => 'default',
- 'MergeWorker' => 'default',
- 'DeleteDiffFilesWorker' => 'delete_diff_files'))
- .and_call_original
+ expect(migrator).to receive(:migrate_set).with(sidekiq_set).and_call_original
run_rake_task("gitlab:sidekiq:migrate_jobs:#{sidekiq_set}")
@@ -50,4 +50,30 @@ RSpec.describe 'sidekiq.rake', :aggregate_failures, :silence_stdout do
it_behaves_like 'migration rake task'
end
+
+ describe 'gitlab:sidekiq:migrate_jobs:queued rake task' do
+ let(:mappings) { { 'PostReceive' => 'default' } }
+
+ it 'runs the migrator with a mapping of workers to queues' do
+ test_routes = [
+ ['*', 'default']
+ ]
+
+ test_router = ::Gitlab::SidekiqConfig::WorkerRouter.new(test_routes)
+
+ allow(::Gitlab::SidekiqConfig::WorkerRouter)
+ .to receive(:global).and_return(test_router)
+
+ expect(::Gitlab::SidekiqMigrateJobs)
+ .to receive(:new).with(a_hash_including('PostReceive' => 'default',
+ 'MergeWorker' => 'default'),
+ logger: an_instance_of(Logger)).and_return(migrator)
+
+ expect(migrator).to receive(:migrate_queues).and_call_original
+
+ run_rake_task("gitlab:sidekiq:migrate_jobs:queued")
+
+ expect($stdout.string).to include('List of queues based on routing rules: ["default"]')
+ end
+ end
end
diff --git a/spec/tasks/gitlab/update_templates_rake_spec.rb b/spec/tasks/gitlab/update_templates_rake_spec.rb
index 7eccdf22a1f..85da490f718 100644
--- a/spec/tasks/gitlab/update_templates_rake_spec.rb
+++ b/spec/tasks/gitlab/update_templates_rake_spec.rb
@@ -4,6 +4,7 @@ require 'rake_helper'
RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout do
let!(:tmpdir) { Dir.mktmpdir }
+ let(:template) { Gitlab::ProjectTemplate.find(:rails) }
before do
Rake.application.rake_require 'tasks/gitlab/update_templates'
@@ -14,7 +15,7 @@ RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout do
.and_return(Pathname.new(tmpdir))
# Gitlab::HTTP resolves the domain to an IP prior to WebMock taking effect, hence the wildcard
- stub_request(:get, %r{^https://.*/api/v4/projects/gitlab-org%2Fproject-templates%2Frails/repository/commits\?page=1&per_page=1})
+ stub_request(:get, %r{^https://.*/api/v4/projects/#{template.uri_encoded_project_path}/repository/commits\?page=1&per_page=1})
.to_return(
status: 200,
body: [{ id: '67812735b83cb42710f22dc98d73d42c8bf4d907' }].to_json,
@@ -27,8 +28,8 @@ RSpec.describe 'gitlab:update_project_templates rake task', :silence_stdout do
end
it 'updates valid project templates' do
- expect { run_rake_task('gitlab:update_project_templates', ['rails']) }
+ expect { run_rake_task('gitlab:update_project_templates', [template.name]) }
.to change { Dir.entries(tmpdir) }
- .by(['rails.tar.gz'])
+ .by(["#{template.name}.tar.gz"])
end
end
diff --git a/spec/tooling/danger/project_helper_spec.rb b/spec/tooling/danger/project_helper_spec.rb
index 6793a4b8de3..f9ad9ed13c2 100644
--- a/spec/tooling/danger/project_helper_spec.rb
+++ b/spec/tooling/danger/project_helper_spec.rb
@@ -33,6 +33,12 @@ RSpec.describe Tooling::Danger::ProjectHelper do
where(:path, :expected_categories) do
'glfm_specification/example_snapshots/prosemirror_json.yml' | [:frontend]
'glfm_specification/input/glfm_anything.yml' | [:frontend, :backend]
+
+ 'doc/api/graphql/reference/index.md' | [:docs, :backend]
+ 'doc/api/graphql/reference/some_other_file.txt' | [:docs, :backend]
+ 'doc/api/openapi/openapi.yaml' | [:docs, :backend]
+ 'doc/api/openapi/any_other_file.yaml' | [:docs, :backend]
+
'usage_data.rb' | [:database, :backend, :product_intelligence]
'doc/foo.md' | [:docs]
'CONTRIBUTING.md' | [:docs]
diff --git a/spec/tooling/lib/tooling/find_codeowners_spec.rb b/spec/tooling/lib/tooling/find_codeowners_spec.rb
index 5f6f83ab2c7..e75793b69c6 100644
--- a/spec/tooling/lib/tooling/find_codeowners_spec.rb
+++ b/spec/tooling/lib/tooling/find_codeowners_spec.rb
@@ -63,7 +63,7 @@ RSpec.describe Tooling::FindCodeowners do
}
},
'[Compliance]': {
- '@gitlab-org/manage/compliance': {
+ '@gitlab-org/govern/compliance': {
entries: %w[
/ee/app/services/audit_events/build_service.rb
],
@@ -115,7 +115,7 @@ RSpec.describe Tooling::FindCodeowners do
it 'retains the array and expands the patterns for the compliance group' do
compliance = subject.load_definitions.dig(
:'[Compliance]',
- :'@gitlab-org/manage/compliance')
+ :'@gitlab-org/govern/compliance')
expect(compliance).to eq(
entries: %w[
diff --git a/spec/tooling/lib/tooling/helm3_client_spec.rb b/spec/tooling/lib/tooling/helm3_client_spec.rb
index 41c51ec5754..52d1b5a1567 100644
--- a/spec/tooling/lib/tooling/helm3_client_spec.rb
+++ b/spec/tooling/lib/tooling/helm3_client_spec.rb
@@ -35,7 +35,7 @@ RSpec.describe Tooling::Helm3Client do
describe '#releases' do
it 'raises an error if the Helm command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --max 256 --offset 0 --output json)])
+ .with([%(helm list --max 256 --offset 0 --output json)])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.releases.to_a }.to raise_error(described_class::CommandFailedError)
@@ -43,7 +43,7 @@ RSpec.describe Tooling::Helm3Client do
it 'calls helm list with default arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --max 256 --offset 0 --output json)])
+ .with([%(helm list --max 256 --offset 0 --output json)])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
subject.releases.to_a
@@ -51,7 +51,7 @@ RSpec.describe Tooling::Helm3Client do
it 'calls helm list with extra arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --max 256 --offset 0 --output json --deployed)])
+ .with([%(helm list --max 256 --offset 0 --output json --deployed)])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
subject.releases(args: ['--deployed']).to_a
@@ -59,7 +59,7 @@ RSpec.describe Tooling::Helm3Client do
it 'returns a list of Release objects' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --max 256 --offset 0 --output json --deployed)])
+ .with([%(helm list --max 256 --offset 0 --output json --deployed)])
.and_return(Gitlab::Popen::Result.new([], raw_helm_list_page2, '', double(success?: true)))
expect(Gitlab::Popen).to receive(:popen_with_detail).ordered
.and_return(Gitlab::Popen::Result.new([], raw_helm_list_empty, '', double(success?: true)))
@@ -80,13 +80,13 @@ RSpec.describe Tooling::Helm3Client do
it 'automatically paginates releases' do
expect(Gitlab::Popen).to receive(:popen_with_detail).ordered
- .with([%(helm list --namespace "#{namespace}" --max 256 --offset 0 --output json)])
+ .with([%(helm list --max 256 --offset 0 --output json)])
.and_return(Gitlab::Popen::Result.new([], raw_helm_list_page1, '', double(success?: true)))
expect(Gitlab::Popen).to receive(:popen_with_detail).ordered
- .with([%(helm list --namespace "#{namespace}" --max 256 --offset 256 --output json)])
+ .with([%(helm list --max 256 --offset 256 --output json)])
.and_return(Gitlab::Popen::Result.new([], raw_helm_list_page2, '', double(success?: true)))
expect(Gitlab::Popen).to receive(:popen_with_detail).ordered
- .with([%(helm list --namespace "#{namespace}" --max 256 --offset 512 --output json)])
+ .with([%(helm list --max 256 --offset 512 --output json)])
.and_return(Gitlab::Popen::Result.new([], raw_helm_list_empty, '', double(success?: true)))
releases = subject.releases.to_a
@@ -98,7 +98,7 @@ RSpec.describe Tooling::Helm3Client do
describe '#delete' do
it 'raises an error if the Helm command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall --namespace "#{namespace}" #{release_name})])
+ .with([%(helm uninstall #{release_name})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.delete(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
@@ -106,7 +106,7 @@ RSpec.describe Tooling::Helm3Client do
it 'calls helm uninstall with default arguments' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall --namespace "#{namespace}" #{release_name})])
+ .with([%(helm uninstall #{release_name})])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
expect(subject.delete(release_name: release_name)).to eq('')
@@ -117,16 +117,16 @@ RSpec.describe Tooling::Helm3Client do
it 'raises an error if the Helm command fails' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall --namespace "#{namespace}" #{release_name.join(' ')})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+ .with([%(helm uninstall #{release_name.join(' ')})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.delete(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
end
it 'calls helm uninstall with multiple release names' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm uninstall --namespace "#{namespace}" #{release_name.join(' ')})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+ .with([%(helm uninstall #{release_name.join(' ')})])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
expect(subject.delete(release_name: release_name)).to eq('')
end
diff --git a/spec/tooling/rspec_flaky/example_spec.rb b/spec/tooling/rspec_flaky/example_spec.rb
index 8ff280fd855..d001ed32444 100644
--- a/spec/tooling/rspec_flaky/example_spec.rb
+++ b/spec/tooling/rspec_flaky/example_spec.rb
@@ -9,7 +9,8 @@ RSpec.describe RspecFlaky::Example do
metadata: {
file_path: 'spec/foo/bar_spec.rb',
line_number: 2,
- full_description: 'hello world'
+ full_description: 'hello world',
+ feature_category: :feature_category
},
execution_result: double(status: 'passed', exception: 'BOOM!'),
attempts: 1
@@ -89,4 +90,10 @@ RSpec.describe RspecFlaky::Example do
expect(subject.exception).to eq(rspec_example.execution_result.exception)
end
end
+
+ describe '#feature_category' do
+ it 'returns the metadata[:feature_category] of the RSpec::Core::Example' do
+ expect(subject.feature_category).to eq(rspec_example.metadata[:feature_category])
+ end
+ end
end
diff --git a/spec/tooling/rspec_flaky/flaky_example_spec.rb b/spec/tooling/rspec_flaky/flaky_example_spec.rb
index 03436ee1cbd..511f3286f56 100644
--- a/spec/tooling/rspec_flaky/flaky_example_spec.rb
+++ b/spec/tooling/rspec_flaky/flaky_example_spec.rb
@@ -9,30 +9,14 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
include ActiveSupport::Testing::TimeHelpers
include StubENV
- let(:flaky_example_attrs) do
+ let(:example_attrs) do
{
example_id: 'spec/foo/bar_spec.rb:2',
file: 'spec/foo/bar_spec.rb',
line: 2,
description: 'hello world',
- first_flaky_at: 1234,
- last_flaky_at: 2345,
- last_flaky_job: 'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/12',
last_attempts_count: 2,
- flaky_reports: 1
- }
- end
-
- let(:example_attrs) do
- {
- uid: 'abc123',
- example_id: flaky_example_attrs[:example_id],
- file: flaky_example_attrs[:file],
- line: flaky_example_attrs[:line],
- description: flaky_example_attrs[:description],
- status: 'passed',
- exception: 'BOOM!',
- attempts: flaky_example_attrs[:last_attempts_count]
+ feature_category: :feature_category
}
end
@@ -48,18 +32,19 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
it 'returns valid attributes' do
attrs = flaky_example.to_h
- expect(attrs[:uid]).to eq(flaky_example_attrs[:uid])
- expect(attrs[:file]).to eq(flaky_example_attrs[:file])
- expect(attrs[:line]).to eq(flaky_example_attrs[:line])
- expect(attrs[:description]).to eq(flaky_example_attrs[:description])
+ expect(attrs[:uid]).to eq(example_attrs[:uid])
+ expect(attrs[:file]).to eq(example_attrs[:file])
+ expect(attrs[:line]).to eq(example_attrs[:line])
+ expect(attrs[:description]).to eq(example_attrs[:description])
+ expect(attrs[:feature_category]).to eq(example_attrs[:feature_category])
expect(attrs[:first_flaky_at]).to eq(expected_first_flaky_at)
expect(attrs[:last_flaky_at]).to eq(expected_last_flaky_at)
- expect(attrs[:last_attempts_count]).to eq(flaky_example_attrs[:last_attempts_count])
+ expect(attrs[:last_attempts_count]).to eq(example_attrs[:last_attempts_count])
expect(attrs[:flaky_reports]).to eq(expected_flaky_reports)
end
end
- context 'when given an Example hash' do
+ context 'when given an Example.to_h' do
it_behaves_like 'a valid FlakyExample instance' do
let(:args) { example_attrs }
let(:expected_first_flaky_at) { Time.now }
@@ -67,18 +52,9 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
let(:expected_flaky_reports) { 0 }
end
end
-
- context 'when given a FlakyExample hash' do
- it_behaves_like 'a valid FlakyExample instance' do
- let(:args) { flaky_example_attrs }
- let(:expected_flaky_reports) { flaky_example_attrs[:flaky_reports] }
- let(:expected_first_flaky_at) { flaky_example_attrs[:first_flaky_at] }
- let(:expected_last_flaky_at) { flaky_example_attrs[:last_flaky_at] }
- end
- end
end
- describe '#update_flakiness!' do
+ describe '#update!' do
shared_examples 'an up-to-date FlakyExample instance' do
let(:flaky_example) { described_class.new(args) }
@@ -86,18 +62,18 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
args[:first_flaky_at] = nil
freeze_time do
- flaky_example.update_flakiness!
+ flaky_example.update!(example_attrs)
expect(flaky_example.to_h[:first_flaky_at]).to eq(Time.now)
end
end
it 'maintains the first_flaky_at if exists' do
- flaky_example.update_flakiness!
+ flaky_example.update!(example_attrs)
expected_first_flaky_at = flaky_example.to_h[:first_flaky_at]
travel_to(Time.now + 42) do
- flaky_example.update_flakiness!
+ flaky_example.update!(example_attrs)
expect(flaky_example.to_h[:first_flaky_at]).to eq(expected_first_flaky_at)
end
end
@@ -105,7 +81,7 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
it 'updates the last_flaky_at' do
travel_to(Time.now + 42) do
the_future = Time.now
- flaky_example.update_flakiness!
+ flaky_example.update!(example_attrs)
expect(flaky_example.to_h[:last_flaky_at]).to eq(the_future)
end
@@ -114,16 +90,15 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
it 'updates the flaky_reports' do
expected_flaky_reports = flaky_example.to_h[:first_flaky_at] ? flaky_example.to_h[:flaky_reports] + 1 : 1
- expect { flaky_example.update_flakiness! }.to change { flaky_example.to_h[:flaky_reports] }.by(1)
+ expect { flaky_example.update!(example_attrs) }.to change { flaky_example.to_h[:flaky_reports] }.by(1)
expect(flaky_example.to_h[:flaky_reports]).to eq(expected_flaky_reports)
end
- context 'when passed a :last_attempts_count' do
- it 'updates the last_attempts_count' do
- flaky_example.update_flakiness!(last_attempts_count: 42)
+ it 'updates the last_attempts_count' do
+ example_attrs[:last_attempts_count] = 42
+ flaky_example.update!(example_attrs)
- expect(flaky_example.to_h[:last_attempts_count]).to eq(42)
- end
+ expect(flaky_example.to_h[:last_attempts_count]).to eq(42)
end
context 'when run on the CI' do
@@ -134,7 +109,7 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
end
it 'updates the last_flaky_job' do
- flaky_example.update_flakiness!
+ flaky_example.update!(example_attrs)
expect(flaky_example.to_h[:last_flaky_job]).to eq(job_url)
end
@@ -146,12 +121,6 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
let(:args) { example_attrs }
end
end
-
- context 'when given a FlakyExample hash' do
- it_behaves_like 'an up-to-date FlakyExample instance' do
- let(:args) { flaky_example_attrs }
- end
- end
end
describe '#to_h', :freeze_time do
@@ -160,7 +129,7 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
it 'returns a valid hash' do
flaky_example = described_class.new(args)
- final_hash = flaky_example_attrs.merge(additional_attrs)
+ final_hash = example_attrs.merge(additional_attrs)
expect(flaky_example.to_h).to eq(final_hash)
end
@@ -175,11 +144,5 @@ RSpec.describe RspecFlaky::FlakyExample, :aggregate_failures do
end
end
end
-
- context 'when given a FlakyExample hash' do
- let(:args) { flaky_example_attrs }
-
- it_behaves_like 'a valid FlakyExample hash'
- end
end
end
diff --git a/spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb b/spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb
index e5f985c9596..9d75c97febe 100644
--- a/spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb
+++ b/spec/tooling/rspec_flaky/flaky_examples_collection_spec.rb
@@ -20,6 +20,7 @@ RSpec.describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures, :freeze
last_flaky_at: Time.now,
last_flaky_job: nil,
flaky_reports: 0,
+ feature_category: nil,
last_attempts_count: nil
},
b: {
@@ -28,6 +29,7 @@ RSpec.describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures, :freeze
last_flaky_at: Time.now,
last_flaky_job: nil,
flaky_reports: 0,
+ feature_category: nil,
last_attempts_count: nil
}
}
@@ -69,6 +71,7 @@ RSpec.describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures, :freeze
last_flaky_at: Time.now,
last_flaky_job: nil,
flaky_reports: 0,
+ feature_category: nil,
last_attempts_count: nil
})
end
diff --git a/spec/tooling/rspec_flaky/listener_spec.rb b/spec/tooling/rspec_flaky/listener_spec.rb
index 62bbe53cac1..0bbd6454969 100644
--- a/spec/tooling/rspec_flaky/listener_spec.rb
+++ b/spec/tooling/rspec_flaky/listener_spec.rb
@@ -128,6 +128,7 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do
first_flaky_at: 1234,
last_attempts_count: 2,
flaky_reports: 2,
+ feature_category: nil,
last_flaky_job: nil
}
end
@@ -154,6 +155,7 @@ RSpec.describe RspecFlaky::Listener, :aggregate_failures do
description: 'hello GitLab',
last_attempts_count: 2,
flaky_reports: 1,
+ feature_category: nil,
last_flaky_job: nil
}
end
diff --git a/spec/tooling/rspec_flaky/report_spec.rb b/spec/tooling/rspec_flaky/report_spec.rb
index ffd0cd987aa..e7365c1e150 100644
--- a/spec/tooling/rspec_flaky/report_spec.rb
+++ b/spec/tooling/rspec_flaky/report_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe RspecFlaky::Report, :aggregate_failures, :freeze_time do
last_flaky_at: 4321,
last_attempts_count: 3,
flaky_reports: 1,
+ feature_category: 'feature_category',
last_flaky_job: nil
}
}
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index a7fd040837f..d7c9ef7e0d5 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe JobArtifactUploader do
describe '#cdn_enabled_url' do
it 'returns URL and false' do
- result = uploader.cdn_enabled_url(nil, '127.0.0.1')
+ result = uploader.cdn_enabled_url('127.0.0.1')
expect(result.used_cdn).to be false
end
diff --git a/spec/uploaders/object_storage/cdn/google_cdn_spec.rb b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
index 8e209dabddc..96755b7292b 100644
--- a/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
+++ b/spec/uploaders/object_storage/cdn/google_cdn_spec.rb
@@ -92,26 +92,52 @@ RSpec.describe ObjectStorage::CDN::GoogleCDN,
end
end
- describe '#signed_url' do
+ describe '#signed_url', :freeze_time do
let(:path) { '/path/to/file.txt' }
+ let(:expiration) { (Time.current + 10.minutes).utc.to_i }
+ let(:cdn_query_params) { "Expires=#{expiration}&KeyName=#{key_name}" }
- it 'returns a valid signed URL' do
- url = subject.signed_url(path)
-
+ def verify_signature(url, unsigned_url)
expect(url).to start_with("#{options[:url]}#{path}")
uri = Addressable::URI.parse(url)
- parsed_query = Rack::Utils.parse_nested_query(uri.query)
- signature = parsed_query.delete('Signature')
+ query = uri.query_values
+ signature = query['Signature']
- signed_url = "#{options[:url]}#{path}?Expires=#{parsed_query['Expires']}&KeyName=#{key_name}"
- computed_signature = OpenSSL::HMAC.digest('SHA1', key, signed_url)
+ computed_signature = OpenSSL::HMAC.digest('SHA1', key, unsigned_url)
aggregate_failures do
- expect(parsed_query['Expires'].to_i).to be > 0
- expect(parsed_query['KeyName']).to eq(key_name)
+ expect(query['Expires'].to_i).to be > 0
+ expect(query['KeyName']).to eq(key_name)
expect(signature).to eq(Base64.urlsafe_encode64(computed_signature))
end
end
+
+ context 'with default query parameters' do
+ let(:url) { subject.signed_url(path) }
+ let(:unsigned_url) { "#{options[:url]}#{path}?#{cdn_query_params}" }
+
+ it 'returns a valid signed URL' do
+ verify_signature(url, unsigned_url)
+ end
+ end
+
+ context 'with nil query parameters' do
+ let(:url) { subject.signed_url(path, params: nil) }
+ let(:unsigned_url) { "#{options[:url]}#{path}?#{cdn_query_params}" }
+
+ it 'returns a valid signed URL' do
+ verify_signature(url, unsigned_url)
+ end
+ end
+
+ context 'with extra query parameters' do
+ let(:url) { subject.signed_url(path, params: { 'response-content-type' => 'text/plain' }) }
+ let(:unsigned_url) { "#{options[:url]}#{path}?response-content-type=text%2Fplain&#{cdn_query_params}" }
+
+ it 'returns a valid signed URL' do
+ verify_signature(url, unsigned_url)
+ end
+ end
end
end
diff --git a/spec/uploaders/object_storage/cdn_spec.rb b/spec/uploaders/object_storage/cdn_spec.rb
index f99450b274f..2a447921a19 100644
--- a/spec/uploaders/object_storage/cdn_spec.rb
+++ b/spec/uploaders/object_storage/cdn_spec.rb
@@ -44,30 +44,13 @@ RSpec.describe ObjectStorage::CDN do
end
describe '#cdn_enabled_url' do
- context 'with ci_job_artifacts_cdn feature flag disabled' do
- before do
- stub_feature_flags(ci_job_artifacts_cdn: false)
- end
-
- it 'calls #url' do
- expect(subject).to receive(:url).and_call_original
- expect(subject).not_to receive(:cdn_signed_url)
-
- result = subject.cdn_enabled_url(project, public_ip)
-
- expect(result.used_cdn).to be false
- end
- end
+ it 'calls #cdn_signed_url' do
+ expect(subject).not_to receive(:url)
+ expect(subject).to receive(:cdn_signed_url).and_call_original
- context 'with ci_job_artifacts_cdn feature flag enabled' do
- it 'calls #cdn_signed_url' do
- expect(subject).not_to receive(:url)
- expect(subject).to receive(:cdn_signed_url).and_call_original
+ result = subject.cdn_enabled_url(public_ip)
- result = subject.cdn_enabled_url(project, public_ip)
-
- expect(result.used_cdn).to be true
- end
+ expect(result.used_cdn).to be true
end
end
diff --git a/spec/validators/branch_filter_validator_spec.rb b/spec/validators/web_hooks/wildcard_branch_filter_validator_spec.rb
index 2d869fa674d..7f29e1df007 100644
--- a/spec/validators/branch_filter_validator_spec.rb
+++ b/spec/validators/web_hooks/wildcard_branch_filter_validator_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BranchFilterValidator do
+RSpec.describe WebHooks::WildcardBranchFilterValidator do
let(:validator) { described_class.new(attributes: [:push_events_branch_filter]) }
let(:hook) { build(:project_hook) }
diff --git a/spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb b/spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb
new file mode 100644
index 00000000000..7cfc2db5a41
--- /dev/null
+++ b/spec/views/admin/application_settings/_jira_connect.html.haml_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'admin/application_settings/_jira_connect.html.haml' do
+ let_it_be(:admin) { create(:admin) }
+ let(:application_setting) { build(:application_setting) }
+
+ before do
+ assign(:application_setting, application_setting)
+ allow(view).to receive(:expanded).and_return(true)
+ end
+
+ it 'renders the application ID field' do
+ render
+ expect(rendered).to have_field('Jira Connect Application ID', type: 'text')
+ end
+
+ it 'renders the asymmetric jwt cdn url field' do
+ render
+ expect(rendered).to have_field('Jira Connect Proxy URL', type: 'text')
+ end
+end
diff --git a/spec/views/admin/application_settings/general.html.haml_spec.rb b/spec/views/admin/application_settings/general.html.haml_spec.rb
index c7d156cde39..a8c7bec36e3 100644
--- a/spec/views/admin/application_settings/general.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/general.html.haml_spec.rb
@@ -69,19 +69,19 @@ RSpec.describe 'admin/application_settings/general.html.haml' do
end
end
- describe 'jira connect application key' do
- it 'shows the jira connect application key section' do
+ describe 'jira connect settings' do
+ it 'shows the jira connect settings section' do
render
expect(rendered).to have_css('#js-jira_connect-settings')
end
- context 'when the jira_connect_oauth feature flag is disabled' do
+ context 'when the jira_connect_oauth_self_managed_setting feature flag is disabled' do
before do
- stub_feature_flags(jira_connect_oauth: false)
+ stub_feature_flags(jira_connect_oauth_self_managed_setting: false)
end
- it 'does not show the jira connect application key section' do
+ it 'does not show the jira connect settings section' do
render
expect(rendered).not_to have_css('#js-jira_connect-settings')
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 9f1ff960444..6e06af92232 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe 'admin/dashboard/index.html.haml' do
end
it 'renders the version check badge' do
- expect(rendered).to have_selector('.js-gitlab-version-check')
+ expect(rendered).to have_selector('.js-gitlab-version-check-badge')
end
end
diff --git a/spec/views/devise/confirmations/almost_there.html.haml_spec.rb b/spec/views/devise/confirmations/almost_there.html.haml_spec.rb
new file mode 100644
index 00000000000..c091efe9295
--- /dev/null
+++ b/spec/views/devise/confirmations/almost_there.html.haml_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'devise/confirmations/almost_there' do
+ describe 'confirmations text' do
+ subject { render(template: 'devise/confirmations/almost_there') }
+
+ before do
+ allow(view).to receive(:params).and_return(email: email)
+ end
+
+ context 'when correct email' do
+ let(:email) { 'こんにちは@test' }
+
+ specify do
+ subject
+
+ expect(rendered).to have_content(
+ "Please check your email (#{email}) to confirm your account"
+ )
+ end
+ end
+
+ context 'when random text' do
+ let(:email) { 'random text' }
+
+ specify do
+ subject
+
+ expect(rendered).to have_content(
+ 'Please check your email to confirm your account'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/views/events/event/_common.html.haml_spec.rb b/spec/views/events/event/_common.html.haml_spec.rb
index ad8e5c2ef77..2160245fb63 100644
--- a/spec/views/events/event/_common.html.haml_spec.rb
+++ b/spec/views/events/event/_common.html.haml_spec.rb
@@ -18,9 +18,22 @@ RSpec.describe 'events/event/_common.html.haml' do
create(:event, :created, project: project, target: work_item, target_type: 'WorkItem', author: user)
end
- it 'renders the correct url' do
+ context 'when use_iid_in_work_items_path feature flag is disabled' do
+ before do
+ stub_feature_flags(use_iid_in_work_items_path: false)
+ render partial: 'events/event/common', locals: { event: event.present }
+ end
+
+ it 'renders the correct url' do
+ expect(rendered).to have_link(
+ work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.id}"
+ )
+ end
+ end
+
+ it 'renders the correct url with iid' do
expect(rendered).to have_link(
- work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.id}"
+ work_item.reference_link_text, href: "/#{project.full_path}/-/work_items/#{work_item.iid}?iid_path=true"
)
end
diff --git a/spec/views/groups/observability.html.haml_spec.rb b/spec/views/groups/observability.html.haml_spec.rb
deleted file mode 100644
index db280d5a2ba..00000000000
--- a/spec/views/groups/observability.html.haml_spec.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'groups/observability/index' do
- let_it_be(:iframe_url) { "foo.test" }
-
- before do
- assign(:observability_iframe_src, iframe_url)
- end
-
- it 'renders as expected' do
- render
- page = Capybara.string(rendered)
- iframe = page.find('iframe#observability-ui-iframe')
- expect(iframe['src']).to eq(iframe_url)
- end
-end
diff --git a/spec/views/groups/observability/observability.html.haml_spec.rb b/spec/views/groups/observability/observability.html.haml_spec.rb
new file mode 100644
index 00000000000..0561737cb39
--- /dev/null
+++ b/spec/views/groups/observability/observability.html.haml_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'groups/observability/observability.html.haml' do
+ let(:iframe_url) { "foo.test" }
+
+ before do
+ allow(view).to receive(:observability_iframe_src).and_return(iframe_url)
+ end
+
+ it 'renders as expected' do
+ render
+ page = Capybara.string(rendered)
+ div = page.find('#js-observability-app')
+ expect(div['data-observability-iframe-src']).to eq(iframe_url)
+ end
+end
diff --git a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
index 2f423c72ca6..4f1dcf54216 100644
--- a/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
+++ b/spec/views/layouts/header/_gitlab_version.html.haml_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'layouts/header/_gitlab_version' do
end
it 'renders the version check badge' do
- expect(rendered).to have_selector('.js-gitlab-version-check')
+ expect(rendered).to have_selector('.js-gitlab-version-check-badge')
end
it 'renders the container as a link' do
@@ -18,5 +18,20 @@ RSpec.describe 'layouts/header/_gitlab_version' do
'a[data-testid="gitlab-version-container"][href="/help/update/index"]'
)
end
+
+ it 'renders the container with correct data-tracking attributes' do
+ expect(rendered).to have_selector(
+ 'a[data-testid="gitlab-version-container"][data-track-action="click_link"]'
+ )
+
+ expect(rendered).to have_selector(
+ 'a[data-testid="gitlab-version-container"][data-track-label="version_help_dropdown"]'
+ )
+
+ expect(rendered).to have_selector(
+ 'a[data-testid="gitlab-version-container"]' \
+ "[data-track-property=\"#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}\"]"
+ )
+ end
end
end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index e7d9a8a4708..d0d220fed66 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -472,24 +472,6 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
end
-
- describe 'Product Analytics' do
- it 'has a link to the product analytics page' do
- render
-
- expect(rendered).to have_link('Product Analytics', href: project_product_analytics_path(project))
- end
-
- describe 'when feature flag :product_analytics is disabled' do
- it 'does not have a link to the feature flags page' do
- stub_feature_flags(product_analytics: false)
-
- render
-
- expect(rendered).not_to have_link('Product Analytics')
- end
- end
- end
end
describe 'Infrastructure' do
diff --git a/spec/views/projects/artifacts/_artifact.html.haml_spec.rb b/spec/views/projects/artifacts/_artifact.html.haml_spec.rb
deleted file mode 100644
index 5d930d6b0f2..00000000000
--- a/spec/views/projects/artifacts/_artifact.html.haml_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe "projects/artifacts/_artifact.html.haml" do
- let(:project) { create(:project) }
-
- describe 'delete button' do
- before do
- create(:ci_build, :artifacts, project: project)
-
- allow(view).to receive(:current_user).and_return(user)
- assign(:project, project)
- end
-
- context 'with admin' do
- let(:user) { build(:admin) }
-
- context 'when admin mode is enabled', :enable_admin_mode do
- it 'has a delete button' do
- render_partial
-
- expect(rendered).to have_link('Delete artifacts', href: project_artifact_path(project, project.job_artifacts.first))
- end
- end
-
- context 'when admin mode is disabled' do
- it 'has no delete button' do
- project.add_reporter(user)
- render_partial
-
- expect(rendered).not_to have_link('Delete artifacts')
- end
- end
- end
-
- context 'with owner' do
- let(:user) { create(:user) }
- let(:project) { build(:project, namespace: user.namespace) }
-
- it 'has a delete button' do
- render_partial
-
- expect(rendered).to have_link('Delete artifacts', href: project_artifact_path(project, project.job_artifacts.first))
- end
- end
-
- context 'with master' do
- let(:user) { create(:user) }
-
- it 'has a delete button' do
- allow_any_instance_of(ProjectTeam).to receive(:max_member_access).and_return(Gitlab::Access::MAINTAINER)
- render_partial
-
- expect(rendered).to have_link('Delete artifacts', href: project_artifact_path(project, project.job_artifacts.first))
- end
- end
-
- context 'with developer' do
- let(:user) { build(:user) }
-
- it 'has no delete button' do
- project.add_developer(user)
- render_partial
-
- expect(rendered).not_to have_link('Delete artifacts')
- end
- end
-
- context 'with reporter' do
- let(:user) { build(:user) }
-
- it 'has no delete button' do
- project.add_reporter(user)
- render_partial
-
- expect(rendered).not_to have_link('Delete artifacts')
- end
- end
- end
-
- def render_partial
- render partial: 'projects/artifacts/artifact', collection: project.job_artifacts, as: :artifact
- end
-end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index c503e085d02..4335a0901ae 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'projects/commit/_commit_box.html.haml' do
it 'shows the commit SHA' do
render
- expect(rendered).to have_text("#{Commit.truncate_sha(project.commit.sha)}")
+ expect(rendered).to have_text(Commit.truncate_sha(project.commit.sha).to_s)
end
context 'when there is a pipeline present' do
diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb
index 59182f6e757..4d5c987ce37 100644
--- a/spec/views/projects/commit/show.html.haml_spec.rb
+++ b/spec/views/projects/commit/show.html.haml_spec.rb
@@ -67,5 +67,13 @@ RSpec.describe 'projects/commit/show.html.haml' do
expect(rendered).to have_content("This commit is part of merge request")
expect(rendered).to have_link(merge_request.to_reference, href: merge_request_url)
end
+
+ context 'when merge request is nil' do
+ let(:merge_request) { nil }
+
+ it 'renders the page' do
+ expect { rendered }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb b/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb
index 416f4253e1b..99339e956cc 100644
--- a/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'projects/merge_requests/_close_reopen_draft_report_toggle.html.h
render
- expect(rendered).to have_css('li', class: 'js-sidebar-subscriptions-entry-point')
+ expect(rendered).to have_css('li', class: 'js-sidebar-subscriptions-widget-root')
end
end
@@ -27,7 +27,7 @@ RSpec.describe 'projects/merge_requests/_close_reopen_draft_report_toggle.html.h
it 'is not present' do
render
- expect(rendered).not_to have_css('li', class: 'js-sidebar-subscriptions-entry-point')
+ expect(rendered).not_to have_css('li', class: 'js-sidebar-subscriptions-widget-root')
end
end
end
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index f0273c1716f..90ee6638142 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -39,16 +39,4 @@ RSpec.describe 'projects/merge_requests/_commits.html.haml', :sidekiq_might_not_
expect(rendered).to have_css('.gpg-status-box')
end
-
- context 'when there are hidden commits' do
- before do
- assign(:hidden_commit_count, 1)
- end
-
- it 'shows notice about omitted commits' do
- render
-
- expect(rendered).to match(/1 additional commit has been omitted to prevent performance issues/)
- end
- end
end
diff --git a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
index 038a94fe7c3..feb82e6a2b2 100644
--- a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
@@ -35,17 +35,4 @@ RSpec.describe 'projects/merge_requests/creations/_new_submit.html.haml' do
expect(rendered).not_to have_text('Builds')
end
end
-
- context 'when there are hidden commits' do
- before do
- assign(:pipelines, Ci::Pipeline.none)
- assign(:hidden_commit_count, 2)
- end
-
- it 'shows notice about omitted commits' do
- render
-
- expect(rendered).to match(/2 additional commits have been omitted to prevent performance issues/)
- end
- end
end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 215d404e395..75956160c0a 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
- expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+ expect(rendered).to have_selector('.js-milestone-dropdown-root')
expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
end
end
@@ -57,7 +57,7 @@ RSpec.describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
- expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+ expect(rendered).to have_selector('.js-milestone-dropdown-root')
expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
end
end
diff --git a/spec/views/search/show.html.haml_spec.rb b/spec/views/search/show.html.haml_spec.rb
index 565dadd64fe..5f9c6c65a08 100644
--- a/spec/views/search/show.html.haml_spec.rb
+++ b/spec/views/search/show.html.haml_spec.rb
@@ -11,10 +11,10 @@ RSpec.describe 'search/show' do
stub_template "search/_results.html.haml" => 'Results Partial'
end
- context 'feature flag enabled' do
+ context 'search_page_vertical_nav feature flag enabled' do
before do
- allow(self).to receive(:current_user).and_return(user)
- @search_term = search_term
+ allow(view).to receive(:current_user) { user }
+ assign(:search_term, search_term)
render
end
@@ -29,11 +29,11 @@ RSpec.describe 'search/show' do
end
end
- context 'feature flag disabled' do
+ context 'search_page_vertical_nav feature flag disabled' do
before do
stub_feature_flags(search_page_vertical_nav: false)
- @search_term = search_term
+ assign(:search_term, search_term)
render
end
diff --git a/spec/views/shared/access_tokens/_table.html.haml_spec.rb b/spec/views/shared/access_tokens/_table.html.haml_spec.rb
deleted file mode 100644
index 74de9e12d04..00000000000
--- a/spec/views/shared/access_tokens/_table.html.haml_spec.rb
+++ /dev/null
@@ -1,151 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'shared/access_tokens/_table.html.haml' do
- let(:type) { 'token' }
- let(:type_plural) { 'tokens' }
- let(:empty_message) { nil }
- let(:impersonation) { false }
-
- let_it_be(:user) { create(:user) }
- let_it_be(:tokens) { [create(:personal_access_token, user: user)] }
- let_it_be(:resource) { false }
-
- before do
- if resource
- resource.add_maintainer(user)
- end
-
- # Forcibly removing scopes from one token as it's not possible to do with the current modal on creation
- # But the check exists in the template (it may be there for legacy reasons), so we should test the outcome
- if tokens.size > 1
- tokens[1].scopes = []
- end
-
- locals = {
- type: type,
- type_plural: type_plural,
- active_tokens: tokens,
- resource: resource,
- impersonation: impersonation,
- revoke_route_helper: ->(token) { 'path/' }
- }
-
- if empty_message
- locals[:no_active_tokens_message] = empty_message
- end
-
- render partial: 'shared/access_tokens/table', locals: locals
- end
-
- context 'if personal' do
- it 'does not show non-personal content', :aggregate_failures do
- expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
- expect(rendered).not_to have_selector 'th', text: 'Role'
- end
- end
-
- context 'if impersonation' do
- let(:impersonation) { true }
-
- it 'shows the impersonation content', :aggregate_failures do
- expect(rendered).to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
-
- expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.'
- expect(rendered).not_to have_selector 'th', text: 'Role'
- end
- end
-
- context 'if resource is project' do
- let_it_be(:resource) { create(:project) }
-
- it 'shows the project content', :aggregate_failures do
- expect(rendered).to have_selector 'th', text: 'Role'
- expect(rendered).to have_selector 'td', text: 'Maintainer'
-
- expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.'
- expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
- end
- end
-
- context 'if resource is group' do
- let_it_be(:resource) { create(:group) }
-
- it 'shows the group content', :aggregate_failures do
- expect(rendered).to have_selector 'th', text: 'Role'
- expect(rendered).to have_selector 'td', text: 'Maintainer'
-
- expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.'
- expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.'
- end
- end
-
- context 'without tokens' do
- let_it_be(:tokens) { [] }
-
- it 'has the correct content', :aggregate_failures do
- expect(rendered).to have_content 'Active tokens (0)'
- expect(rendered).to have_content 'This user has no active tokens.'
- end
-
- context 'with a custom empty text' do
- let(:empty_message) { 'Custom empty message' }
-
- it 'shows the custom empty text' do
- expect(rendered).to have_content empty_message
- end
- end
- end
-
- context 'with tokens' do
- let_it_be(:tokens) do
- [
- create(:personal_access_token, user: user, name: 'Access token', last_used_at: 4.days.from_now, expires_at: nil, scopes: [:read_api, :read_user]),
- create(:personal_access_token, user: user, expires_at: 1.day.from_now, scopes: [:read_api, :read_user])
- ]
- end
-
- let_it_be(:expired_token) { build(:personal_access_token, name: "Expired token", expires_at: 2.days.ago).tap { |t| t.save!(validate: false) } }
-
- it 'has the correct content', :aggregate_failures do
- # Heading content
- expect(rendered).to have_content 'Active tokens (2)'
-
- # Table headers
- expect(rendered).to have_selector 'th', text: 'Token name'
- expect(rendered).to have_selector 'th', text: 'Scopes'
- expect(rendered).to have_selector 'th', text: 'Created'
- expect(rendered).to have_selector 'th', text: 'Last Used'
- expect(rendered).to have_selector 'th', text: 'Expires'
-
- # Table contents
- expect(rendered).to have_content 'Access token'
- expect(rendered).not_to have_content 'Expired token'
- expect(rendered).to have_content 'read_api, read_user'
- expect(rendered).to have_content 'no scopes selected'
- expect(rendered).to have_content Time.now.to_date.to_s(:medium)
- expect(rendered).to have_content l(4.days.from_now, format: "%b %d, %Y")
-
- # Revoke buttons
- expect(rendered).to have_link 'Revoke', href: 'path/', class: 'btn-danger-secondary', count: 1
- expect(rendered).to have_link 'Revoke', href: 'path/', count: 2
- end
-
- context 'without the last used time' do
- let_it_be(:tokens) { [create(:personal_access_token, user: user, expires_at: 5.days.ago)] }
-
- it 'shows the last used empty text' do
- expect(rendered).to have_content 'Never'
- end
- end
-
- context 'without expired at' do
- let_it_be(:tokens) { [create(:personal_access_token, user: user, expires_at: nil, last_used_at: 1.day.ago)] }
-
- it 'shows the expired at empty text' do
- expect(rendered).to have_content 'Never'
- end
- end
- end
-end
diff --git a/spec/views/shared/deploy_tokens/_form.html.haml_spec.rb b/spec/views/shared/deploy_tokens/_form.html.haml_spec.rb
deleted file mode 100644
index 74ad0ccb77a..00000000000
--- a/spec/views/shared/deploy_tokens/_form.html.haml_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'shared/deploy_tokens/_form.html.haml' do
- using RSpec::Parameterized::TableSyntax
-
- let_it_be(:user) { create(:user) }
- let_it_be(:token) { build(:deploy_token) }
-
- RSpec.shared_examples "display deploy token settings" do |role, shows_package_registry_permissions|
- before do
- subject.add_member(user, role)
- allow(view).to receive(:current_user).and_return(user)
- stub_config(packages: { enabled: packages_enabled })
- end
-
- it "correctly renders the form" do
- render 'shared/deploy_tokens/form', token: token, group_or_project: subject
-
- if shows_package_registry_permissions
- expect(rendered).to have_content('Allows read-only access to the package registry.')
- else
- expect(rendered).not_to have_content('Allows read-only access to the package registry.')
- end
- end
- end
-
- context "when the subject is a project" do
- let_it_be(:subject, refind: true) { create(:project, :private) }
-
- where(:packages_enabled, :feature_enabled, :role, :shows_package_registry_permissions) do
- true | true | :maintainer | true
- false | true | :maintainer | false
- true | false | :maintainer | false
- false | false | :maintainer | false
- end
-
- with_them do
- before do
- subject.update!(packages_enabled: feature_enabled)
- end
-
- it_behaves_like 'display deploy token settings', params[:role], params[:shows_package_registry_permissions]
- end
- end
-
- context "when the subject is a group" do
- let_it_be(:subject, refind: true) { create(:group, :private) }
-
- where(:packages_enabled, :role, :shows_package_registry_permissions) do
- true | :owner | true
- false | :owner | false
- true | :maintainer | true
- false | :maintainer | false
- end
-
- with_them do
- it_behaves_like 'display deploy token settings', params[:role], params[:shows_package_registry_permissions]
- end
- end
-end
diff --git a/spec/views/shared/issuable/_sidebar.html.haml_spec.rb b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
index 43a723dbb2c..31f79c25073 100644
--- a/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
it 'is expected not to be shown' do
create(:contact, group: group)
- expect(rendered).not_to have_css('#js-issue-crm-contacts')
+ expect(rendered).not_to have_css('.js-sidebar-crm-contacts-root')
end
end
@@ -51,7 +51,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
it 'is expected not to be shown' do
group.add_developer(user)
- expect(rendered).not_to have_css('#js-issue-crm-contacts')
+ expect(rendered).not_to have_css('.js-sidebar-crm-contacts-root')
end
end
@@ -60,7 +60,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
create(:contact, group: group)
group.add_developer(user)
- expect(rendered).to have_css('#js-issue-crm-contacts')
+ expect(rendered).to have_css('.js-sidebar-crm-contacts-root')
end
end
end
diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb
index 0fcdbccc304..e3f0ee65205 100644
--- a/spec/workers/bulk_imports/entity_worker_spec.rb
+++ b/spec/workers/bulk_imports/entity_worker_spec.rb
@@ -39,8 +39,11 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => nil,
'message' => 'Stage starting',
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -71,7 +74,10 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => nil,
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -82,9 +88,15 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => nil,
- 'message' => 'Error!',
- 'importer' => 'gitlab_migration'
+ 'message' => 'Entity failed',
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'StandardError',
+ 'exception.message' => 'Error!',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end
@@ -95,6 +107,9 @@ RSpec.describe BulkImports::EntityWorker do
exception,
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
+ source_version: entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration'
)
@@ -112,8 +127,11 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => 0,
'message' => 'Stage running',
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -142,7 +160,10 @@ RSpec.describe BulkImports::EntityWorker do
hash_including(
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
'current_stage' => 0,
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
index 597eed3a9b9..7eb8150fb2e 100644
--- a/spec/workers/bulk_imports/export_request_worker_spec.rb
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
end
shared_examples 'requests relations export for api resource' do
- include_examples 'an idempotent worker' do
+ it_behaves_like 'an idempotent worker' do
it 'requests relations export & schedules entity worker' do
expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
expect(client).to receive(:post).with(expected).twice
@@ -44,18 +44,22 @@ RSpec.describe BulkImports::ExportRequestWorker do
it 'logs retry request and reenqueues' do
allow(exception).to receive(:retriable?).twice.and_return(true)
- expect(Gitlab::Import::Logger).to receive(:error).with(
- hash_including(
- 'bulk_import_entity_id' => entity.id,
- 'pipeline_class' => 'ExportRequestWorker',
- 'exception_class' => 'BulkImports::NetworkError',
- 'exception_message' => 'Export error',
- 'bulk_import_id' => bulk_import.id,
- 'bulk_import_entity_type' => entity.source_type,
- 'importer' => 'gitlab_migration',
- 'message' => 'Retrying export request'
- )
- ).twice
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::NetworkError',
+ 'exception.message' => 'Export error',
+ 'message' => 'Retrying export request',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
+ )
+ ).twice
+ end
expect(described_class).to receive(:perform_in).twice.with(2.seconds, entity.id)
@@ -65,18 +69,24 @@ RSpec.describe BulkImports::ExportRequestWorker do
context 'when error is not retriable' do
it 'logs export failure and marks entity as failed' do
- expect(Gitlab::Import::Logger).to receive(:error).with(
- hash_including(
- 'bulk_import_entity_id' => entity.id,
- 'pipeline_class' => 'ExportRequestWorker',
- 'exception_class' => 'BulkImports::NetworkError',
- 'exception_message' => 'Export error',
- 'correlation_id_value' => anything,
- 'bulk_import_id' => bulk_import.id,
- 'bulk_import_entity_type' => entity.source_type,
- 'importer' => 'gitlab_migration'
- )
- ).twice
+ allow(exception).to receive(:retriable?).twice.and_return(false)
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::NetworkError',
+ 'exception.message' => 'Export error',
+ 'message' => "Request to export #{entity.source_type} failed",
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
+ )
+ ).twice
+ end
perform_multiple(job_args)
@@ -119,25 +129,30 @@ RSpec.describe BulkImports::ExportRequestWorker do
let(:entity_source_id) { 'invalid' }
it 'logs the error & requests relations export using full path url' do
+ allow(BulkImports::EntityWorker).to receive(:perform_async)
+
expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
expect(client).to receive(:post).with(full_path_url).twice
end
entity.update!(source_xid: nil)
- expect(Gitlab::Import::Logger).to receive(:error).with(
- a_hash_including(
- 'message' => 'Failed to fetch source entity id',
- 'bulk_import_entity_id' => entity.id,
- 'pipeline_class' => 'ExportRequestWorker',
- 'exception_class' => 'NoMethodError',
- 'exception_message' => "undefined method `model_id' for nil:NilClass",
- 'correlation_id_value' => anything,
- 'bulk_import_id' => bulk_import.id,
- 'bulk_import_entity_type' => entity.source_type,
- 'importer' => 'gitlab_migration'
- )
- ).twice
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(
+ a_hash_including(
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_id' => entity.bulk_import_id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'NoMethodError',
+ 'exception.message' => "undefined method `model_id' for nil:NilClass",
+ 'message' => 'Failed to fetch source entity id',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
+ )
+ ).twice
+ end
perform_multiple(job_args)
@@ -153,7 +168,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
let(:expected) { "/groups/#{entity.source_xid}/export_relations" }
let(:full_path_url) { '/groups/foo%2Fbar/export_relations' }
- include_examples 'requests relations export for api resource'
+ it_behaves_like 'requests relations export for api resource'
end
context 'when entity is project' do
@@ -161,7 +176,7 @@ RSpec.describe BulkImports::ExportRequestWorker do
let(:expected) { "/projects/#{entity.source_xid}/export_relations" }
let(:full_path_url) { '/projects/foo%2Fbar/export_relations' }
- include_examples 'requests relations export for api resource'
+ it_behaves_like 'requests relations export for api resource'
end
end
end
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index ee65775f170..23fbc5688ec 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -37,9 +37,10 @@ RSpec.describe BulkImports::PipelineWorker do
.with(
hash_including(
'pipeline_name' => 'FakePipeline',
- 'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_id' => entity.id,
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path
)
)
end
@@ -87,8 +88,10 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_tracker_id' => pipeline_tracker.id,
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'message' => 'Unstarted pipeline not found',
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
+ 'message' => 'Unstarted pipeline not found'
)
)
end
@@ -126,7 +129,13 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_name' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'message' => 'Error!',
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'class' => 'BulkImports::PipelineWorker',
+ 'exception.backtrace' => anything,
+ 'exception.message' => 'Error!',
+ 'message' => 'Pipeline failed',
+ 'source_version' => entity.bulk_import.source_version_info.to_s,
'importer' => 'gitlab_migration'
)
)
@@ -137,9 +146,12 @@ RSpec.describe BulkImports::PipelineWorker do
.with(
instance_of(StandardError),
bulk_import_entity_id: entity.id,
- bulk_import_id: entity.bulk_import_id,
+ bulk_import_id: entity.bulk_import.id,
+ bulk_import_entity_type: entity.source_type,
+ source_full_path: entity.source_full_path,
pipeline_name: pipeline_tracker.pipeline_name,
- importer: 'gitlab_migration'
+ importer: 'gitlab_migration',
+ source_version: entity.bulk_import.source_version_info.to_s
)
expect(BulkImports::EntityWorker)
@@ -188,8 +200,9 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_name' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'message' => 'Skipping pipeline due to failed entity',
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'message' => 'Skipping pipeline due to failed entity'
)
)
end
@@ -237,7 +250,8 @@ RSpec.describe BulkImports::PipelineWorker do
'pipeline_name' => 'FakePipeline',
'bulk_import_entity_id' => entity.id,
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path
)
)
end
@@ -361,9 +375,16 @@ RSpec.describe BulkImports::PipelineWorker do
hash_including(
'pipeline_name' => 'NdjsonPipeline',
'bulk_import_entity_id' => entity.id,
- 'message' => 'Pipeline timeout',
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'class' => 'BulkImports::PipelineWorker',
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::Pipeline::ExpiredError',
+ 'exception.message' => 'Pipeline timeout',
+ 'importer' => 'gitlab_migration',
+ 'message' => 'Pipeline failed',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end
@@ -390,9 +411,14 @@ RSpec.describe BulkImports::PipelineWorker do
hash_including(
'pipeline_name' => 'NdjsonPipeline',
'bulk_import_entity_id' => entity.id,
- 'message' => 'Export from source instance failed: Error!',
'bulk_import_id' => entity.bulk_import_id,
- 'importer' => 'gitlab_migration'
+ 'bulk_import_entity_type' => entity.source_type,
+ 'source_full_path' => entity.source_full_path,
+ 'exception.backtrace' => anything,
+ 'exception.class' => 'BulkImports::Pipeline::FailedError',
+ 'exception.message' => 'Export from source instance failed: Error!',
+ 'importer' => 'gitlab_migration',
+ 'source_version' => entity.bulk_import.source_version_info.to_s
)
)
end
diff --git a/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb b/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb
index e18539cc6e3..0d4b8243050 100644
--- a/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb
+++ b/spec/workers/ci/job_artifacts/track_artifact_report_worker_spec.rb
@@ -8,7 +8,10 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportWorker do
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
- let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project, user: user) }
+ let_it_be(:pipeline) do
+ create(:ci_pipeline, :with_test_reports, :with_coverage_reports,
+ project: project, user: user)
+ end
subject(:perform) { described_class.new.perform(pipeline_id) }
@@ -25,17 +28,29 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportWorker do
it_behaves_like 'an idempotent worker' do
let(:job_args) { pipeline_id }
- let(:test_event_name) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_1) { 'i_testing_test_report_uploaded' }
+ let(:test_event_name_2) { 'i_testing_coverage_report_uploaded' }
let(:start_time) { 1.week.ago }
let(:end_time) { 1.week.from_now }
subject(:idempotent_perform) { perform_multiple(pipeline_id, exec_times: 2) }
- it 'does not try to increment again' do
+ it 'does not try to increment again for the test event' do
+ idempotent_perform
+
+ unique_pipeline_pass = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
+ event_names: test_event_name_1,
+ start_date: start_time,
+ end_date: end_time
+ )
+ expect(unique_pipeline_pass).to eq(1)
+ end
+
+ it 'does not try to increment again for the coverage event' do
idempotent_perform
unique_pipeline_pass = Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
- event_names: test_event_name,
+ event_names: test_event_name_2,
start_date: start_time,
end_date: end_time
)
diff --git a/spec/workers/cluster_configure_istio_worker_spec.rb b/spec/workers/cluster_configure_istio_worker_spec.rb
deleted file mode 100644
index 5d949fde973..00000000000
--- a/spec/workers/cluster_configure_istio_worker_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ClusterConfigureIstioWorker do
- describe '#perform' do
- shared_examples 'configure istio service' do
- it 'configures istio' do
- expect_any_instance_of(Clusters::Kubernetes::ConfigureIstioIngressService).to receive(:execute)
-
- described_class.new.perform(cluster.id)
- end
- end
-
- context 'when provider type is gcp' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-
- it_behaves_like 'configure istio service'
- end
-
- context 'when provider type is aws' do
- let(:cluster) { create(:cluster, :project, :provided_by_aws) }
-
- it_behaves_like 'configure istio service'
- end
-
- context 'when provider type is user' do
- let(:cluster) { create(:cluster, :project, :provided_by_user) }
-
- it_behaves_like 'configure istio service'
- end
-
- context 'when cluster does not exist' do
- it 'does not provision a cluster' do
- expect_any_instance_of(Clusters::Kubernetes::ConfigureIstioIngressService).not_to receive(:execute)
-
- described_class.new.perform(123)
- end
- end
- end
-end
diff --git a/spec/workers/cluster_update_app_worker_spec.rb b/spec/workers/cluster_update_app_worker_spec.rb
deleted file mode 100644
index 8f61ee17162..00000000000
--- a/spec/workers/cluster_update_app_worker_spec.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ClusterUpdateAppWorker do
- include ExclusiveLeaseHelpers
-
- let_it_be(:project) { create(:project) }
-
- let(:prometheus_update_service) { spy }
-
- subject { described_class.new }
-
- around do |example|
- freeze_time { example.run }
- end
-
- before do
- allow(::Clusters::Applications::PrometheusUpdateService).to receive(:new).and_return(prometheus_update_service)
- end
-
- describe '#perform' do
- context 'when the application last_update_started_at is higher than the time the job was scheduled in' do
- it 'does nothing' do
- application = create(:clusters_applications_prometheus, :updated, last_update_started_at: Time.current)
-
- expect(prometheus_update_service).not_to receive(:execute)
-
- expect(subject.perform(application.name, application.id, project.id, Time.current - 5.minutes)).to be_nil
- end
- end
-
- context 'when another worker is already running' do
- it 'returns nil' do
- application = create(:clusters_applications_prometheus, :updating)
-
- expect(subject.perform(application.name, application.id, project.id, Time.current)).to be_nil
- end
- end
-
- it 'executes PrometheusUpdateService' do
- application = create(:clusters_applications_prometheus, :installed)
-
- expect(prometheus_update_service).to receive(:execute)
-
- subject.perform(application.name, application.id, project.id, Time.current)
- end
-
- context 'application is externally installed' do
- it 'does not execute PrometheusUpdateService' do
- application = create(:clusters_applications_prometheus, :externally_installed)
-
- expect(prometheus_update_service).not_to receive(:execute)
-
- subject.perform(application.name, application.id, project.id, Time.current)
- end
- end
-
- context 'with exclusive lease' do
- let_it_be(:user) { create(:user) }
-
- let(:application) { create(:clusters_applications_prometheus, :installed) }
- let(:lease_key) { "#{described_class.name.underscore}-#{application.id}" }
-
- before do
- # update_highest_role uses exclusive key too:
- allow(Gitlab::ExclusiveLease).to receive(:new).and_call_original
- stub_exclusive_lease_taken(lease_key)
- end
-
- it 'does not allow same app to be updated concurrently by same project' do
- expect(Clusters::Applications::PrometheusUpdateService).not_to receive(:new)
-
- subject.perform(application.name, application.id, project.id, Time.current)
- end
-
- it 'does not allow same app to be updated concurrently by different project', :aggregate_failures do
- project1 = create(:project, namespace: create(:namespace, owner: user))
-
- expect(Clusters::Applications::PrometheusUpdateService).not_to receive(:new)
-
- subject.perform(application.name, application.id, project1.id, Time.current)
- end
-
- it 'allows different app to be updated concurrently by same project' do
- application2 = create(:clusters_applications_prometheus, :installed)
- lease_key2 = "#{described_class.name.underscore}-#{application2.id}"
-
- stub_exclusive_lease(lease_key2)
-
- expect(Clusters::Applications::PrometheusUpdateService).to receive(:new)
- .with(application2, project)
-
- subject.perform(application2.name, application2.id, project.id, Time.current)
- end
-
- it 'allows different app to be updated by different project', :aggregate_failures do
- application2 = create(:clusters_applications_prometheus, :installed)
- lease_key2 = "#{described_class.name.underscore}-#{application2.id}"
-
- project2 = create(:project, namespace: create(:namespace, owner: user))
-
- stub_exclusive_lease(lease_key2)
-
- expect(Clusters::Applications::PrometheusUpdateService).to receive(:new)
- .with(application2, project2)
-
- subject.perform(application2.name, application2.id, project2.id, Time.current)
- end
- end
- end
-end
diff --git a/spec/workers/cluster_wait_for_app_update_worker_spec.rb b/spec/workers/cluster_wait_for_app_update_worker_spec.rb
deleted file mode 100644
index b7f7622a0e6..00000000000
--- a/spec/workers/cluster_wait_for_app_update_worker_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ClusterWaitForAppUpdateWorker do
- let(:check_upgrade_progress_service) { spy }
-
- before do
- allow(::Clusters::Applications::CheckUpgradeProgressService).to receive(:new).and_return(check_upgrade_progress_service)
- end
-
- it 'runs CheckUpgradeProgressService when application is found' do
- application = create(:clusters_applications_prometheus)
-
- expect(check_upgrade_progress_service).to receive(:execute)
-
- subject.perform(application.name, application.id)
- end
-
- it 'does not run CheckUpgradeProgressService when application is not found' do
- expect(check_upgrade_progress_service).not_to receive(:execute)
-
- expect do
- subject.perform("prometheus", -1)
- end.to raise_error(ActiveRecord::RecordNotFound)
- end
-end
diff --git a/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb
deleted file mode 100644
index 7a42c988a92..00000000000
--- a/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ClusterWaitForIngressIpAddressWorker do
- describe '#perform' do
- let(:service) { instance_double(Clusters::Applications::CheckIngressIpAddressService, execute: true) }
- let(:application) { instance_double(Clusters::Applications::Ingress) }
- let(:worker) { described_class.new }
-
- before do
- allow(worker)
- .to receive(:find_application)
- .with('ingress', 117)
- .and_yield(application)
-
- allow(Clusters::Applications::CheckIngressIpAddressService)
- .to receive(:new)
- .with(application)
- .and_return(service)
-
- allow(described_class)
- .to receive(:perform_in)
- end
-
- it 'finds the application and calls CheckIngressIpAddressService#execute' do
- worker.perform('ingress', 117)
-
- expect(service).to have_received(:execute)
- end
- end
-end
diff --git a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
deleted file mode 100644
index d1dd1cd738b..00000000000
--- a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do
- let(:app) { create(:clusters_applications_helm) }
- let(:app_name) { app.name }
- let(:app_id) { app.id }
-
- subject { described_class.new.perform(app_name, app_id) }
-
- context 'when app exists' do
- let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) }
-
- it 'calls the check service' do
- expect(Clusters::Applications::CheckUninstallProgressService).to receive(:new).with(app).and_return(service)
- expect(service).to receive(:execute).once
-
- subject
- end
- end
-
- context 'when app does not exist' do
- let(:app_id) { 0 }
-
- it 'does not call the check service' do
- expect(Clusters::Applications::CheckUninstallProgressService).not_to receive(:new)
-
- expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
- end
- end
-end
diff --git a/spec/workers/concerns/reenqueuer_spec.rb b/spec/workers/concerns/reenqueuer_spec.rb
index 56db2239bb1..e7287b55af2 100644
--- a/spec/workers/concerns/reenqueuer_spec.rb
+++ b/spec/workers/concerns/reenqueuer_spec.rb
@@ -121,14 +121,7 @@ RSpec.describe Reenqueuer::ReenqueuerSleeper do
# Unit test ensure_minimum_duration
describe '#ensure_minimum_duration' do
around do |example|
- # Allow Timecop.travel without the block form
- Timecop.safe_mode = false
-
- Timecop.freeze do
- example.run
- end
-
- Timecop.safe_mode = true
+ freeze_time { example.run }
end
let(:minimum_duration) { 4.seconds }
@@ -140,31 +133,31 @@ RSpec.describe Reenqueuer::ReenqueuerSleeper do
expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
dummy.ensure_minimum_duration(minimum_duration) do
- Timecop.travel(minimum_duration - time_left)
+ travel(minimum_duration - time_left)
end
end
end
context 'when the block completes just before the minimum duration' do
- let(:time_left) { 0.1.seconds }
+ let(:time_left) { 1.second }
it 'sleeps until the minimum duration' do
expect(dummy).to receive(:sleep).with(a_value_within(0.01).of(time_left))
dummy.ensure_minimum_duration(minimum_duration) do
- Timecop.travel(minimum_duration - time_left)
+ travel(minimum_duration - time_left)
end
end
end
context 'when the block completes just after the minimum duration' do
- let(:time_over) { 0.1.seconds }
+ let(:time_over) { 1.second }
it 'does not sleep' do
expect(dummy).not_to receive(:sleep)
dummy.ensure_minimum_duration(minimum_duration) do
- Timecop.travel(minimum_duration + time_over)
+ travel(minimum_duration + time_over)
end
end
end
@@ -176,7 +169,7 @@ RSpec.describe Reenqueuer::ReenqueuerSleeper do
expect(dummy).not_to receive(:sleep)
dummy.ensure_minimum_duration(minimum_duration) do
- Timecop.travel(minimum_duration + time_over)
+ travel(minimum_duration + time_over)
end
end
end
diff --git a/spec/workers/container_registry/cleanup_worker_spec.rb b/spec/workers/container_registry/cleanup_worker_spec.rb
new file mode 100644
index 00000000000..ffcb421ce1e
--- /dev/null
+++ b/spec/workers/container_registry/cleanup_worker_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::CleanupWorker, :aggregate_failures do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ let_it_be_with_reload(:container_repository) { create(:container_repository) }
+
+ subject(:perform) { worker.perform }
+
+ context 'with no delete scheduled container repositories' do
+ it "doesn't enqueue delete container repository jobs" do
+ expect(ContainerRegistry::DeleteContainerRepositoryWorker).not_to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
+ context 'with delete scheduled container repositories' do
+ before do
+ container_repository.delete_scheduled!
+ end
+
+ it 'enqueues delete container repository jobs' do
+ expect(ContainerRegistry::DeleteContainerRepositoryWorker).to receive(:perform_with_capacity)
+
+ perform
+ end
+ end
+
+ context 'with stale delete ongoing container repositories' do
+ let(:delete_started_at) { (described_class::STALE_DELETE_THRESHOLD + 5.minutes).ago }
+
+ before do
+ container_repository.update!(status: :delete_ongoing, delete_started_at: delete_started_at)
+ end
+
+ it 'resets them and enqueue delete container repository jobs' do
+ expect(ContainerRegistry::DeleteContainerRepositoryWorker).to receive(:perform_with_capacity)
+
+ expect { perform }
+ .to change { container_repository.reload.status }.from('delete_ongoing').to('delete_scheduled')
+ .and change { container_repository.reload.delete_started_at }.to(nil)
+ end
+ end
+
+ context 'for counts logging' do
+ let_it_be(:delete_started_at) { (described_class::STALE_DELETE_THRESHOLD + 5.minutes).ago }
+ let_it_be(:stale_delete_container_repository) do
+ create(:container_repository, :status_delete_ongoing, delete_started_at: delete_started_at)
+ end
+
+ before do
+ container_repository.delete_scheduled!
+ end
+
+ it 'logs the counts' do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_scheduled_container_repositories_count, 1)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:stale_delete_container_repositories_count, 1)
+
+ perform
+ end
+ end
+
+ context 'with container_registry_delete_repository_with_cron_worker disabled' do
+ before do
+ stub_feature_flags(container_registry_delete_repository_with_cron_worker: false)
+ end
+
+ it 'does not run' do
+ expect(worker).not_to receive(:reset_stale_deletes)
+ expect(worker).not_to receive(:enqueue_delete_container_repository_jobs)
+ expect(worker).not_to receive(:log_counts)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/container_registry/delete_container_repository_worker_spec.rb b/spec/workers/container_registry/delete_container_repository_worker_spec.rb
new file mode 100644
index 00000000000..381e0cc164c
--- /dev/null
+++ b/spec/workers/container_registry/delete_container_repository_worker_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ContainerRegistry::DeleteContainerRepositoryWorker, :aggregate_failures do
+ let_it_be_with_reload(:container_repository) { create(:container_repository) }
+ let_it_be(:second_container_repository) { create(:container_repository) }
+
+ let(:worker) { described_class.new }
+
+ describe '#perform_work' do
+ subject(:perform_work) { worker.perform_work }
+
+ context 'with no work to do - no container repositories pending deletion' do
+ it 'will not delete any container repository' do
+ expect(::Projects::ContainerRepository::CleanupTagsService).not_to receive(:new)
+
+ expect { perform_work }.to not_change { ContainerRepository.count }
+ end
+ end
+
+ context 'with work to do' do
+ let(:tags_count) { 0 }
+ let(:cleanup_tags_service_response) { { status: :success, original_size: 100, deleted_size: 0 } }
+ let(:cleanup_tags_service_double) do
+ instance_double('Projects::ContainerRepository::CleanupTagsService', execute: cleanup_tags_service_response)
+ end
+
+ before do
+ container_repository.delete_scheduled!
+ allow(Projects::ContainerRepository::CleanupTagsService)
+ .to receive(:new)
+ .with(container_repository: container_repository, params: described_class::CLEANUP_TAGS_SERVICE_PARAMS)
+ .and_return(cleanup_tags_service_double)
+ end
+
+ it 'picks and destroys the delete scheduled container repository' do
+ expect_next_pending_destruction_container_repository do |repo|
+ expect_logs_on(repo, tags_size_before_delete: 100, deleted_tags_size: 0)
+ expect(repo).to receive(:destroy!).and_call_original
+ end
+ perform_work
+ expect(ContainerRepository.all).to contain_exactly(second_container_repository)
+ end
+
+ context 'with an error during the tags cleanup' do
+ let(:cleanup_tags_service_response) { { status: :error, original_size: 100, deleted_size: 0 } }
+
+ it 'does not delete the container repository' do
+ expect_next_pending_destruction_container_repository do |repo|
+ expect_logs_on(repo, tags_size_before_delete: 100, deleted_tags_size: 0)
+ expect(repo).to receive(:set_delete_scheduled_status).and_call_original
+ expect(repo).not_to receive(:destroy!)
+ end
+ expect { perform_work }.to not_change(ContainerRepository, :count)
+ .and not_change { container_repository.reload.status }
+ expect(container_repository.delete_started_at).to eq(nil)
+ end
+ end
+
+ context 'with an error during the destroy' do
+ it 'does not delete the container repository' do
+ expect_next_pending_destruction_container_repository do |repo|
+ expect_logs_on(repo, tags_size_before_delete: 100, deleted_tags_size: 0)
+ expect(repo).to receive(:destroy!).and_raise('Error!')
+ expect(repo).to receive(:set_delete_scheduled_status).and_call_original
+ end
+
+ expect(::Gitlab::ErrorTracking).to receive(:log_exception)
+ .with(instance_of(RuntimeError), class: described_class.name)
+ expect { perform_work }.to not_change(ContainerRepository, :count)
+ .and not_change { container_repository.reload.status }
+ expect(container_repository.delete_started_at).to eq(nil)
+ end
+ end
+
+ context 'with tags left to destroy' do
+ let(:tags_count) { 10 }
+
+ it 'does not delete the container repository' do
+ expect_next_pending_destruction_container_repository do |repo|
+ expect(repo).not_to receive(:destroy!)
+ expect(repo).to receive(:set_delete_scheduled_status).and_call_original
+ end
+
+ expect { perform_work }.to not_change(ContainerRepository, :count)
+ .and not_change { container_repository.reload.status }
+ expect(container_repository.delete_started_at).to eq(nil)
+ end
+ end
+
+ context 'with no tags on the container repository' do
+ let(:tags_count) { 0 }
+ let(:cleanup_tags_service_response) { { status: :success, original_size: 0, deleted_size: 0 } }
+
+ it 'picks and destroys the delete scheduled container repository' do
+ expect_next_pending_destruction_container_repository do |repo|
+ expect_logs_on(repo, tags_size_before_delete: 0, deleted_tags_size: 0)
+ expect(repo).to receive(:destroy!).and_call_original
+ end
+ perform_work
+ expect(ContainerRepository.all).to contain_exactly(second_container_repository)
+ end
+ end
+
+ def expect_next_pending_destruction_container_repository
+ original_method = ContainerRepository.method(:next_pending_destruction)
+ expect(ContainerRepository).to receive(:next_pending_destruction).with(order_by: nil) do
+ original_method.call(order_by: nil).tap do |repo|
+ allow(repo).to receive(:tags_count).and_return(tags_count)
+ expect(repo).to receive(:set_delete_ongoing_status).and_call_original
+ yield repo
+ end
+ end
+ end
+
+ def expect_logs_on(container_repository, tags_size_before_delete:, deleted_tags_size:)
+ payload = {
+ project_id: container_repository.project.id,
+ container_repository_id: container_repository.id,
+ container_repository_path: container_repository.path,
+ tags_size_before_delete: tags_size_before_delete,
+ deleted_tags_size: deleted_tags_size
+ }
+ expect(worker.logger).to receive(:info).with(worker.structured_payload(payload))
+ .and_call_original
+ end
+ end
+ end
+
+ describe '#max_running_jobs' do
+ subject { worker.max_running_jobs }
+
+ it { is_expected.to eq(described_class::MAX_CAPACITY) }
+ end
+
+ describe '#remaining_work_count' do
+ let_it_be(:delete_scheduled_container_repositories) do
+ create_list(:container_repository, described_class::MAX_CAPACITY + 2, :status_delete_scheduled)
+ end
+
+ subject { worker.remaining_work_count }
+
+ it { is_expected.to eq(described_class::MAX_CAPACITY + 1) }
+ end
+end
diff --git a/spec/workers/database/batched_background_migration/execution_worker_spec.rb b/spec/workers/database/batched_background_migration/execution_worker_spec.rb
new file mode 100644
index 00000000000..9a850a98f2f
--- /dev/null
+++ b/spec/workers/database/batched_background_migration/execution_worker_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Database::BatchedBackgroundMigration::ExecutionWorker, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
+ describe '#perform' do
+ let(:database_name) { Gitlab::Database::MAIN_DATABASE_NAME.to_sym }
+ let(:base_model) { Gitlab::Database.database_base_models[database_name] }
+ let(:table_name) { :events }
+ let(:job_interval) { 5.minutes }
+ let(:lease_timeout) { job_interval * described_class::LEASE_TIMEOUT_MULTIPLIER }
+ let(:interval_variance) { described_class::INTERVAL_VARIANCE }
+
+ subject(:worker) { described_class.new }
+
+ context 'when the feature flag is disabled' do
+ let(:migration) do
+ create(:batched_background_migration, :active, interval: job_interval, table_name: table_name)
+ end
+
+ before do
+ stub_feature_flags(execute_batched_migrations_on_schedule: false)
+ end
+
+ it 'does nothing' do
+ expect(Gitlab::Database::BackgroundMigration::BatchedMigration).not_to receive(:find_executable)
+ expect(worker).not_to receive(:run_migration_job)
+
+ worker.perform(database_name, migration.id)
+ end
+ end
+
+ context 'when the feature flag is enabled' do
+ before do
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
+ end
+
+ context 'when the provided database is sharing config' do
+ before do
+ skip_if_multiple_databases_not_setup
+ end
+
+ it 'does nothing' do
+ ci_model = Gitlab::Database.database_base_models['ci']
+ expect(Gitlab::Database).to receive(:db_config_share_with)
+ .with(ci_model.connection_db_config).and_return('main')
+
+ expect(Gitlab::Database::BackgroundMigration::BatchedMigration).not_to receive(:find_executable)
+ expect(worker).not_to receive(:run_migration_job)
+
+ worker.perform(:ci, 123)
+ end
+ end
+
+ context 'when migration does not exist' do
+ it 'does nothing' do
+ expect(worker).not_to receive(:run_migration_job)
+
+ worker.perform(database_name, non_existing_record_id)
+ end
+ end
+
+ context 'when migration exist' do
+ let(:migration) do
+ create(:batched_background_migration, :active, interval: job_interval, table_name: table_name)
+ end
+
+ before do
+ allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:find_executable)
+ .with(migration.id, connection: base_model.connection)
+ .and_return(migration)
+ end
+
+ context 'when the migration is no longer active' do
+ it 'does not run the migration' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield
+
+ expect(migration).to receive(:active?).and_return(false)
+
+ expect(worker).not_to receive(:run_migration_job)
+
+ worker.perform(database_name, migration.id)
+ end
+ end
+
+ context 'when the interval has not elapsed' do
+ it 'does not run the migration' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield
+ expect(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(false)
+ expect(worker).not_to receive(:run_migration_job)
+
+ worker.perform(database_name, migration.id)
+ end
+ end
+
+ context 'when the migration is still active and the interval has elapsed' do
+ let(:table_name_lease_key) do
+ "#{described_class.name.underscore}:database_name:#{database_name}:" \
+ "table_name:#{table_name}"
+ end
+
+ context 'when can not obtain lease on the table name' do
+ it 'does nothing' do
+ stub_exclusive_lease_taken(table_name_lease_key, timeout: lease_timeout)
+
+ expect(worker).not_to receive(:run_migration_job)
+
+ worker.perform(database_name, migration.id)
+ end
+ end
+
+ it 'always cleans up the exclusive lease' do
+ expect_to_obtain_exclusive_lease(table_name_lease_key, 'uuid-table-name', timeout: lease_timeout)
+ expect_to_cancel_exclusive_lease(table_name_lease_key, 'uuid-table-name')
+
+ expect(worker).to receive(:run_migration_job).and_raise(RuntimeError, 'I broke')
+
+ expect { worker.perform(database_name, migration.id) }.to raise_error(RuntimeError, 'I broke')
+ end
+
+ it 'runs the migration' do
+ expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield
+
+ expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |instance|
+ expect(instance).to receive(:run_migration_job).with(migration)
+ end
+
+ expect_to_obtain_exclusive_lease(table_name_lease_key, 'uuid-table-name', timeout: lease_timeout)
+ expect_to_cancel_exclusive_lease(table_name_lease_key, 'uuid-table-name')
+
+ expect(worker).to receive(:run_migration_job).and_call_original
+
+ worker.perform(database_name, migration.id)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 322f516fbeb..e705ca28e54 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -188,6 +188,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Clusters::Cleanup::ProjectNamespaceWorker' => 3,
'Clusters::Cleanup::ServiceAccountWorker' => 3,
'ContainerExpirationPolicies::CleanupContainerRepositoryWorker' => 0,
+ 'ContainerRegistry::DeleteContainerRepositoryWorker' => 0,
'CreateCommitSignatureWorker' => 3,
'CreateGithubWebhookWorker' => 3,
'CreateNoteDiffFileWorker' => 3,
@@ -269,6 +270,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Gitlab::GithubImport::ImportProtectedBranchWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestMergedByWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestReviewWorker' => 5,
+ 'Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestWorker' => 5,
'Gitlab::GithubImport::RefreshImportJidWorker' => 5,
'Gitlab::GithubImport::Stage::FinishImportWorker' => 5,
@@ -280,6 +282,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Gitlab::GithubImport::Stage::ImportProtectedBranchesWorker' => 5,
'Gitlab::GithubImport::Stage::ImportNotesWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker' => 5,
+ 'Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportPullRequestsWorker' => 5,
'Gitlab::GithubImport::Stage::ImportRepositoryWorker' => 5,
@@ -339,6 +342,7 @@ RSpec.describe 'Every Sidekiq worker' do
'MergeRequests::AssigneesChangeWorker' => 3,
'MergeRequests::CreatePipelineWorker' => 3,
'MergeRequests::DeleteSourceBranchWorker' => 3,
+ 'MergeRequests::FetchSuggestedReviewersWorker' => 3,
'MergeRequests::HandleAssigneesChangeWorker' => 3,
'MergeRequests::ResolveTodosWorker' => 3,
'MergeRequests::SyncCodeOwnerApprovalRulesWorker' => 3,
@@ -398,6 +402,7 @@ RSpec.describe 'Every Sidekiq worker' do
'Projects::ScheduleBulkRepositoryShardMovesWorker' => 3,
'Projects::UpdateRepositoryStorageWorker' => 3,
'Projects::RefreshBuildArtifactsSizeStatisticsWorker' => 0,
+ 'Projects::RegisterSuggestedReviewersProjectWorker' => 3,
'PropagateIntegrationGroupWorker' => 3,
'PropagateIntegrationInheritDescendantWorker' => 3,
'PropagateIntegrationInheritWorker' => 3,
diff --git a/spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb b/spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb
new file mode 100644
index 00000000000..fdcbfb18beb
--- /dev/null
+++ b/spec/workers/gitlab/github_import/pull_requests/import_review_request_worker_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker do
+ subject(:worker) { described_class.new }
+
+ describe '#import' do
+ let(:import_state) { build_stubbed(:import_state, :started) }
+
+ let(:project) do
+ instance_double('Project', full_path: 'foo/bar', id: 1, import_state: import_state)
+ end
+
+ let(:client) { instance_double('Gitlab::GithubImport::Client') }
+ let(:importer) { instance_double('Gitlab::GithubImport::Importer::IssueEventImporter') }
+
+ let(:review_request_hash) do
+ {
+ 'merge_request_id' => 6501124486,
+ 'users' => [
+ { 'id' => 4, 'login' => 'alice' },
+ { 'id' => 5, 'login' => 'bob' }
+ ]
+ }
+ end
+
+ it 'imports an pull request review requests' do
+ expect(Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImporter)
+ .to receive(:new)
+ .with(
+ an_instance_of(Gitlab::GithubImport::Representation::PullRequests::ReviewRequests),
+ project,
+ client
+ )
+ .and_return(importer)
+
+ expect(importer).to receive(:execute)
+
+ expect(Gitlab::GithubImport::ObjectCounter)
+ .to receive(:increment).with(project, :pull_request_review_request, :imported)
+
+ worker.import(project, client, review_request_hash)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
index 6fcb5db2a54..5d6dcdc10ee 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_merged_by_worker_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker do
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
- .with(project.id, { '123' => 2 }, :pull_request_reviews)
+ .with(project.id, { '123' => 2 }, :pull_request_review_requests)
worker.import(client, project)
end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
new file mode 100644
index 00000000000..151de9bdffc
--- /dev/null
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_review_requests_worker_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewRequestsWorker do
+ subject(:worker) { described_class.new }
+
+ let(:project) { instance_double(Project, id: 1, import_state: import_state) }
+ let(:import_state) { instance_double(ProjectImportState, refresh_jid_expiration: true) }
+ let(:client) { instance_double(Gitlab::GithubImport::Client) }
+ let(:importer) { instance_double(Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImporter) }
+ let(:waiter) { Gitlab::JobWaiter.new(2, '123') }
+
+ describe '#import' do
+ it 'imports all PR review requests' do
+ expect(Gitlab::GithubImport::Importer::PullRequests::ReviewRequestsImporter)
+ .to receive(:new)
+ .with(project, client)
+ .and_return(importer)
+
+ expect(importer).to receive(:execute).and_return(waiter)
+ expect(import_state).to receive(:refresh_jid_expiration)
+
+ expect(Gitlab::GithubImport::AdvanceStageWorker)
+ .to receive(:perform_async)
+ .with(project.id, { '123' => 2 }, :pull_request_reviews)
+
+ worker.import(client, project)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
index 75d4d2dff2e..18a70273219 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker_spec.rb
@@ -23,8 +23,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsReviewsWorker do
.to receive(:execute)
.and_return(waiter)
- expect(import_state)
- .to receive(:refresh_jid_expiration)
+ expect(import_state).to receive(:refresh_jid_expiration)
expect(Gitlab::GithubImport::AdvanceStageWorker)
.to receive(:perform_async)
diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb
index c46ef87333a..a5419291d35 100644
--- a/spec/workers/gitlab_shell_worker_spec.rb
+++ b/spec/workers/gitlab_shell_worker_spec.rb
@@ -2,37 +2,45 @@
require 'spec_helper'
-RSpec.describe GitlabShellWorker do
- let(:worker) { described_class.new }
-
+RSpec.describe GitlabShellWorker, :sidekiq_inline do
describe '#perform' do
- describe '#add_key' do
- it 'delegates to Gitlab::AuthorizedKeys' do
- expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance|
- expect(instance).to receive(:add_key).with('foo', 'bar')
+ Gitlab::Shell::PERMITTED_ACTIONS.each do |action|
+ describe "with the #{action} action" do
+ it 'forwards the message to Gitlab::Shell' do
+ expect_next_instance_of(Gitlab::Shell) do |instance|
+ expect(instance).to respond_to(action)
+ expect(instance).to receive(action).with('foo', 'bar')
+ end
+
+ described_class.perform_async(action, 'foo', 'bar')
end
-
- worker.perform('add_key', 'foo', 'bar')
end
end
- describe '#remove_key' do
- it 'delegates to Gitlab::AuthorizedKeys' do
- expect_next_instance_of(Gitlab::AuthorizedKeys) do |instance|
- expect(instance).to receive(:remove_key).with('foo', 'bar')
+ describe 'all other commands' do
+ context 'when verify_gitlab_shell_worker_method_names is enabled' do
+ it 'raises ArgumentError' do
+ allow_next_instance_of(described_class) do |job_instance|
+ expect(job_instance).not_to receive(:gitlab_shell)
+ end
+
+ expect { described_class.perform_async('foo', 'bar', 'baz') }
+ .to raise_error(ArgumentError, 'foo not allowed for GitlabShellWorker')
end
-
- worker.perform('remove_key', 'foo', 'bar')
end
- end
- describe 'all other commands' do
- it 'delegates them to Gitlab::Shell' do
- expect_next_instance_of(Gitlab::Shell) do |instance|
- expect(instance).to receive(:foo).with('bar', 'baz')
+ context 'when verify_gitlab_shell_worker_method_names is disabled' do
+ before do
+ stub_feature_flags(verify_gitlab_shell_worker_method_names: false)
end
- worker.perform('foo', 'bar', 'baz')
+ it 'forwards the message to Gitlab::Shell' do
+ expect_next_instance_of(Gitlab::Shell) do |instance|
+ expect(instance).to receive('foo').with('bar', 'baz')
+ end
+
+ described_class.perform_async('foo', 'bar', 'baz')
+ end
end
end
end
diff --git a/spec/workers/incident_management/add_severity_system_note_worker_spec.rb b/spec/workers/incident_management/add_severity_system_note_worker_spec.rb
index bda6f729759..4d6e6610a92 100644
--- a/spec/workers/incident_management/add_severity_system_note_worker_spec.rb
+++ b/spec/workers/incident_management/add_severity_system_note_worker_spec.rb
@@ -14,28 +14,41 @@ RSpec.describe IncidentManagement::AddSeveritySystemNoteWorker do
subject(:perform) { described_class.new.perform(incident_id, user_id) }
- shared_examples 'does not add a system note' do
+ shared_examples 'does not add anything' do
it 'does not change incident notes count' do
expect { perform }.not_to change { incident.notes.count }
end
+
+ it 'does not create a timeline event' do
+ expect(IncidentManagement::TimelineEvents::CreateService).not_to receive(:change_severity)
+ perform
+ end
end
context 'when incident and user exist' do
it 'creates a system note' do
expect { perform }.to change { incident.notes.where(author: user).count }.by(1)
end
+
+ it 'creates a timeline event' do
+ expect(IncidentManagement::TimelineEvents::CreateService)
+ .to receive(:change_severity)
+ .with(incident, user)
+ .and_call_original
+ perform
+ end
end
context 'when incident does not exist' do
let(:incident_id) { -1 }
- it_behaves_like 'does not add a system note'
+ it_behaves_like 'does not add anything'
end
context 'when incident_id is nil' do
let(:incident_id) { nil }
- it_behaves_like 'does not add a system note'
+ it_behaves_like 'does not add anything'
end
context 'when issue is not an incident' do
@@ -43,19 +56,19 @@ RSpec.describe IncidentManagement::AddSeveritySystemNoteWorker do
let(:incident_id) { issue.id }
- it_behaves_like 'does not add a system note'
+ it_behaves_like 'does not add anything'
end
context 'when user does not exist' do
let(:user_id) { -1 }
- it_behaves_like 'does not add a system note'
+ it_behaves_like 'does not add anything'
end
context 'when user_id is nil' do
let(:user_id) { nil }
- it_behaves_like 'does not add a system note'
+ it_behaves_like 'does not add anything'
end
end
end
diff --git a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
index 77190dc49d9..09d58a1189e 100644
--- a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
+++ b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb
@@ -105,9 +105,10 @@ RSpec.describe LooseForeignKeys::CleanupWorker do
def perform_for(db:)
time = Time.current.midnight
- if db == :main
+ case db
+ when :main
time += 2.minutes
- elsif db == :ci
+ when :ci
time += 3.minutes
end
@@ -124,37 +125,6 @@ RSpec.describe LooseForeignKeys::CleanupWorker do
expect(loose_fk_child_table_2_1.count).to eq(0)
end
- context 'when deleting in batches' do
- before do
- stub_const('LooseForeignKeys::CleanupWorker::BATCH_SIZE', 2)
- end
-
- it 'cleans up all rows' do
- expect(LooseForeignKeys::BatchCleanerService).to receive(:new).exactly(:twice).and_call_original
-
- perform_for(db: :main)
-
- expect(loose_fk_child_table_1_1.count).to eq(0)
- expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
- expect(loose_fk_child_table_2_1.count).to eq(0)
- end
- end
-
- context 'when the deleted rows count limit have been reached' do
- def count_deletable_rows
- loose_fk_child_table_1_1.count + loose_fk_child_table_2_1.count
- end
-
- before do
- stub_const('LooseForeignKeys::ModificationTracker::MAX_DELETES', 2)
- stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1)
- end
-
- it 'cleans up 2 rows' do
- expect { perform_for(db: :main) }.to change { count_deletable_rows }.by(-2)
- end
- end
-
describe 'multi-database support' do
where(:current_minute, :configured_base_models, :expected_connection_model) do
2 | { main: 'ActiveRecord::Base', ci: 'Ci::ApplicationRecord' } | 'ActiveRecord::Base'
diff --git a/spec/workers/mail_scheduler/notification_service_worker_spec.rb b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
index ff4a1646d09..3c17025c152 100644
--- a/spec/workers/mail_scheduler/notification_service_worker_spec.rb
+++ b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
@@ -42,9 +42,42 @@ RSpec.describe MailScheduler::NotificationServiceWorker do
end
end
- context 'when the method is not a public method' do
- it 'raises NoMethodError' do
- expect { worker.perform('notifiable?', *serialize(key)) }.to raise_error(NoMethodError)
+ context 'when the method is allowed' do
+ it 'calls the method on NotificationService' do
+ NotificationService.permitted_actions.each do |action|
+ expect(worker.notification_service).to receive(action).with(key)
+
+ worker.perform(action, *serialize(key))
+ end
+ end
+ end
+
+ context 'when the method is not allowed' do
+ context 'when verify_mail_scheduler_notification_service_worker_method_names is enabled' do
+ it 'raises ArgumentError' do
+ expect(worker.notification_service).not_to receive(:async)
+ expect(worker.notification_service).not_to receive(:foo)
+
+ expect { worker.perform('async', *serialize(key)) }
+ .to raise_error(ArgumentError, 'async not allowed for MailScheduler::NotificationServiceWorker')
+
+ expect { worker.perform('foo', *serialize(key)) }
+ .to raise_error(ArgumentError, 'foo not allowed for MailScheduler::NotificationServiceWorker')
+ end
+ end
+
+ context 'when verify_mail_scheduler_notification_service_worker_method_names is disabled' do
+ before do
+ stub_feature_flags(verify_mail_scheduler_notification_service_worker_method_names: false)
+ end
+
+ it 'forwards the argument to the service' do
+ expect(worker.notification_service).to receive(:async)
+ expect(worker.notification_service).to receive(:foo)
+
+ worker.perform('async', *serialize(key))
+ worker.perform('foo', *serialize(key))
+ end
end
end
end
diff --git a/spec/workers/merge_requests/delete_branch_worker_spec.rb b/spec/workers/merge_requests/delete_branch_worker_spec.rb
new file mode 100644
index 00000000000..80ca8c061f5
--- /dev/null
+++ b/spec/workers/merge_requests/delete_branch_worker_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::DeleteBranchWorker do
+ let_it_be(:merge_request) { create(:merge_request) }
+ let_it_be(:user) { create(:user) }
+
+ let(:branch) { merge_request.source_branch }
+ let(:sha) { merge_request.source_branch_sha }
+ let(:retarget_branch) { true }
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ context 'with a non-existing merge request' do
+ it 'does nothing' do
+ expect(::Branches::DeleteService).not_to receive(:new)
+ worker.perform(non_existing_record_id, user.id, branch, retarget_branch)
+ end
+ end
+
+ context 'with a non-existing user' do
+ it 'does nothing' do
+ expect(::Branches::DeleteService).not_to receive(:new)
+
+ worker.perform(merge_request.id, non_existing_record_id, branch, retarget_branch)
+ end
+ end
+
+ context 'with existing user and merge request' do
+ it 'calls service to delete source branch' do
+ expect_next_instance_of(::Branches::DeleteService) do |instance|
+ expect(instance).to receive(:execute).with(branch)
+ end
+
+ worker.perform(merge_request.id, user.id, branch, retarget_branch)
+ end
+
+ context 'when retarget branch param is true' do
+ it 'calls the retarget chain service' do
+ expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
+ expect(instance).to receive(:execute).with(merge_request)
+ end
+
+ worker.perform(merge_request.id, user.id, branch, retarget_branch)
+ end
+ end
+
+ context 'when retarget branch param is false' do
+ let(:retarget_branch) { false }
+
+ it 'does not call the retarget chain service' do
+ expect(::MergeRequests::RetargetChainService).not_to receive(:new)
+
+ worker.perform(merge_request.id, user.id, branch, retarget_branch)
+ end
+ end
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:merge_request) { create(:merge_request) }
+ let(:job_args) { [merge_request.id, sha, user.id, true] }
+ end
+ end
+end
diff --git a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
index fe677103fd0..2935d3ef5dc 100644
--- a/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
+++ b/spec/workers/merge_requests/delete_source_branch_worker_spec.rb
@@ -10,96 +10,116 @@ RSpec.describe MergeRequests::DeleteSourceBranchWorker do
let(:worker) { described_class.new }
describe '#perform' do
- context 'with a non-existing merge request' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
+ context 'when the add_delete_branch_worker feature flag is enabled' do
+ context 'with a non-existing merge request' do
+ it 'does nothing' do
+ expect(::MergeRequests::DeleteBranchWorker).not_to receive(:perform_async)
- worker.perform(non_existing_record_id, sha, user.id)
+ worker.perform(non_existing_record_id, sha, user.id)
+ end
end
- end
- context 'with a non-existing user' do
- it 'does nothing' do
- expect(::Branches::DeleteService).not_to receive(:new)
- expect(::MergeRequests::RetargetChainService).not_to receive(:new)
+ context 'with a non-existing user' do
+ it 'does nothing' do
+ expect(::MergeRequests::DeleteBranchWorker).not_to receive(:perform_async)
- worker.perform(merge_request.id, sha, non_existing_record_id)
+ worker.perform(merge_request.id, sha, non_existing_record_id)
+ end
end
- end
- context 'with existing user and merge request' do
- it 'calls service to delete source branch' do
- expect_next_instance_of(::Branches::DeleteService) do |instance|
- expect(instance).to receive(:execute).with(merge_request.source_branch)
+ context 'with existing user and merge request' do
+ it 'creates a new delete branch worker async' do
+ expect(::MergeRequests::DeleteBranchWorker).to receive(:perform_async).with(merge_request.id, user.id,
+ merge_request.source_branch, true)
+
+ worker.perform(merge_request.id, sha, user.id)
end
- worker.perform(merge_request.id, sha, user.id)
- end
+ context 'source branch sha does not match' do
+ it 'does nothing' do
+ expect(::MergeRequests::DeleteBranchWorker).not_to receive(:perform_async)
- it 'calls service to try retarget merge requests' do
- expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
- expect(instance).to receive(:execute).with(merge_request)
+ worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
+ end
end
+ end
- worker.perform(merge_request.id, sha, user.id)
+ it_behaves_like 'an idempotent worker' do
+ let(:merge_request) { create(:merge_request) }
+ let(:job_args) { [merge_request.id, sha, user.id] }
+ end
+ end
+
+ context 'when the add_delete_branch_worker feature flag is disabled' do
+ before do
+ stub_feature_flags(add_delete_branch_worker: false)
end
- context 'source branch sha does not match' do
+ context 'with a non-existing merge request' do
it 'does nothing' do
expect(::Branches::DeleteService).not_to receive(:new)
expect(::MergeRequests::RetargetChainService).not_to receive(:new)
- worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
+ worker.perform(non_existing_record_id, sha, user.id)
end
end
- context 'when delete service returns an error' do
- let(:service_result) { ServiceResponse.error(message: 'placeholder') }
+ context 'with a non-existing user' do
+ it 'does nothing' do
+ expect(::Branches::DeleteService).not_to receive(:new)
+ expect(::MergeRequests::RetargetChainService).not_to receive(:new)
+
+ worker.perform(merge_request.id, sha, non_existing_record_id)
+ end
+ end
- it 'tracks the exception' do
+ context 'with existing user and merge request' do
+ it 'calls service to delete source branch' do
expect_next_instance_of(::Branches::DeleteService) do |instance|
- expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result)
+ expect(instance).to receive(:execute).with(merge_request.source_branch)
end
- expect(service_result).to receive(:track_exception).and_call_original
+ worker.perform(merge_request.id, sha, user.id)
+ end
+
+ it 'calls service to try retarget merge requests' do
+ expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
+ expect(instance).to receive(:execute).with(merge_request)
+ end
worker.perform(merge_request.id, sha, user.id)
end
- context 'when track_delete_source_errors is disabled' do
- before do
- stub_feature_flags(track_delete_source_errors: false)
+ context 'source branch sha does not match' do
+ it 'does nothing' do
+ expect(::Branches::DeleteService).not_to receive(:new)
+ expect(::MergeRequests::RetargetChainService).not_to receive(:new)
+
+ worker.perform(merge_request.id, 'new-source-branch-sha', user.id)
end
+ end
+
+ context 'when delete service returns an error' do
+ let(:service_result) { ServiceResponse.error(message: 'placeholder') }
- it 'does not track the exception' do
+ it 'still retargets the merge request' do
expect_next_instance_of(::Branches::DeleteService) do |instance|
expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result)
end
- expect(service_result).not_to receive(:track_exception)
+ expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
+ expect(instance).to receive(:execute).with(merge_request)
+ end
worker.perform(merge_request.id, sha, user.id)
end
end
-
- it 'still retargets the merge request' do
- expect_next_instance_of(::Branches::DeleteService) do |instance|
- expect(instance).to receive(:execute).with(merge_request.source_branch).and_return(service_result)
- end
-
- expect_next_instance_of(::MergeRequests::RetargetChainService) do |instance|
- expect(instance).to receive(:execute).with(merge_request)
- end
-
- worker.perform(merge_request.id, sha, user.id)
- end
end
- end
- it_behaves_like 'an idempotent worker' do
- let(:merge_request) { create(:merge_request) }
- let(:job_args) { [merge_request.id, sha, user.id] }
+ it_behaves_like 'an idempotent worker' do
+ let(:merge_request) { create(:merge_request) }
+ let(:job_args) { [merge_request.id, sha, user.id] }
+ end
end
end
end
diff --git a/spec/workers/namespaces/root_statistics_worker_spec.rb b/spec/workers/namespaces/root_statistics_worker_spec.rb
index 7b774da0bdc..30854415405 100644
--- a/spec/workers/namespaces/root_statistics_worker_spec.rb
+++ b/spec/workers/namespaces/root_statistics_worker_spec.rb
@@ -89,4 +89,17 @@ RSpec.describe Namespaces::RootStatisticsWorker, '#perform' do
.not_to change { Namespace::AggregationSchedule.count }
end
end
+
+ it_behaves_like 'worker with data consistency',
+ described_class,
+ feature_flag: :root_statistics_worker_read_replica,
+ data_consistency: :sticky
+
+ it 'has the `until_executed` deduplicate strategy' do
+ expect(described_class.get_deduplicate_strategy).to eq(:until_executed)
+ end
+
+ it 'has an option to reschedule once if deduplicated' do
+ expect(described_class.get_deduplication_options).to include({ if_deduplicated: :reschedule_once })
+ end
end
diff --git a/spec/workers/pages/invalidate_domain_cache_worker_spec.rb b/spec/workers/pages/invalidate_domain_cache_worker_spec.rb
index b9c27c54fa1..c786d4658d4 100644
--- a/spec/workers/pages/invalidate_domain_cache_worker_spec.rb
+++ b/spec/workers/pages/invalidate_domain_cache_worker_spec.rb
@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe Pages::InvalidateDomainCacheWorker do
shared_examples 'clears caches with' do |event_class:, event_data:, caches:|
- let(:event) do
- event_class.new(data: event_data)
- end
+ include AfterNextHelpers
+
+ let(:event) { event_class.new(data: event_data) }
subject { consume_event(subscriber: described_class, event: event) }
@@ -14,9 +14,8 @@ RSpec.describe Pages::InvalidateDomainCacheWorker do
it 'clears the cache with Gitlab::Pages::CacheControl' do
caches.each do |cache|
- expect_next_instance_of(Gitlab::Pages::CacheControl, type: cache[:type], id: cache[:id]) do |cache_control|
- expect(cache_control).to receive(:clear_cache)
- end
+ expect_next(Gitlab::Pages::CacheControl, type: cache[:type], id: cache[:id])
+ .to receive(:clear_cache)
end
subject
@@ -181,19 +180,17 @@ RSpec.describe Pages::InvalidateDomainCacheWorker do
]
end
- it 'does not clear the cache when the attributes is not pages related' do
- event = Projects::ProjectAttributesChangedEvent.new(
- data: {
- project_id: 1,
- namespace_id: 2,
- root_namespace_id: 3,
- attributes: ['unknown']
- }
- )
-
- expect(described_class).not_to receive(:clear_cache)
-
- ::Gitlab::EventStore.publish(event)
+ it_behaves_like 'ignores the published event' do
+ let(:event) do
+ Projects::ProjectAttributesChangedEvent.new(
+ data: {
+ project_id: 1,
+ namespace_id: 2,
+ root_namespace_id: 3,
+ attributes: ['unknown']
+ }
+ )
+ end
end
end
@@ -204,26 +201,24 @@ RSpec.describe Pages::InvalidateDomainCacheWorker do
project_id: 1,
namespace_id: 2,
root_namespace_id: 3,
- features: ["pages_access_level"]
+ features: ['pages_access_level']
},
caches: [
{ type: :project, id: 1 },
{ type: :namespace, id: 3 }
]
- it 'does not clear the cache when the features is not pages related' do
- event = Projects::ProjectFeaturesChangedEvent.new(
- data: {
- project_id: 1,
- namespace_id: 2,
- root_namespace_id: 3,
- features: ['unknown']
- }
- )
-
- expect(described_class).not_to receive(:clear_cache)
-
- ::Gitlab::EventStore.publish(event)
+ it_behaves_like 'ignores the published event' do
+ let(:event) do
+ Projects::ProjectFeaturesChangedEvent.new(
+ data: {
+ project_id: 1,
+ namespace_id: 2,
+ root_namespace_id: 3,
+ features: ['unknown']
+ }
+ )
+ end
end
end
diff --git a/spec/workers/pages_worker_spec.rb b/spec/workers/pages_worker_spec.rb
index ad714d8d11e..f0d29037fa4 100644
--- a/spec/workers/pages_worker_spec.rb
+++ b/spec/workers/pages_worker_spec.rb
@@ -3,14 +3,26 @@
require 'spec_helper'
RSpec.describe PagesWorker, :sidekiq_inline do
- let(:project) { create(:project) }
- let(:ci_build) { create(:ci_build, project: project) }
+ let_it_be(:ci_build) { create(:ci_build) }
- it 'calls UpdatePagesService' do
- expect_next_instance_of(Projects::UpdatePagesService, project, ci_build) do |service|
- expect(service).to receive(:execute)
+ context 'when called with the deploy action' do
+ it 'calls UpdatePagesService' do
+ expect_next_instance_of(Projects::UpdatePagesService, ci_build.project, ci_build) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ described_class.perform_async(:deploy, ci_build.id)
end
+ end
- described_class.perform_async(:deploy, ci_build.id)
+ context 'when called with any other action' do
+ it 'does nothing' do
+ expect_next_instance_of(described_class) do |job_class|
+ expect(job_class).not_to receive(:foo)
+ expect(job_class).not_to receive(:deploy)
+ end
+
+ described_class.perform_async(:foo)
+ end
end
end
diff --git a/spec/workers/projects/after_import_worker_spec.rb b/spec/workers/projects/after_import_worker_spec.rb
index a14b2443173..85d15c89b0a 100644
--- a/spec/workers/projects/after_import_worker_spec.rb
+++ b/spec/workers/projects/after_import_worker_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Projects::AfterImportWorker do
- include GitHelpers
-
subject { worker.perform(project.id) }
let(:worker) { described_class.new }
diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb
index 3158ac9fa27..732dc540fb7 100644
--- a/spec/workers/projects/post_creation_worker_spec.rb
+++ b/spec/workers/projects/post_creation_worker_spec.rb
@@ -81,6 +81,40 @@ RSpec.describe Projects::PostCreationWorker do
end
end
end
+
+ describe 'Incident timeline event tags' do
+ context 'when project is nil' do
+ let(:job_args) { [nil] }
+
+ it 'does not create event tags' do
+ expect { subject }.not_to change { IncidentManagement::TimelineEventTag.count }
+ end
+ end
+
+ context 'when project is created', :aggregate_failures do
+ it 'creates tags for the project' do
+ expect { subject }.to change { IncidentManagement::TimelineEventTag.count }.by(2)
+
+ expect(project.incident_management_timeline_event_tags.pluck_names).to match_array(
+ [
+ ::IncidentManagement::TimelineEventTag::START_TIME_TAG_NAME,
+ ::IncidentManagement::TimelineEventTag::END_TIME_TAG_NAME
+ ]
+ )
+ end
+
+ it 'raises error if record creation fails' do
+ allow_next_instance_of(IncidentManagement::TimelineEventTag) do |tag|
+ allow(tag).to receive(:valid?).and_return(false)
+ end
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice
+ subject
+
+ expect(project.incident_management_timeline_event_tags).to be_empty
+ end
+ end
+ end
end
end
end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
index 44b8fa21be4..062a9bcfa83 100644
--- a/spec/workers/remove_expired_members_worker_spec.rb
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -56,27 +56,13 @@ RSpec.describe RemoveExpiredMembersWorker do
expect(Member.find_by(user_id: expired_project_bot.id)).to be_nil
end
- context 'when user_destroy_with_limited_execution_time_worker is enabled' do
- it 'initiates project bot removal' do
- worker.perform
-
- expect(
- Users::GhostUserMigration.where(user: expired_project_bot,
- initiator_user: nil)
- ).to be_exists
- end
- end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'deletes expired project bot' do
- worker.perform
+ it 'initiates project bot removal' do
+ worker.perform
- expect(User.exists?(expired_project_bot.id)).to be(false)
- end
+ expect(
+ Users::GhostUserMigration.where(user: expired_project_bot,
+ initiator_user: nil)
+ ).to be_exists
end
end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index dbb24cc047e..b8db262598b 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -6,12 +6,18 @@ require 'fileutils'
RSpec.describe RepositoryCheck::SingleRepositoryWorker do
subject(:worker) { described_class.new }
+ before do
+ allow(::Gitlab::Git::Repository).to receive(:new).and_call_original
+ end
+
it 'skips when the project has no push events' do
project = create(:project, :repository, :wiki_disabled)
project.events.destroy_all # rubocop: disable Cop/DestroyAll
- break_project(project)
- expect(worker).not_to receive(:git_fsck)
+ repository = instance_double(::Gitlab::Git::Repository)
+ allow(::Gitlab::Git::Repository).to receive(:new)
+ .with(project.repository_storage, "#{project.disk_path}.git", anything, anything, container: project)
+ .and_return(repository)
worker.perform(project.id)
@@ -21,7 +27,12 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
it 'fails when the project has push events and a broken repository' do
project = create(:project, :repository)
create_push_event(project)
- break_project(project)
+
+ repository = project.repository.raw
+ expect(repository).to receive(:fsck).and_raise(::Gitlab::Git::Repository::GitError)
+ expect(::Gitlab::Git::Repository).to receive(:new)
+ .with(project.repository_storage, "#{project.disk_path}.git", anything, anything, container: project)
+ .and_return(repository)
worker.perform(project.id)
@@ -32,7 +43,11 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
project = create(:project, :repository, :wiki_disabled)
create_push_event(project)
- expect(worker).to receive(:git_fsck).and_call_original
+ repository = project.repository.raw
+ expect(repository).to receive(:fsck).and_call_original
+ expect(::Gitlab::Git::Repository).to receive(:new)
+ .with(project.repository_storage, "#{project.disk_path}.git", anything, anything, container: project)
+ .and_return(repository)
expect do
worker.perform(project.id)
@@ -50,7 +65,12 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(false)
- break_wiki(project)
+ repository = project.wiki.repository.raw
+ expect(repository).to receive(:fsck).and_raise(::Gitlab::Git::Repository::GitError)
+ expect(::Gitlab::Git::Repository).to receive(:new)
+ .with(project.repository_storage, "#{project.disk_path}.wiki.git", anything, anything, container: project.wiki)
+ .and_return(repository)
+
worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(true)
@@ -59,7 +79,10 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
it 'skips wikis when disabled' do
project = create(:project, :wiki_disabled)
# Make sure the test would fail if the wiki repo was checked
- break_wiki(project)
+ repository = instance_double(::Gitlab::Git::Repository)
+ allow(::Gitlab::Git::Repository).to receive(:new)
+ .with(project.repository_storage, "#{project.disk_path}.wiki.git", anything, anything, container: project)
+ .and_return(repository)
subject.perform(project.id)
@@ -88,31 +111,4 @@ RSpec.describe RepositoryCheck::SingleRepositoryWorker do
def create_push_event(project)
project.events.create!(action: :pushed, author_id: create(:user).id)
end
-
- def break_wiki(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- break_repo(wiki_path(project))
- end
- end
-
- def wiki_path(project)
- project.wiki.repository.path_to_repo
- end
-
- def break_project(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- break_repo(project.repository.path_to_repo)
- end
- end
-
- def break_repo(repo)
- # Create or replace blob ffffffffffffffffffffffffffffffffffffffff with an empty file
- # This will make the repo invalid, _and_ 'git init' cannot fix it.
- path = File.join(repo, 'objects', 'ff')
- file = File.join(path, 'ffffffffffffffffffffffffffffffffffffff')
-
- FileUtils.mkdir_p(path)
- FileUtils.rm_f(file)
- FileUtils.touch(file)
- end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 82d975cb85a..1dc77fbf83f 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -46,36 +46,24 @@ RSpec.describe RepositoryImportWorker do
end
context 'when the import has failed' do
- it 'hide the credentials that were used in the import URL' do
- error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+ it 'updates the error on Import/Export & hides credentials from import URL' do
+ import_url = 'https://user:pass@test.com/root/repoC.git/'
+ error = "#{import_url} not found"
import_state.update!(jid: '123')
- expect_next_instance_of(Projects::ImportService) do |instance|
- expect(instance).to receive(:execute).and_return({ status: :error, message: error })
- end
-
- expect do
- subject.perform(project.id)
- end.to raise_error(RuntimeError, error)
- expect(import_state.reload.jid).not_to be_nil
- expect(import_state.status).to eq('failed')
- end
-
- it 'updates the error on Import/Export' do
- error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
-
project.update!(import_type: 'gitlab_project')
- import_state.update!(jid: '123')
+
expect_next_instance_of(Projects::ImportService) do |instance|
- expect(instance).to receive(:execute).and_return({ status: :error, message: error })
+ expect(instance).to receive(:track_start_import).and_raise(StandardError, error)
end
- expect do
- subject.perform(project.id)
- end.to raise_error(RuntimeError, error)
+ expect { subject.perform(project.id) }.not_to raise_error
- expect(import_state.reload.last_error).not_to be_nil
+ import_state.reload
+ expect(import_state.jid).to eq('123')
expect(import_state.status).to eq('failed')
+ expect(import_state.last_error).to include("[FILTERED] not found")
+ expect(import_state.last_error).not_to include(import_url)
end
end
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index 10c22b736d2..5fa7c5d64db 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -42,14 +42,42 @@ RSpec.describe RunPipelineScheduleWorker do
end
end
- context 'when everything is ok' do
- let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) }
+ describe "#run_pipeline_schedule" do
+ let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService, execute: service_response) }
+ let(:service_response) { instance_double(ServiceResponse, payload: pipeline, error?: false) }
- it 'calls the Service' do
+ before do
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
- expect(create_pipeline_service).to receive(:execute!).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
- worker.perform(pipeline_schedule.id, user.id)
+ expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule).and_return(service_response)
+ end
+
+ context "when pipeline is persisted" do
+ let(:pipeline) { instance_double(Ci::Pipeline, persisted?: true) }
+
+ it "returns the service response" do
+ expect(worker.perform(pipeline_schedule.id, user.id)).to eq(service_response)
+ end
+
+ it "does not log errors" do
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ expect(worker.perform(pipeline_schedule.id, user.id)).to eq(service_response)
+ end
+ end
+
+ context "when pipeline was not persisted" do
+ let(:service_response) { instance_double(ServiceResponse, error?: true, message: "Error", payload: pipeline) }
+ let(:pipeline) { instance_double(Ci::Pipeline, persisted?: false) }
+
+ it "logs a pipeline creation error" do
+ expect(worker)
+ .to receive(:log_extra_metadata_on_done)
+ .with(:pipeline_creation_error, service_response.message)
+ .and_call_original
+
+ expect(worker.perform(pipeline_schedule.id, user.id)).to eq(service_response.message)
+ end
end
end
@@ -82,20 +110,5 @@ RSpec.describe RunPipelineScheduleWorker do
worker.perform(pipeline_schedule.id, user.id)
end
end
-
- context 'when pipeline cannot be created' do
- before do
- allow(Ci::CreatePipelineService).to receive(:new) { raise Ci::CreatePipelineService::CreateError }
- end
-
- it 'logging a pipeline error' do
- expect(worker)
- .to receive(:log_extra_metadata_on_done)
- .with(:pipeline_creation_error, an_instance_of(Ci::CreatePipelineService::CreateError))
- .and_call_original
-
- worker.perform(pipeline_schedule.id, user.id)
- end
- end
end
end
diff --git a/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb b/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb
index f42033fdb9c..7c585542e30 100644
--- a/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb
+++ b/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb
@@ -38,16 +38,4 @@ RSpec.describe Users::MigrateRecordsToGhostUserInBatchesWorker do
expect(issue.last_edited_by).to eq(User.ghost)
end
end
-
- context 'when user_destroy_with_limited_execution_time_worker is disabled' do
- before do
- stub_feature_flags(user_destroy_with_limited_execution_time_worker: false)
- end
-
- it 'does not execute the service' do
- expect(Users::MigrateRecordsToGhostUserInBatchesService).not_to receive(:new)
-
- worker.perform
- end
- end
end